Elixir child specifications became a lot easier

and keep concerns separate along the way

The official announcement of Elixir 1.5 was dominantly about improvements for debugging. Here I want to talk about a seemingly small change to how supervisor children can be specified. This change demonstrates that the Elixir builders take architectural concerns as well as developer happiness very seriously.

If you are not familiar with the Erlang architecture, think about an Erlang application at runtime as a tree of processes communicating via message passing. The leaves of the tree are the “workers” responsible for handling data, connections and more. Above them, supervisors take care of maintaining the tree structure by restarting child processes if necessary.

This starts a hypothetical top level supervisor in the MyApp Phoenix application:

elixirdefmodule MyApp.Application do
  use Application

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

    children = [
      supervisor(MyApp.Repo, []),
      supervisor(MyAppWeb.Endpoint, []),
      supervisor(MyApp.Metering.Sup, [])
    ]

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

As we’ll see further down, the metering subsystem requires two processes (“workers”) to be present, and one of them needs startup parameters. We could have added them to the top level supervisor, but that would make determining the application’s components and their interconnectedness very hard further down the road. We would also need to pass detailed connection parameters for one of the workers right at the top, mixing high-level and low-level concerns.

Thus, we have to keep that information out of the top level supervisor by adding a supervisor to the metering subsystem (which has more implications btw). This means we need to refer to the subsystem’s supervisor instead, which is, for namespacing reasons, awkwardly named Sup.

Wouldn’t it be better to be able to refer to the subsystem in an opaque way instead?

Before demonstrating a better way, we have a look at the current metering code. The metering subsystem consists of a supervisor and two workers. One holds a connection to Redis and is implemented through redix. I use it here as an example of a worker that wants to be started with data.

elixirdefmodule MyApp.Metering do
  defmodule Sup do
    use Supervisor

    def start_link do
      Supervisor.start_link(__MODULE__, [], name: __MODULE__)
    end

    def init(_) do
      children = [
        worker(MyApp.Metering.Monitor, []),
        worker(Redix, [System.get_env("REDIS_URL"), [name: MyApp.Metering.Redix]])
      ]

      Supervisor.init(children, strategy: :one_for_one)
    end
  end

  defmodule Monitor do
    # Monitors things and stores values in Redis through MyApp.Metering.Redix

    def start_link do
      ...
    end
  end
end

Elixir’s Supervisor module is basically a wrapper around Erlang’s :supervisor, so it has to deal with the rather ugly child spec tuples required by it. They are produced by the supervisorand worker functions from Supervisor.Spec.

In version 1.5, Elixir’s Supervisor implements a kind of delegation mechanism to be able to keep child specification details close to the module implementing the process. Using it, the new top level file reads like this:

elixirdefmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyAppWeb.Endpoint,
      MyApp.Metering
    ]

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

Much cleaner and calmer. This is possible because MyApp.Metering implements the new child_spec/1 callback that returns the child spec for the module, and because Ecto and Phoenix help by implementing the same in MyApp.Repo. and MyAppWeb.Endpoint for us. The submodule now looks like this:

elixirdefmodule MyApp.Metering do
  def child_spec(_) do
    Supervisor.Spec.supervisor(__MODULE__.Sup, [])
  end

  defmodule Sup do
    use Supervisor

    def start_link do
      Supervisor.start_link(__MODULE__, [], name: __MODULE__)
    end

    def init(_) do
      children = [
        worker(MyApp.Metering.Monitor, []),
        worker(Redix, [System.get_env("REDIS_URL"), [name: MyApp.Metering.Redix]])
      ]

      Supervisor.init(children, strategy: :one_for_one)
    end
  end

  defmodule Monitor do
    ... (as before) ...
  end
end

What we have done at the top level should also work here. Luckily, Redix already implements child_spec/1 for us (currently on the master branch), so all we need to do is to implement it on Monitor as well.

Interestingly, we can get rid of the awkward Sup module as well now. Previously it was required to build a module-based supervisor, because we did not want to include its children specifications in the top level supervisor in the first place. Now the subsystem child spec is delegated into the Metering module, we can build a short-hand supervisor instead, further reducing code size and complexity:

elixirdefmodule MyApp.Metering do
  def child_spec(_) do
    children = [
      MyApp.Metering.Monitor,
      {Redix, [[], [name: MyApp.Metering.Redix]]}
    ]

    Supervisor.Spec.supervisor(Supervisor, [children, [strategy: :one_for_one, name: __MODULE__]])
  end

  defmodule Monitor do
    def child_spec(_) do
      Supervisor.Spec.worker(__MODULE__, [])
    end

    def start_link do
      ...
    end
  end
end

Because the Redix worker requires arguments, we need to use the tuple {Redix, [...]} where the second entry is passed to Redix.child_spec/1.

Note I also named the metering supervisor the same as the metering module, because it represents the subsystem. This will show up in tools like the graphical :observer.

Once again we have made a change to simpler and calmer code. But what is even more interesting:

By delegating the child specification to the module implementing a subsystem, we are free to switch between a simple worker and a process tree rooted in a supervisor. The system above no longer needs to care!

This is separation of concerns. The level above won’t need to change when the architecture of a subsystem changes dramatically. This is a nice example where Elixir is not bound to handle everything like Erlang would do it, but can instead add meaningful abstractions on top.


If you need a robust product serving a large number of clients, we would like to hear from you. We are eager to share our experiences with you.