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 inchild_spec
:start: {__MODULE__, :start_link, [arg]}
. It's just convention to call this methodstart_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" moduleC
, we'll need to uncomment the line{C, []}
in our original definition ofA
.
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.
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.