Skip to content

Instantly share code, notes, and snippets.

@gkaemmer
Last active December 18, 2023 14:37
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save gkaemmer/12a536f7c859c576200e974235e2f923 to your computer and use it in GitHub Desktop.
Save gkaemmer/12a536f7c859c576200e974235e2f923 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

Note: To let module A "supervise" module C, we'll need to uncomment the line {C, []} in our original definition of A.

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.

@gsfordham
Copy link

Useful. Having only recently started Elixir, I've come across these a few times in the last couple weeks, used as main parts of programs, so I've been trying to understand them more.

@bulenterdemir
Copy link

bulenterdemir commented Jul 5, 2020

Thank you for this informative post. I get the point, however, the code does not seem to start processes C and D. There needs to be a way to tie C to A. This is not documented in the post. Maybe you want want to add a sentence to uncomment the child spec for C in module A code as a reminder.

@gkaemmer
Copy link
Author

gkaemmer commented Jul 6, 2020

@bulenterdemir Great point! I added a note about that.

@jsmestad
Copy link

Very useful indeed. It may be good to contribute this to say ElixirSchool or even the official docs if you have not already :)

@ewildgoose
Copy link

This is a very informative post, but I wonder if it would be enhanced if you soften the statement about "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."?

In fact the GenServer and other behaviours will define and enable various advanced functionality such as introspection, code reloading and of course they have some useful functionality such as handle_{call,cast,info} etc.

I don't think this should get in the way of the basic point your are making, probably enough to leave it here as a comment. The key point you are establishing is the basic functionality needed to interact with a supervisor is not so large.

@kosciolek
Copy link

I suggest an edit:

Process.exit(Process.whereis(:b), :shutdown)
Process.sleep(100) # ADD THIS -----------
Agent.get(:b, fn list -> list end)

I had to add Process.sleep(100) for the example to work. Otherwise Agent.get gets called before the restart happens, and an error is thrown.

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