Skip to content

Instantly share code, notes, and snippets.

@mikalv
Forked from gkaemmer/Elixir_Supervision_Trees.md
Created December 12, 2019 00:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikalv/b606f563072e586cc5de8044ffe78fbd to your computer and use it in GitHub Desktop.
Save mikalv/b606f563072e586cc5de8044ffe78fbd to your computer and use it in GitHub Desktop.
Quick guide to creating Elixir supervision trees from scratch

Elixir Supervision Trees Made Easy

I started with Elixir just a couple weeks after the switch from 1.4 to 1.5, so the bulk of online resources were out of date (or at least resulted in deprecation warnings). This guide is for defining Elixir 1.5 supervised modules.

It's not actually terribly complicated. It's just sometimes unclear from examples what's implemented by the language and what you actually have to implement yourself.

Say we want a supervision tree like this (where each atom is a process):

    :a
    / \
  :b  :c
       |
      :d 

Basically, process :a boots up two children, :b and :c. Unbeknowst to :a, process :c also spawns a child :d. In the end, we have two workers running in a supervised way: :b, and :d.

The only purpose of the :c abstraction between :a and :d is to demonstrate that supervisors can be nested. There's no reason not to just have :a supervise :d directly.

We'll define this setup with four modules, one for each atom in the tree.

defmodule A do
end
defmodule B do
end
defmodule C do
end
defmodule D do
end

Let's start with supervisor A. Initially, we're not going to use any built-in modules, because I find it adds scary indirection that makes learning a bit tricky.

A is going to be a plain app, which we can boot up using A.start.

defmodule A do
  def start do
    children = [
      {B, []},
      # Let's comment out C for now to focus on getting one child working
      # {C, []}
    ]
    Supervisor.start_link(children, name: :a, strategy: :one_for_one)
  end
end

If you go ahead and try to fire that up with A.start, you'll get an error The module B was given as a child to a supervisor but it does not implement child_spec. Telling! Let's do that now.

defmodule B do
  # This is required so that `A` knows how to start and restart this module
  def child_spec(_) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      type: :worker
    }
  end

  def start_link() do
    Agent.start_link(fn -> [] end, name: :b)
  end
end

Here, the process named :b is just an Agent that stores an empty array. Notice that we don't need to use Agent or any of that fancy stuff. This is a bare module that is supervise-able.

All we had to do was define a function child_spec/1 that returns a map. start is the important key there: it says to the supervisor, "To start me up, call B.start_link() with no arguments." It also defines a unique ID for this child, and the type, which is either :worker or :supervisor.

Also note: the name of the start_link method doesn't matter! It just needs to match the atom defined in child_spec: start: {__MODULE__, :start_link, [arg]}. It's just convention to call this method start_link in your custom modules.

If we compile modules A and B now and run A.start, we can see that we can use :b as a piece of state:

A.start

Agent.get(:b, fn list -> list end)
# => []

Agent.update(:b, fn list -> ["hello" | list] end)
Agent.get(:b, fn list -> list end)
# => ["hello"]

# We can also kill :b--its supervisor :a will restart it
#   and reset its state
Process.exit(Process.whereis(:b), :shutdown)
Agent.get(:b, fn list -> list end)
# => []

Cool. So we have a supervisor named :a and an agent process named :b. Now what about :c? Its module C is also going to need a child_spec definition, but this time it will have type :supervisor. Let's also go ahead and define D, which will look just like B:

defmodule C do
  def child_spec(_) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      type: :supervisor
    }
  end

  def start_link() do
    children = [
      {D, []}
    ]
    Supervisor.start_link(children, name: :c, strategy: :one_for_one)
  end
end

defmodule D do
  def child_spec(_) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      type: :worker
    }
  end

  def start_link() do
    Agent.start_link(fn -> [] end, name: :d)
  end
end

Once again, all we really had to define was child_spec/1. C.start_link looks just like A.start, because C is really just the same--its a supervisor for other processes.

You can keep repeating this all you want to build yourself a process tree.

A lot of examples in the documentation encourage you to use Supervisor or use GenServer or use Agent. All these are really doing is defining a child_spec for you. I think it's important to point out that you can define these specs for yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment