🎓 A1Learner
🔍
0 of 46 lessons completed

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?