Elixir by Example

Supervisors

Edit any cell and run it in your browser Download source

A supervisor starts child processes, watches them, and restarts them when they crash. This is "let it crash" in practice: instead of defensive code for every failure, you return to a known good state.

Crash reports would print pids that change every run, so we silence the logger for this example.

Logger.configure(level: :none)

defmodule Worker do
  use GenServer

  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: name)

  @impl true
  def init(name) do
    IO.puts("starting #{inspect(name)}")
    {:ok, name}
  end

  @impl true
  def handle_call(:crash, _from, _state), do: raise("simulated bug")
end

A child spec says how to start a child and how to restart it. use GenServer generates one, so a {Module, arg} tuple is enough.

children = [
  Supervisor.child_spec({Worker, :a}, id: :a),
  Supervisor.child_spec({Worker, :b}, id: :b),
  Supervisor.child_spec({Worker, :c}, id: :c)
]

:one_for_one: only the crashed child restarts.

{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_one)
starting :a
starting :b
starting :c

Crash :b. The call fails in the caller too (it exits), so we catch that exit with a small helper.

crash = fn name ->
  try do
    GenServer.call(name, :crash)
  catch
    :exit, _ -> :crashed
  end
end

pid_before = Process.whereis(:b)
crash.(:b)

Wait until the supervisor has restarted :b. The restart is synchronous inside the supervisor, so a call to the supervisor returns after it's done.

_ = Supervisor.which_children(sup)
IO.inspect(Process.whereis(:b) != pid_before, label: "b has a new pid")
IO.inspect(Supervisor.count_children(sup), label: "children")
Supervisor.stop(sup)
starting :b
b has a new pid: true
children: %{active: 3, workers: 3, supervisors: 0, specs: 3}

:one_for_all: one crash restarts every child. Use it when children depend on each other's state. (:rest_for_one restarts the crashed child and the ones started after it.)

IO.puts("-- one_for_all --")
{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_all)
crash.(:a)
_ = Supervisor.which_children(sup)
Supervisor.stop(sup)
-- one_for_all --
starting :a
starting :b
starting :c
starting :a
starting :b
starting :c

Restarts are rate limited: more than max_restarts within max_seconds (defaults: 3 in 5s) and the supervisor itself gives up and exits (with reason :shutdown), escalating the failure to its supervisor.

IO.puts("-- max_restarts: 1 --")
Process.flag(:trap_exit, true)

{:ok, sup} =
  Supervisor.start_link([Supervisor.child_spec({Worker, :a}, id: :a)],
    strategy: :one_for_one,
    max_restarts: 1
  )

crash.(:a)
_ = Supervisor.which_children(sup)
crash.(:a)

receive do
  {:EXIT, ^sup, reason} -> IO.inspect(reason, label: "supervisor exited")
end
-- max_restarts: 1 --
starting :a
starting :a
supervisor exited: :shutdown

Pitfall: a supervisor restarts a child with its initial arguments. State held only in the process is gone after a crash. Keep state you can't lose in ETS, a database, or another process.

Pitfall: don't do slow or failure-prone work in init/1 (connecting to a database, HTTP calls). The supervisor blocks while children start, and a failing init crash-loops into max_restarts. Use {:ok, state, {:continue, :connect}} instead.