Visualizing Parallel Requests in Elixir

Emil Soman  - January 14, 2016  |  

We have been evaluating Elixir at Codemancers and today I was learning how to spin up a minimal HTTP API endpoint using Elixir. Like Rack in Ruby land, Elixir comes with Plug, a swiss army knife for dealing with HTTP connections.

Using Plug to build an HTTP endpoint

First, let’s create a new Elixir project:

$ mix new http_api --sup

This creates a new Elixir OTP app. Let’s add :cowboy and :plug as hex and application dependencies:

# Change the following parts in mix.exs

  def application do
    [applications: [:logger, :cowboy, :plug],
     mod: {HttpApi, []}]
  end

  defp deps do
    [
      {:cowboy, "~>1.0.4"},
      {:plug, "~>1.1.0"}
    ]
  end

Plug comes with a router which we can use to build HTTP endpoints with ease. Let’s create a module to encapsulate the router:

# lib/http_api/router.ex
defmodule HttpApi.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Hello Plug!")
  end

  match _ do
    send_resp(conn, 404, "Nothing here")
  end
end

If you have worked with sinatra-like frameworks, this should look familiar to you. You can read the router docs to understand what everything does if you are curious.

To start the server, we’ll tell the application supervisor to start the Plug’s Cowboy adapter:

# lib/http_api.ex

defmodule HttpApi do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # `start_server` function is used to spawn the worker process
      worker(__MODULE__, [], function: :start_server)
    ]
    opts = [strategy: :one_for_one, name: HttpApi.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Start Cowboy server and use our router
  def start_server do
    { :ok, _ } = Plug.Adapters.Cowboy.http HttpApi.Router, []
  end
end

The complete code for the above example can be found here. You can run the server using:

$ iex -S mix

This starts the interactive Elixir shell and runs your application on the Erlang VM. Now comes the fun part.

Visualizing processes using :observer

In the iex prompt, start the Erlang :observer tool using this command:

iex> :observer.start

This opens a GUI tool that looks like this:

observer

On the left hand side of the Applications panel, you can see a list of all the applications currently running on the Erlang VM - this includes our app (http_api) and all its dependencies, the important ones being cowboy and ranch.

Cowboy and Ranch

Cowboy is a popular HTTP server in the Erlang world and it uses Ranch , another Erlang library, to handle TCP connections behind the scenes. When we start the Plug router, we pass on the router module to Plug’s Cowboy adapter. Now when Cowboy receives a connection, it passes it over to Plug, and Plug runs it through it’s plug pipeline and sends back the request.

Concurrent Requests

Plug by default asks cowboy to start 100 TCP connection acceptor processes in Ranch. You can see the 100 acceptor processes for yourself if you see the application graph of ranch using :observer.

acceptors

Does this mean that we can only have 100 concurrent connections? Let’s find out. We’ll change the number of acceptors to 2 by passing it as a parameter to Plug’s Cowboy adapter:

Plug.Adapters.Cowboy.http HttpApi.Router, [], [acceptors: 2]

Let’s see the how the processes look like now:

acceptors

Okay, so we’ve got only 2 TCP connection acceptor processes running. Let’s try making 5 long running concurrent requests and see what happens.

# lib/http_api/router.ex

# Modify router to add some sleep
defmodule HttpApi.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  # Sleep for 100 seconds before sending the reponse
  get "/" do
    :timer.sleep(100000)
    send_resp(conn, 200, "Hello Plug!")
  end

  match _ do
    send_resp(conn, 404, "Nothing here")
  end
end

Let’s make 5 requests now by running this in the iex prompt:

for n <- 1..5, do: spawn(fn -> :httpc.request('http://localhost:4000') end)

Start :observer from iex using :observer.start and see the process graph:

connection processes

We can see that there are only 2 acceptor processes still, but 5 other processes were spawned somewhere else. These are connection processes which hold accepted connections. What this means is that, acceptor processes do not dictate how many processes we can run at a time, it just restricts how many new processes can be accepted at a time. Even if you want to serve 1000 concurrent requests, it’s safe to leave the number of acceptor processes at the default value of 100.

Summary