Custom parenting of processes in Elixir
Support for custom parenting of processes. See docs for reference.
Parent is a toolkit for building processes which parent other children and manage their life cycles. The library provides features similar to
Supervisor, such as the support for automatic restarts and failure escalation (maximum restart intensity), with some additional benefits that can help flattening the supervision tree, reduce the amount of custom process monitors, and simplify the process structure. The most important differences from
Supervisorare:
:ephemeral?is used to achieve dynamic behaviour.
Parent.GenServerand
Parent, which can be used to build custom parent processes (i.e. supervisors with custom logic).
Parent.Supervisor.start_link( # child spec is a superset of supervisor child specification child_specs,parent options, note that there's no
:strategy
max_restarts: 3, max_seconds: 5,
std. Supervisor/GenServer options
name: MODULE )
Parent.Supervisor.start_link( [ Parent.child_spec(Child1), Parent.child_spec(Child2, binds_to: [Child1]), Parent.child_spec(Child3, binds_to: [Child1]), Parent.child_spec(Child4, shutdown_group: :children4_to_6), Parent.child_spec(Child5, shutdown_group: :children4_to_6), Parent.child_spec(Child6, shutdown_group: :children4_to_6), Parent.child_spec(Child7, binds_to: [Child1]), ] )
Child1is restarted,
Child2,
Child3, and
Child7will be restarted too
Child2,
Child3, or
Child7is restarted, nothing else is restarted
Child4,
Child5, or
Child6is restarted, all other processes from the shutdown group are restarted too
Parent.Supervisor.start_link( [ Parent.child_spec(Child1), Parent.child_spec(Child2, binds_to: [Child1]), # ... ] )defmodule Child2 do def start_link do # can be safely invoked inside the parent process child1 = Parent.child_pid(:child1)
# ...
end end
# stops child1 and all children depending on it, removing it from the parent stopped_children = Parent.Client.shutdown_child(some_parent, :child1)...
returns all stopped children back to the parent
Parent.Client.return_children(some_parent, stopped_children)
Parent.Supervisor.start_link([])set
ephemeral?: true
for dynamic children if child is temporary/transient{:ok, pid1} = Parent.Client.start_child(MySup, Parent.child_spec(Child, id: nil, ephemeral?: true)) {:ok, pid2} = Parent.Client.start_child(MySup, Parent.child_spec(Child, id: nil, ephemeral?: true))
...
Parent.Client.shutdown_child(MySup, pid1) Parent.Client.restart_child(MySup, pid2)
Parent.Supervisor.start_link([], name: MySup)meta is an optional value associated with the child
Parent.Client.start_child(MySup, Parent.child_spec(Child, id: id1, ephemeral?: true, meta: some_meta)) Parent.Client.start_child(MySup, Parent.child_spec(Child, id: id2, ephemeral?: true, meta: another_meta))
...
synchronous calls into the parent process
pid = Parent.Client.child_pid(MySup, id1) meta = Parent.Client.child_meta(MySub, id1) all_children = Parent.Client.children(MySup)
Optional ETS-powered registry:
Parent.Supervisor.start_link([], registry?: true)start some children
ETS lookup, no call into parent involved
Parent.Client.child_pid(my_sup, id1) Parent.Client.children(my_sup)
Parent.Supervisor.start_link( [ Parent.child_spec(Child1, max_restarts: 10, max_seconds: 10), Parent.child_spec(Child2, max_restarts: 3, max_seconds: 5) ],Per-parent max restart frequency can be disabled, or a parent-wide limit can be used. In the
former case make sure that this limit is higher than the limit of any child.
max_restarts: :infinity )
defmodule MySup do use Parent.GenServerdef start_link(init_arg), do: Parent.GenServer.start_link(MODULE, init_arg, name: MODULE)
@impl GenServer def init(_init_arg) do Parent.start_all_children!(children) {:ok, initial_state} end end
defmodule MySup do use Parent.GenServerdef start_link(init_arg), do: Parent.GenServer.start_link(MODULE, init_arg, name: MODULE)
@impl GenServer def init(_init_arg) do # Make sure that children are temporary and ephemeral b/c otherwise
handle_stopped_children/2
# won't be invoked. Parent.start_all_children!(children) {:ok, initial_state} end@impl Parent.GenServer def handle_stopped_children(stopped_children, state) do # invoked when a child stops and is not restarted Process.send_after(self, {:restart, stopped_children}, delay) {:noreply, state} end
def handle_info({:restart, stopped_children}, state) do # Returns the child to the parent preserving its place according to startup order and bumping # its restart count. This is basically a manual restart. Parent.return_children(stopped_children) {:noreply, state} end end
defmodule MySup do use Parent.GenServerdef start_link(init_arg), do: Parent.GenServer.start_link(MODULE, init_arg, name: MODULE)
@impl GenServer def init(_init_arg) do Parent.start_child(first_child_spec) {:ok, initial_state} end
@impl Parent.GenServer def handle_stopped_children(%{child1: info}, state) do Parent.start_child(other_children) {:noreply, state} end
def handle_stopped_children(_other, state), do: {:noreply, state} end
defp init_process do Parent.initialize(parent_opts) start_some_children() loop() enddefp loop() do receive do msg -> case Parent.handle_message(msg) do # parent handled the message :ignore -> loop()
# parent handled the message and returned some useful information {:stopped_children, stopped_children} -> handle_stopped_children(stopped_children) # not a parent message nil -> custom_handle_message(msg) end
end end
This library has seen production usage in a couple of different projects. However, features such as automatic restarts and ETS registry are pretty fresh (aded in late 2020) and so they haven't seen any serious production testing yet.
Based on a very quick & shallow test, Parent is about 3x slower and consumes about 2x more memory than DynamicSupervisor.
The API is prone to significant changes.
Compared to supervisor crash reports, the error logging is very basic and probably not sufficient.