Skip to content

Instantly share code, notes, and snippets.

@fishcakez
Last active March 22, 2018 17:28
Show Gist options
  • Save fishcakez/5087a14bc65c7ad2d7b0 to your computer and use it in GitHub Desktop.
Save fishcakez/5087a14bc65c7ad2d7b0 to your computer and use it in GitHub Desktop.
Examples of supervision trees for `:simple_one_for_one` supervisors
defmodule SimpleSup do
@moduledoc """
This file shows methods for starting a configurable number of children under
a `:simple_one_for_one` supervisor.
When the supervision tree is first started all methods behave the same, `size`
children are started and the `:starter` returns `:ignore`. However if the
restart limit for those children is reached the `:simple_one_for_one`
supervisor will be restarted and then the `:starter`. It is possible that the
`:simple_one_for_one` is restarted successfully but the `:starter` fails to
start all children on the first attempt and crashes. The parent supervisor
will then either restart both (`:one_for_all`) or just the `:starter`
(`:rest_for_one`).
The simplest method is to use `:one_for_all` and require all children to be
successfully started. More complex methods can use `:rest_for_one`, which
provides better isolation of errors as the `:starter` can make multi attempts
to restart only those children that were not started successful on a previous
restart attempt. Whereas the `:one_for_all` method will stop all children and
then restart them all if any child fails to start.
The `:rest_for_one` method should be chosen based on the properties of the
children. If it is not easy to use one those methods then use the
`:one_for_all` method.
The asynchronous approach can be made to work with all the methods but uses
the `:one_for_all` method in the example. This approach can be used for
other supervisor strategies too if starting children asynchronous is
desired.
"""
use Application
import Supervisor.Spec, warn: false
def start(_type, _args) do
children = [
supervisor(Supervisor, [
[simple_supervisor(SimpleSup.OneForAll),
starter(SimpleSup.OneForAll)],
[strategy: :one_for_all]],
[id: :one_for_all]),
supervisor(Supervisor, [
[simple_supervisor(SimpleSup.NamedChildren),
starter(SimpleSup.NamedChildren)],
[strategy: :rest_for_one]],
[id: :named]),
supervisor(Supervisor, [
[simple_supervisor(SimpleSup.CountChildren),
starter(SimpleSup.CountChildren)],
[strategy: :rest_for_one]],
[id: :count]),
supervisor(Supervisor, [
[simple_supervisor(SimpleSup.CallChildren),
starter(SimpleSup.CallChildren)],
[strategy: :rest_for_one]],
[id: :call]),
supervisor(Supervisor, [
[simple_supervisor(SimpleSup.AsyncChildren),
starter(SimpleSup.AsyncChildren)],
[strategy: :one_for_all]],
[id: :async]),
]
opts = [strategy: :one_for_one, name: SimpleSup.Supervisor]
Supervisor.start_link(children, opts)
end
defp simple_supervisor(name) do
children = [worker(Agent, [])]
options = [strategy: :simple_one_for_one, name: name]
supervisor(Supervisor, [children, options], [id: :simple])
end
defp starter(mod) do
# Note that the `:starter` is `:transient`
worker(mod, [], [id: :starter, restart: :transient])
end
end
defmodule SimpleSup.NamedChildren do
@moduledoc """
Starter ensures all children are started. Importantly if a child is already
started (i.e. process already exists with that name) the error is treated as
a success.
If the configuration changes (.e.g. `:size` increased to 5) the starter can
be restarted using the parent supervisor to ensure all are started:
{:ok, :undefined} = Supervisor.restart_child(parent_sup, :starter)
For new config to be taken into account the config must be looked up in the
starter's `init/1` and not passed as arguments to its `start_link`.
"""
def start_link(), do: GenServer.start_link(__MODULE__, nil)
def init(_) do
size = Application.get_env(:simple_sup, :size, 4)
_ = for n <- 1..size do
name = String.to_atom("named_agent_#{n}")
case Supervisor.start_child(__MODULE__, [&Map.new/0, [name: name]]) do
{:ok, _} -> :ok
{:ok, _, _} -> :ok
{:error, {:already_started, _}} -> :ok
{:error, reason} -> exit({:failed_to_start_child, name, reason})
end
end
:ignore
end
end
defmodule SimpleSup.OneForAll do
@moduledoc """
Simplest method. If any child fails to start then the parent supervisor
restarts both the simple_one_for_one supervisor and the starter. This means
there will be no children under the simple_one_for_one supervisor when the
starter is started - unless another process starts the children.
Unlike other `:rest_for_one` method can not use `Supervisor.restart_child/2`
to ensure all children are started.
This method might be required if it is not possible to work out which children
are active.
"""
def start_link(), do: GenServer.start_link(__MODULE__, nil)
def init(_) do
size = Application.get_env(:simple_sup, :size, 4)
_ = for _ <- 1..size do
case Supervisor.start_child(__MODULE__, [&Map.new/0]) do
{:ok, _} -> :ok
{:ok, _, _} -> :ok
{:error, reason} -> exit({:failed_to_start_child, reason})
end
end
:ignore
end
end
defmodule SimpleSup.CountChildren do
@moduledoc """
Starter ensures the correct number of children are started.
If the configuration changes (.e.g. `:size` increased to 5) the starter can
be restarted using the parent supervisor to ensure enough are started:
{:ok, :undefined} = Supervisor.restart_child(parent_sup, :starter)
For new config to be taken into account the config must be looked up in the
starter's `init/1` and not passed as arguments to its `start_link`.
"""
def start_link(), do: GenServer.start_link(__MODULE__, nil)
def init(_) do
size = Application.get_env(:simple_sup, :size, 4)
count = Supervisor.count_children(SimpleSup.CountChildren)[:workers]
_ = for _ <- 1..(size-count) do
case Supervisor.start_child(__MODULE__, [&Map.new/0]) do
{:ok, _} -> :ok
{:ok, _, _} -> :ok
{:error, reason} -> exit({:failed_to_start_child, reason})
end
end
:ignore
end
end
defmodule SimpleSup.CallChildren do
@moduledoc """
Starter ensures the children are started by getting information on all the
children and starting the missing children.
If the configuration changes (.e.g. `:size` increased to 5) the starter can
be restarted using the parent supervisor to ensure enough are started:
{:ok, :undefined} = Supervisor.restart_child(parent_sup, :starter)
For new config to be taken into account the config must be looked up in the
starter's `init/1` and not passed as arguments to its `start_link`.
"""
def start_link(), do: GenServer.start_link(__MODULE__, nil)
def init(_) do
size = Application.get_env(:simple_sup, :size, 4)
active = for {_, child, _, _} <- Supervisor.which_children(__MODULE__) do
Agent.get(child, &Map.fetch!(&1, :id))
end
_ = for id <- Enum.to_list(1..size) -- active do
case Supervisor.start_child(__MODULE__, [fn() -> %{id: id} end]) do
{:ok, _} -> :ok
{:ok, _, _} -> :ok
{:error, reason} -> exit({:failed_to_start_child, reason})
end
end
:ignore
end
end
defmodule SimpleSup.AsyncChildren do
@moduledoc """
The asynchronous approach can be used with any of the above methods, below
is an example using `:one_for_all`.
Children are started asynchronously - that is starting the children does not
block the supervision tree.
For the `:rest_for_one` methods the `Supervisor.restart_child/2` call will
still work. However a failure will not be returned from that call. Instead the
`:starter` will crash and be restarted by its parent. This will continue until
either all children are started or the parent's restart limit is reached.
"""
use GenServer
def start_link(), do: GenServer.start_link(__MODULE__, nil)
def init(_) do
size = Application.get_env(:simple_sup, :size, 4)
GenServer.cast(self, {:start, size})
{:ok, nil}
end
def handle_cast({:start, size}, state) do
_ = for _ <- 1..size do
case Supervisor.start_child(__MODULE__, [&Map.new/0]) do
{:ok, _} -> :ok
{:ok, _, _} -> :ok
{:error, reason} -> exit({:failed_to_start_child, reason})
end
end
{:stop, :normal, state}
end
end
@ktec
Copy link

ktec commented Feb 7, 2016

Found this really useful, thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment