Parent
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 Supervisor
are:
- No supervision strategies (one_for_one, rest_for_one, etc). Instead, Parent uses bindings and shutdown groups to achieve the similar behaviour.
- No distinction between static and dynamic supervisors. Instead, a per-child option called
:ephemeral?
is used to achieve dynamic behaviour. - Basic registry-like capabilities for simple children discovery baked-in directly into parent.
- Exposed lower level plumbing modules, such as
Parent.GenServer
andParent
, which can be used to build custom parent processes (i.e. supervisors with custom logic).
Examples
Basic supervisor
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__
)
Binding lifecycles
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]),
]
)
- if
Child1
is restarted,Child2
,Child3
, andChild7
will be restarted too - if
Child2
,Child3
, orChild7
is restarted, nothing else is restarted - if any of
Child4
,Child5
, orChild6
is restarted, all other processes from the shutdown group are restarted too
Discovering siblings during startup
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
Pausing and resuming a part of the system
# 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)
Dynamic supervisor with anonymous 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)
Dynamic supervisor with child discovery
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)
Per-child max restart frequency
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
)
Module-based supervisor
defmodule MySup do
use Parent.GenServer
def 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
Restarting with a delay
defmodule MySup do
use Parent.GenServer
def 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
Starting additional children after a child stops
defmodule MySup do
use Parent.GenServer
def 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
Building a custom parent process or behaviour from scratch
defp init_process do
Parent.initialize(parent_opts)
start_some_children()
loop()
end
defp 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
Status
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.