🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Graceful Shutdown

Graceful shutdown is a stop where a worker finishes its cleanup before it exits. The supervisor gives each child time to clean up and stops children in reverse start order.

Terminate runs cleanup on a direct stop

A worker runs a cleanup line when it stops. Stopping it directly prints the cleanup line before the worker is gone.

defmodule Worker do
  use GenServer
  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: name)
  def init(name), do: {:ok, name}
  def handle_call(:get, _from, name), do: {:reply, name, name}
  def terminate(_reason, name), do: IO.puts("cleaned up #{name}")
end
{:ok, pid} = Worker.start_link(:solo)
IO.puts("running: #{GenServer.call(:solo, :get)}")
GenServer.stop(:solo)
IO.puts("alive: #{Process.alive?(pid)}")

A supervisor stops children in reverse order

Two workers start in order, first then second. The supervisor stops the second worker first and the first worker last.

defmodule Worker do
  use GenServer
  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: name)
  def init(name) do
    Process.flag(:trap_exit, true)
    {:ok, name}
  end
  def terminate(_reason, name), do: IO.puts("stopping #{name}")
end
children = [
  %{id: :first, start: {Worker, :start_link, [:first]}},
  %{id: :second, start: {Worker, :start_link, [:second]}}
]
{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_one)
IO.puts("started")
Supervisor.stop(sup)
IO.puts("stopped")

A child spec carries its own shutdown timeout

The child spec sets a shutdown timeout of 2000 milliseconds. The supervisor still runs the worker cleanup when it stops.

defmodule Worker do
  use GenServer
  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: name)
  def init(name) do
    Process.flag(:trap_exit, true)
    {:ok, name}
  end
  def terminate(_reason, name), do: IO.puts("stopping #{name}")
end
spec = %{id: Worker, start: {Worker, :start_link, [:timed]}, shutdown: 2000}
IO.puts("shutdown: #{spec.shutdown}")
{:ok, sup} = Supervisor.start_link([spec], strategy: :one_for_one)
Supervisor.stop(sup)
IO.puts("stopped")

Example

defmodule Worker do
  use GenServer
  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: name)
  def init(name) do
    Process.flag(:trap_exit, true)
    {:ok, name}
  end
  def terminate(_reason, name), do: IO.puts("stopping #{name}")
end
children = [
  %{id: :first, start: {Worker, :start_link, [:first]}, shutdown: 2000},
  %{id: :second, start: {Worker, :start_link, [:second]}, shutdown: 3000}
]
{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_one)
IO.puts("running")
Supervisor.stop(sup)
IO.puts("stopped")

Key points

  • Terminate: the callback a worker runs when it stops, used for cleanup.
  • Cleanup: work such as closing a connection, done inside terminate.
  • Shutdown timeout: the number of milliseconds a supervisor waits for a child to finish stopping.
  • Reverse order: a supervisor stops its last started child first.
  • Trapped exits: a worker must trap exits for terminate to run when its supervisor stops it.

In this example, two workers trap exits and carry shutdown timeouts of 2000 and 3000 milliseconds in their child specs. The supervisor starts the first worker and then the second worker. When the supervisor stops, the second worker cleans up first and the first worker cleans up last.

Note

a child spec can set its own shutdown timeout, instead of using the default.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A supervisor starts worker A and then worker B. When the supervisor stops, which worker runs its terminate callback first?