Fast Cowboy microservice: Recipe

  • Dmitry Rubinstein
  • 15 February 2019
  • Comments

Cowboy with Websocket

In nowadays world of microservices and small web applications, Elixir’s ecosystem obviously have it’s own possibilities to implement them. If you want to create small endpoint, process WebHooks for your process or just like to build everything by yourself it’s good idea not to bring to your project heavy-weight frameworks like Phoenix.

Let’s look at how this problem can be easily solved.

TL;DR: Use Aqua to bootstrap ready-to-go project.

  1. $ mix archive.install hex aqua #(only if not installed)
  2. $ mix aqua new plug my_app --ws --static
  3. ???
  4. Profit!

Ingredients

Great Web Application in 2019 should be able to do several things:

  1. Expose HTTP endpoint
  2. Expose WebSocket endpoint
  3. Serve static files

Basicly, all the work for us willi be done by two libraries:

  • Plug - specification and conveniences for composable modules between web applications, that is widely used everywhere in Elixir’s ecosystem;
  • Cowboy - the most popular HTTP server in Erlang’s world (not only HTTP, actually).

We will use special package - plug_cowboy - that will link these two libraries and give us good Elixiry experience with using them.

Directions

We’ll follow all the bootstrapping process step-by-step to understand the basic principles, then we’ll create the same project with one single command using Aqua

Creating project

Our project obviously should be supervised, thus let’s create new project with prebuilt supervision tree:

$ mix new pinger --sup

Adding dependencies

We should add plug and cowboy as a depenency, but plug_cowboy will be right enough, because it has and plug and cowboy in it’s dependency tree, and will bring them to our project.

# ./mix.exs
...

  defp deps do
    [
      {:plug_cowboy, "~> 2.0"}
    ]
  end

...

Get the deps with:

$ mix deps.get

Creating HTTP endpoint

We’ll create HTTP endpoint using Plug specification. This is not Plug tutorial, so we’ll create basic HTTP endpoint with ping-pong GET route just to show the idea.

Following plug_cowboy instructions, we’ll modify our supervision tree to run Cowboy webserver with configured Plug:

# ./lib/pinger/application.ex

defmodule Pinger.Application do
  use Application

  def start(_type, _args) do
    children = [
      Plug.Cowboy.child_spec(scheme: :http, plug: Pinger.Router, options: [port: 4001])
    ]

    opts = [strategy: :one_for_one, name: Pinger.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

And create our simplest plug specification:

# ./lib/pinger/router.ex

defmodule Pinger.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/ping" do
    send_resp(conn, 200, "pong")
  end
end

And start the server:

$ iex -S mix

Now, you can navigate to http://localhost:4001/ping and get your pong output. Everything is working, great? Hm… not so much…

Creating WebSocket endpoint

WebSocket protocol is not a part of HTTP protocol. Thus, it’s not yet presented in Plug library. But we still can use it directly in Cowboy server. Let’s try to add Websocket endpoint to our system.

The problem starts directly here: as you see, we are not working with Cowboy directly, either use Plug.Cowboy wrapper. It’s great in connection with Plug, but how can we add Websocket specification here? It’s possible, but not so obvious, let’s look at the steps.

According to Cowboy’s documentation for sockets, we are creating our ping-pong Websocket handler. With small rewriting Erlang code into Elixir we can get something like this:

# ./lib/pinger/ws.ex

defmodule Pinger.Ws do
  def init(req, opts) do
    {:cowboy_websocket, req, opts}
  end

  def websocket_init(state) do
    {:ok, state}
  end

  def websocket_handle({:text, "ping"}, state) do
    {:reply, {:text, "pong"}, state}
  end

  # Default fallback for unrecognized messages
  def websocket_handle({:text, _}, state) do
    {:ok, state, :hibernate}
  end
end

This module describes Websocket handler with the most basic callbacks - just to show an idea. Our task now is to tell Cowboy dispatch websocket requests to this module.

According to plug_cowboy documentation, we can add :dispatch option to manually configure our Cowboy. While doing this is not directly written in any documentation, because we are passing Erlang configuration into Elixir module, the final dispatch will look something like this (we are adding this code in our application module):

# ./lib/plugger/application.ex

...

  defp dispatch() do
    [
      {:_,
       [
         {"/ws", Plugger.Ws, %{}}
       ]}
    ]
  end

...

You can try to find additional examples and docs here

NOTE: In Erlang application with direct Cowboy calls we’ll need to path these options into (in Elixir dialect) :cowboy_router.compile. But Plug.Cowboy will pass all options given under :dispatch key in this function automaticly for us.

Let’s try to reconfigure our Plug.Cowboy child specification for supervision tree.

# ./lib/pinger/application.ex

...
    Plug.Cowboy.child_spec(scheme: :http, plug: Pinger.Router, options: [port: 4001, dispatch: dispatch()])
...

and start the server with:

$ iex -S mix

Firstly, we’ll check our websocket module (I’m using Simple Websocket Client for Firefox, but you can use whatever you want), we see that it’s working:

Websocket ping-pong

Now, try to navigate to http://localhost:4001/ping to ensure, that HTTP endpoint is still working and see… that it’s not working!

What’s the problem? Sory to say that, but:

:disptach option overrides your :plug!

But what should we do? It’s obvious (but not so simple) - we’ll dispatch to Websocket and Plug.

Let’s modify our dispatch function (the way to this dispatch is extracted from plug_cowboy source code):

# ./lib/plugger/application.ex

...

  defp dispatch() do
    [
      {:_,
       [
         {"/ws", Plugger.Ws, %{}},
         {:_, Plug.Cowboy.Handler, {Pinger.Router, []}}
       ]}
    ]
  end

...

Generally, it will be good idea to remove Plug configuration from child specification (nillify it) - simply not to be confused in future:

# ./lib/pinger/application.ex

...
    Plug.Cowboy.child_spec(scheme: :http, plug: nill, options: [port: 4001, dispatch: dispatch()])
...

Now, check both Websocket and Plug endpoints - they should work as expected.

Serving static files

Upon first consideration, Plug can serve statics by itself - using Plug.Static module. But, while we are already very deep inside spaghetti of Plug and Cowboy, Elixir and Erlang - good idea will be to serve statics directly from Cowboy. In this case we are:

  • not loosing much, because it’s just one line of code - we don’t need to write business logic (in comparison with Plug);
  • not loose possible performanse for dispatching connection from Cowboy to Plug in order to simply serve a file.

In order to do this, we’ll add one simple line in our dispatch function:

# ./lib/plugger/application.ex

...

  defp dispatch() do
    [
      {:_,
       [
         {"/ws", Pinger.Ws, %{}},
         {"/static/[...]", :cowboy_static, {:priv_dir, :pinger, "static"}},
         {:_, Plug.Cowboy.Handler, {Pinger.Router, []}}
       ]}
    ]
  end

...

Now, let’s add some portion of statics in order to ensure that everything is working. Let’s create new file in ./priv/static directory:

<!-- ./priv/static/ping.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Pong</title>
</head>
<body>
    <h1>Pong</h1>
</body>
</html>

Navigate to http://localhost:4001/static/ping.html in order to ensure that everything is working.

Now ensure that Websocket, and Plug are still working - they should, as expected.

Easy way

In order to bootstrap almost the same project in seconds, we can use Aqua Plug template.

In order to use it, firstly install Aqua:

$ mix archive.install hex aqua

Now, create a project:

$ mix aqua new plug pinger --ws --static

We can ommit --ws or --static arguments, if we don’t need Websocket or static endpoints respectedly.

Now, get inside your pinger folder, get the deps with:

$ mix deps.get

Start the application with:

$ iex -S mix

in order to ensure that everything is working, and… start hacking!

Afterwards

If you are stacked, or want to follow the process with dirty hands - download the repo in order to check the code.

Dmitry Rubinstein

Elixir Developer, Architect and Evangelist. Has more then 3 years in Elixir production development, and half a dozen Hex packages under maintaining