🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Supervisor Shutdown

Supervisor shutdown is the orderly end of a supervisor and its children. Each child gets a chance to clean up before its process ends.

Stopping a worker runs cleanup with a normal reason

A worker ends through a normal stop, and its cleanup message carries the normal reason. The parent prints that reason.

defmodule TWorker do
  use GenServer
  def start_link(parent), do: GenServer.start_link(__MODULE__, parent)
  def init(parent), do: {:ok, parent}
  def handle_call(:get, _from, parent), do: {:reply, :ok, parent}
  def terminate(reason, parent) do
    send(parent, {:ended, reason})
    :ok
  end
end
{:ok, pid} = TWorker.start_link(self())
GenServer.stop(pid)
receive do
  {:ended, reason} -> IO.inspect(reason)
end

A supervisor stops children in reverse order with a shutdown reason

Both workers trap exits, so both run cleanup. The second started ends first, and both reasons are shutdown.

defmodule OrdA do
  use GenServer
  def start_link({id, parent}), do: GenServer.start_link(__MODULE__, {id, parent})
  def init({id, parent}) do
    Process.flag(:trap_exit, true)
    {:ok, {id, parent}}
  end
  def handle_call(:get, _from, s), do: {:reply, :ok, s}
  def terminate(reason, {id, parent}) do
    send(parent, {:ended, id, reason})
    :ok
  end
end
defmodule OrdB do
  use GenServer
  def start_link({id, parent}), do: GenServer.start_link(__MODULE__, {id, parent})
  def init({id, parent}) do
    Process.flag(:trap_exit, true)
    {:ok, {id, parent}}
  end
  def handle_call(:get, _from, s), do: {:reply, :ok, s}
  def terminate(reason, {id, parent}) do
    send(parent, {:ended, id, reason})
    :ok
  end
end
defmodule SupO do
  use Supervisor
  def start_link(parent), do: Supervisor.start_link(__MODULE__, parent)
  def init(parent) do
    Supervisor.init([{OrdA, {"first", parent}}, {OrdB, {"second", parent}}], strategy: :one_for_one)
  end
end
{:ok, sup} = SupO.start_link(self())
Supervisor.stop(sup)
order = for _ <- 1..2 do
  receive do
    {:ended, id, reason} -> {id, reason}
  end
end
IO.inspect(order)

A brutal kill child skips cleanup

A trapping worker is marked for a brutal kill. The supervisor stops, no cleanup message arrives, and the worker is gone.

defmodule Brut do
  use GenServer
  def start_link(parent), do: GenServer.start_link(__MODULE__, parent)
  def init(parent) do
    Process.flag(:trap_exit, true)
    {:ok, parent}
  end
  def handle_call(:get, _from, s), do: {:reply, :ok, s}
  def terminate(reason, parent) do
    send(parent, {:ended, reason})
    :ok
  end
end
defmodule SupBrut do
  use Supervisor
  def start_link(parent), do: Supervisor.start_link(__MODULE__, parent)
  def init(parent) do
    children = [%{id: Brut, start: {Brut, :start_link, [parent]}, shutdown: :brutal_kill}]
    Supervisor.init(children, strategy: :one_for_one)
  end
end
{:ok, sup} = SupBrut.start_link(self())
child = sup |> Supervisor.which_children() |> hd() |> elem(1)
Supervisor.stop(sup)
result = receive do
  {:ended, reason} -> reason
after
  500 -> :no_message
end
IO.inspect(result)
IO.inspect(Process.alive?(child))

Example

defmodule TrapSaver do
  use GenServer
  def start_link(parent), do: GenServer.start_link(__MODULE__, parent)
  def init(parent) do
    Process.flag(:trap_exit, true)
    {:ok, {parent, "draft"}}
  end
  def handle_call(:get, _from, s), do: {:reply, :ok, s}
  def terminate(_reason, {parent, value}) do
    send(parent, {:saved, value})
    :ok
  end
end
defmodule Plain do
  use GenServer
  def start_link(parent), do: GenServer.start_link(__MODULE__, parent)
  def init(parent), do: {:ok, parent}
  def handle_call(:get, _from, s), do: {:reply, :ok, s}
  def terminate(_reason, parent) do
    send(parent, {:saved, "plain"})
    :ok
  end
end
defmodule SupEx do
  use Supervisor
  def start_link(parent), do: Supervisor.start_link(__MODULE__, parent)
  def init(parent) do
    Supervisor.init([{TrapSaver, parent}, {Plain, parent}], strategy: :one_for_one)
  end
end
{:ok, sup} = SupEx.start_link(self())
Supervisor.stop(sup)
saved = receive do
  {:saved, value} -> value
end
IO.inspect(saved)
other = receive do
  {:saved, value} -> value
after
  500 -> :no_message
end
IO.inspect(other)

Key points

  • Cleanup: a terminate callback runs cleanup work when a worker ends.
  • Normal stop: stopping a worker on its own runs cleanup with a normal reason.
  • Shutdown reason: stopping a supervisor runs cleanup with a shutdown reason, for workers that trap exits.
  • Trap: trapping exits turns an exit signal into a cleanup chance.
  • Reverse order: a supervisor stops its children in reverse start order.
  • Kill: killing a worker never runs its cleanup.

In this example, a supervisor starts a trapping worker and a plain worker, a stop ends the plain worker first without cleanup, the trapping worker saves its draft, the parent sees the draft, and a second wait finds no message from the plain worker.

Note

a brutal kill child skips cleanup entirely, even when it traps exits.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A supervisor stops. Which worker runs its cleanup?