Last active
March 22, 2018 17:28
-
-
Save fishcakez/5087a14bc65c7ad2d7b0 to your computer and use it in GitHub Desktop.
Examples of supervision trees for `:simple_one_for_one` supervisors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Found this really useful, thanks 👍