Elixir Child Specs
A child spec is a map that tells a supervisor how to start and restart one child. The map carries an identifier, a start entry, and restart rules.
The default map names the module and its start call
The default map uses the module as its identifier. An override can change the identifier or the restart rule.
defmodule W do
use GenServer
def start_link(v), do: GenServer.start_link(__MODULE__, v)
def init(v), do: {:ok, v}
def handle_call(:get, _from, v), do: {:reply, v, v}
end
IO.inspect(W.child_spec("hello"))
IO.inspect(Supervisor.child_spec({W, "hi"}, id: :first))
IO.inspect(Supervisor.child_spec({W, "hi"}, id: :first, restart: :temporary))A temporary child stays gone after a normal stop
A temporary child and a plain worker start together. Stopping the temporary child leaves only the plain worker.
defmodule Temp do
use GenServer
def child_spec(v) do
%{id: Temp, start: {__MODULE__, :start_link, [v]}, restart: :temporary}
end
def start_link(v), do: GenServer.start_link(__MODULE__, v)
def init(v), do: {:ok, v}
def handle_call(:get, _from, v), do: {:reply, v, v}
end
defmodule W do
use GenServer
def start_link(v), do: GenServer.start_link(__MODULE__, v)
def init(v), do: {:ok, v}
def handle_call(:get, _from, v), do: {:reply, v, v}
end
defmodule Waiter do
def wait(_fun, 0), do: :timeout
def wait(fun, n) do
if fun.() do
:ready
else
Process.sleep(5)
wait(fun, n - 1)
end
end
end
defmodule SupT do
use Supervisor
def start_link(_), do: Supervisor.start_link(__MODULE__, :ok)
def init(:ok) do
Supervisor.init([{Temp, "t"}, {W, "keep"}], strategy: :one_for_one)
end
end
{:ok, sup} = SupT.start_link(:ok)
ids = fn ->
Supervisor.which_children(sup)
|> Enum.map(fn {id, _p, _t, _m} -> id end)
|> Enum.sort()
end
IO.inspect(ids.())
kids = Supervisor.which_children(sup) |> Enum.map(fn {id, pid, _t, _m} -> {id, pid} end) |> Enum.into(%{})
GenServer.stop(kids[Temp])
Waiter.wait(fn -> ids.() == [W] end, 200)
IO.inspect(ids.())
IO.inspect(GenServer.call(kids[W], :get))
Supervisor.stop(sup)A transient child restarts after a crash but not after a normal stop
A transient child crashes and starts again, then stops normally and stays gone. The gone check answers true when its process slot is empty.
defmodule Trans do
use GenServer
def child_spec(v) do
%{id: Trans, start: {__MODULE__, :start_link, [v]}, restart: :transient}
end
def start_link(v), do: GenServer.start_link(__MODULE__, v)
def init(v), do: {:ok, v}
def handle_call(:get, _from, v), do: {:reply, v, v}
end
defmodule Waiter do
def wait(_fun, 0), do: :timeout
def wait(fun, n) do
if fun.() do
:ready
else
Process.sleep(5)
wait(fun, n - 1)
end
end
end
defmodule SupTr do
use Supervisor
def start_link(_), do: Supervisor.start_link(__MODULE__, :ok)
def init(:ok) do
Supervisor.init([{Trans, "trans"}], strategy: :one_for_one)
end
end
{:ok, sup} = SupTr.start_link(:ok)
pid1 = sup |> Supervisor.which_children() |> hd() |> elem(1)
Process.exit(pid1, :kill)
Waiter.wait(fn ->
kids = Supervisor.which_children(sup) |> hd() |> elem(1)
is_pid(kids) and kids != pid1
end, 200)
pid2 = sup |> Supervisor.which_children() |> hd() |> elem(1)
IO.inspect(pid2 != pid1)
IO.inspect(GenServer.call(pid2, :get))
GenServer.stop(pid2)
Waiter.wait(fn ->
sup |> Supervisor.which_children() |> hd() |> elem(1) == :undefined
end, 200)
IO.inspect(sup |> Supervisor.which_children() |> hd() |> elem(1) == :undefined)
Supervisor.stop(sup)Example
defmodule W do
use GenServer
def start_link(v), do: GenServer.start_link(__MODULE__, v)
def init(v), do: {:ok, v}
def handle_call(:get, _from, v), do: {:reply, v, v}
end
defmodule SupIds do
use Supervisor
def start_link(_), do: Supervisor.start_link(__MODULE__, :ok)
def init(:ok) do
children = [Supervisor.child_spec({W, "one"}, id: :one), Supervisor.child_spec({W, "two"}, id: :two)]
Supervisor.init(children, strategy: :one_for_one)
end
end
{:ok, sup} = SupIds.start_link(:ok)
IO.inspect(Supervisor.which_children(sup) |> Enum.map(fn {id, _p, _t, _m} -> id end) |> Enum.sort())
kids = Supervisor.which_children(sup) |> Enum.map(fn {id, pid, _t, _m} -> {id, pid} end) |> Enum.into(%{})
IO.inspect(GenServer.call(kids[:one], :get))
IO.inspect(GenServer.call(kids[:two], :get))
Supervisor.stop(sup)Key points
- Start entry: the start entry names a module, a function, and its arguments.
- Identifier: the identifier sets one child apart from another child of the same module.
- Default restart: a child without a restart entry restarts after any exit.
- Restart temporary: a temporary child never restarts, even after a crash.
- Restart transient: a transient child restarts only after an abnormal exit.
In this example, two specs share one module but carry different identifiers, the supervisor starts both, the sorted identifiers show both children, and each call answers with its own starting value.
Note
a custom child spec function in a module replaces the default map for that module.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What does a temporary child do after it stops?