Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Supervisor

guides/05-async/supervisor.livemd

Supervisor

A Supervisor allows you to start a predefined list of GenServers and to restart them if they crash. We can also use a DynamicSupervisor if we want to start GenServers dynamically and on demand, or a PartitionSupervisor, if we wanted to group the supervised GenServers by specific keys. But for this example, we’ll focus on the “plain” Supervisor.

Let’s first define a simple GenServer that we can start and supervise:

defmodule RunElixir.Buffer do
  @moduledoc "Implements a naive Last-in-First-out (LIFO) buffer."

  use GenServer

  require Logger

  def start_link(_args), do: GenServer.start_link(__MODULE__, [])

  def push(pid, message), do: GenServer.cast(pid, {:push, message})
  def pop(pid), do: GenServer.call(pid, :pop)

  # Callbacks

  def init(_args) do
    Logger.info("Buffer started!")
    {:ok, []}
  end

  def handle_cast({:push, message}, state), do: {:noreply, [message | state]}
  def handle_call(:pop, _from, [message | state]), do: {:reply, message, state}
end

Now, let’s start two buffers using a Supervisor:

children = [
  %{id: :buffer1, start: {RunElixir.Buffer, :start_link, [nil]}},
  %{id: :buffer2, start: {RunElixir.Buffer, :start_link, [nil]}},
]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)

Supervisor.count_children(pid) |> IO.inspect(label: 1)

Supervisor.which_children(pid) |> IO.inspect(label: 2)
17:08:53.997 [info] Buffer started! # <- Buffer 1 has started
17:08:53.997 [info] Buffer started! # <- Buffer 2 has started
1: %{active: 2, workers: 2, supervisors: 0, specs: 2}
2: [
  {:buffer2, #PID<0.835.0>, :worker, [RunElixir.Buffer]},
  {:buffer1, #PID<0.834.0>, :worker, [RunElixir.Buffer]}
]

We see that the Supervisor started both buffers successfully. Let’s try to add and pop some messages from the first buffer.

[{_name, buffer_pid, _type, _module} | _] = Supervisor.which_children(pid)

RunElixir.Buffer.push(buffer_pid, "Hello")
RunElixir.Buffer.push(buffer_pid, "World")
RunElixir.Buffer.pop(buffer_pid) |> IO.inspect(label: 1)
RunElixir.Buffer.pop(buffer_pid) |> IO.inspect(label: 2)
RunElixir.Buffer.pop(buffer_pid)
1: "World"
2: "Hello"

15:54:56.530 [error] GenServer #PID<0.808.0> terminating
** (FunctionClauseError) no function clause matching in RunElixir.Buffer.handle_call/3

15:54:56.530 [info] Buffer started!

Oh no! We forgot to handle the case when we try to pop a message from an empty buffer. Thats why the buffer crashed when we tried to pop a third message. Luckily, the Supervisor has got our back and restarted the buffer right away, as you can see in the last log line.

Strategies

We started our children using the :one_for_one strategy, which means that the Supervisor will restart only the crashed GenServer process and won’t touch the remaining ones.

We could also use the :one_for_all strategy, which would instruct the Supervisor to terminate all remaining processes and then to restart all processes, including the terminated one.

Lastly, we could use the :rest_for_one strategy, which means that the Supervisor terminates all process that come after the crashed process in the list of children and then restarts the terminated processes, including the crashed one.

You should choose the strategy that fits your use case best, but you’ll mostly see the :one_for_one strategy “in the wild”.