Elixir Supervision Trees
A supervision tree is a hierarchy of supervisors in which a supervisor can itself be a child of another supervisor. The top supervisor starts the whole tree, and each level watches only its own children.
An outer supervisor starts an inner supervisor
The outer list holds an inner supervisor and one worker. All three workers answer.
defmodule Worker do
use GenServer
def start_link({name, initial}), do: GenServer.start_link(__MODULE__, initial, name: name)
def init(initial), do: {:ok, initial}
def handle_call(:get, _from, state), do: {:reply, state, state}
def handle_cast(:inc, state), do: {:noreply, state + 1}
end
defmodule Inner do
use Supervisor
def start_link(_arg), do: Supervisor.start_link(__MODULE__, [], name: :inner_sup)
def init(_arg) do
children = [
%{id: :ia, start: {Worker, :start_link, [{:ia, 10}]}},
%{id: :ib, start: {Worker, :start_link, [{:ib, 20}]}}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
outer_children = [
%{id: Inner, start: {Inner, :start_link, [[]]}},
%{id: :top, start: {Worker, :start_link, [{:top, 40}]}}
]
{:ok, outer} = Supervisor.start_link(outer_children, strategy: :one_for_one)
IO.inspect(GenServer.call(:ia, :get))
IO.inspect(GenServer.call(:ib, :get))
IO.inspect(GenServer.call(:top, :get))
Supervisor.stop(outer)
The inner supervisor restarts its own worker
The inner worker and the top worker are raised by one. Then the inner worker fails, and only it restarts.
defmodule Worker do
use GenServer
def start_link({name, initial}), do: GenServer.start_link(__MODULE__, initial, name: name)
def init(initial), do: {:ok, initial}
def handle_call(:get, _from, state), do: {:reply, state, state}
def handle_cast(:inc, state), do: {:noreply, state + 1}
end
defmodule Inner do
use Supervisor
def start_link(_arg), do: Supervisor.start_link(__MODULE__, [], name: :inner_sup)
def init(_arg) do
children = [
%{id: :ia, start: {Worker, :start_link, [{:ia, 10}]}},
%{id: :ib, start: {Worker, :start_link, [{:ib, 20}]}}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
outer_children = [
%{id: Inner, start: {Inner, :start_link, [[]]}},
%{id: :top, start: {Worker, :start_link, [{:top, 40}]}}
]
{:ok, outer} = Supervisor.start_link(outer_children, strategy: :one_for_one)
GenServer.cast(:ia, :inc)
GenServer.cast(:top, :inc)
IO.inspect(GenServer.call(:ia, :get))
IO.inspect(GenServer.call(:top, :get))
inner = GenServer.whereis(:inner_sup)
Process.exit(GenServer.whereis(:ia), :kill)
Process.sleep(100)
Supervisor.which_children(inner)
IO.inspect(GenServer.call(:ia, :get))
IO.inspect(GenServer.call(:top, :get))
Supervisor.stop(outer)
The outer supervisor restarts a failed inner supervisor
Both inner workers and the top worker are raised by one, then the inner supervisor fails. The script finds the fresh workers by process id, since their names are briefly unregistered during the restart, and reads all three again.
defmodule Worker do
use GenServer
def start_link({name, initial}), do: GenServer.start_link(__MODULE__, initial, name: name)
def start_link(initial), do: GenServer.start_link(__MODULE__, initial)
def init(initial), do: {:ok, initial}
def handle_call(:get, _from, state), do: {:reply, state, state}
def handle_cast(:inc, state), do: {:noreply, state + 1}
end
defmodule Inner do
use Supervisor
def start_link(_arg), do: Supervisor.start_link(__MODULE__, [], name: :inner_sup)
def init(_arg) do
children = [
%{id: :ia, start: {Worker, :start_link, [10]}},
%{id: :ib, start: {Worker, :start_link, [20]}}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
outer_children = [
%{id: Inner, start: {Inner, :start_link, [[]]}},
%{id: :top, start: {Worker, :start_link, [{:top, 40}]}}
]
{:ok, outer} = Supervisor.start_link(outer_children, strategy: :one_for_one)
find = fn sup, id ->
Enum.find_value(Supervisor.which_children(sup), fn
{^id, pid, _, _} when is_pid(pid) -> pid
_ -> nil
end)
end
ia = find.(:inner_sup, :ia)
ib = find.(:inner_sup, :ib)
GenServer.cast(ia, :inc)
GenServer.cast(ib, :inc)
GenServer.cast(:top, :inc)
IO.inspect(GenServer.call(ia, :get))
IO.inspect(GenServer.call(ib, :get))
IO.inspect(GenServer.call(:top, :get))
old_inner = GenServer.whereis(:inner_sup)
Process.exit(old_inner, :kill)
wait_sup = fn wait_sup, n ->
current = GenServer.whereis(:inner_sup)
cond do
current != nil and current != old_inner -> current
n <= 0 -> :timeout
true -> Process.sleep(10); wait_sup.(wait_sup, n - 1)
end
end
wait_sup.(wait_sup, 500)
wait_worker = fn wait_worker, id, old, n ->
current =
if GenServer.whereis(:inner_sup) == nil do
nil
else
find.(:inner_sup, id)
end
cond do
current != nil and current != old -> current
n <= 0 -> :timeout
true -> Process.sleep(10); wait_worker.(wait_worker, id, old, n - 1)
end
end
new_ia = wait_worker.(wait_worker, :ia, ia, 500)
new_ib = wait_worker.(wait_worker, :ib, ib, 500)
IO.inspect(GenServer.call(new_ia, :get))
IO.inspect(GenServer.call(new_ib, :get))
IO.inspect(GenServer.call(:top, :get))
Supervisor.stop(outer)Example
defmodule Worker do
use GenServer
def start_link({name, initial}), do: GenServer.start_link(__MODULE__, initial, name: name)
def init(initial), do: {:ok, initial}
def handle_call(:get, _from, state), do: {:reply, state, state}
def handle_cast(:inc, state), do: {:noreply, state + 1}
end
defmodule Inner do
use Supervisor
def start_link(_arg), do: Supervisor.start_link(__MODULE__, [], name: :inner_sup)
def init(_arg) do
children = [
%{id: :ia, start: {Worker, :start_link, [{:ia, 10}]}},
%{id: :ib, start: {Worker, :start_link, [{:ib, 20}]}}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
outer_children = [
%{id: Inner, start: {Inner, :start_link, [[]]}},
%{id: :top, start: {Worker, :start_link, [{:top, 40}]}}
]
{:ok, outer} = Supervisor.start_link(outer_children, strategy: :one_for_one)
GenServer.cast(:ia, :inc)
GenServer.cast(:ib, :inc)
GenServer.cast(:top, :inc)
IO.inspect(GenServer.call(:ia, :get))
IO.inspect(GenServer.call(:ib, :get))
IO.inspect(GenServer.call(:top, :get))
inner = GenServer.whereis(:inner_sup)
Process.exit(GenServer.whereis(:ib), :kill)
Process.sleep(100)
Supervisor.which_children(inner)
IO.inspect(GenServer.call(:ia, :get))
IO.inspect(GenServer.call(:ib, :get))
IO.inspect(GenServer.call(:top, :get))
Supervisor.stop(outer)
Key points
- Supervision tree: supervisors arranged in levels, with workers at the bottom.
- Nested supervisor: a supervisor that runs as a child of another supervisor.
- Inner supervisor: the lower supervisor that restarts its own workers when they fail.
- Outer supervisor: the higher supervisor that restarts a failed inner supervisor.
- Failure boundary: a failure inside one branch restarts only that branch, not the whole tree.
In this example, the outer supervisor starts an inner supervisor and a top worker, and all three workers are raised by one. When the inner worker ib fails, its inner supervisor restarts it. After the failure, ia still answers 11, ib answers 20 again, and the top worker still answers 41. The outer supervisor is stopped at the end.
Note
a tree grows by adding a supervisor as a child, not by putting every worker into one list.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
In a supervision tree, an inner worker fails. What restarts it?