🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Dynamic Supervisors

A dynamic supervisor is a supervisor that starts children on demand, instead of from a fixed list at start. A caller asks it to start a child whenever a new worker is needed.

A plain map starts a child on demand

The caller passes a map, and the supervisor starts the child from it. The child answers with its starting value.

defmodule DynWorker 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
{:ok, sup} = DynamicSupervisor.start_link(name: DynSup)
{:ok, child} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["a"]}})
IO.inspect(GenServer.call(child, :get))
IO.inspect(DynamicSupervisor.count_children(sup).active)
DynamicSupervisor.stop(sup)
IO.inspect(Process.alive?(sup))

Removing a child drops it for good

Two children start, one is removed, and the other keeps answering. The count falls by one and stays there.

defmodule DynWorker 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
{:ok, sup} = DynamicSupervisor.start_link(name: DynSup)
{:ok, c1} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["a"]}})
{:ok, c2} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["b"]}})
IO.inspect(DynamicSupervisor.count_children(sup).active)
DynamicSupervisor.terminate_child(sup, c1)
IO.inspect(DynamicSupervisor.count_children(sup).active)
IO.inspect(GenServer.call(c2, :get))
DynamicSupervisor.stop(sup)

A crashed child starts again with its original argument

Killing a child makes the supervisor start it again. The new child answers with the same starting value.

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 DynWorker 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
{:ok, sup} = DynamicSupervisor.start_link(name: DynSup)
{:ok, child} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["dyn"]}})
Process.exit(child, :kill)
Waiter.wait(fn ->
  DynamicSupervisor.which_children(sup)
  |> Enum.map(fn {_id, pid, _t, _m} -> pid end)
  |> Enum.filter(fn pid -> pid != child end)
  |> length() == 1
end, 200)
restarted = DynamicSupervisor.which_children(sup) |> hd() |> elem(1)
IO.inspect(restarted != child)
IO.inspect(GenServer.call(restarted, :get))
DynamicSupervisor.stop(sup)

Example

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 DynWorker 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
{:ok, sup} = DynamicSupervisor.start_link(name: DynSup)
{:ok, a} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["a"]}})
{:ok, b} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["b"]}})
IO.inspect(DynamicSupervisor.count_children(sup).active)
DynamicSupervisor.terminate_child(sup, a)
IO.inspect(DynamicSupervisor.count_children(sup).active)
Process.exit(b, :kill)
Waiter.wait(fn ->
  DynamicSupervisor.which_children(sup)
  |> Enum.map(fn {_id, pid, _t, _m} -> pid end)
  |> Enum.filter(fn pid -> pid != b end)
  |> length() == 1
end, 200)
restarted = DynamicSupervisor.which_children(sup) |> hd() |> elem(1)
IO.inspect(GenServer.call(restarted, :get))
{:ok, _c} = DynamicSupervisor.start_child(sup, %{id: DynWorker, start: {DynWorker, :start_link, ["c"]}})
IO.inspect(DynamicSupervisor.count_children(sup).active)
DynamicSupervisor.stop(sup)

Key points

  • On demand: a child starts only when a caller asks for it.
  • Child map: a plain map with an identifier and a start entry describes a child.
  • Start entry: the start entry names a module, a function, and its arguments.
  • Removal: removing a child also drops its entry, so it does not restart.
  • Restart: a crashed permanent child starts again with its original argument.

In this example, two children start, one is removed so the count falls to one, the other is killed and restarts with its original value, a third child starts, and the final count is two.

Note

a dynamic supervisor keeps no fixed list, so new kinds of work can arrive at any time.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What does a dynamic supervisor need to start a worker?