Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

sasa1977/parent

Custom parenting of processes in Elixir

sasa1977/parent.json
{
"createdAt": "2018-04-08T19:03:34Z",
"defaultBranch": "master",
"description": "Custom parenting of processes in Elixir",
"fullName": "sasa1977/parent",
"homepage": null,
"language": "Elixir",
"name": "parent",
"pushedAt": "2024-10-04T08:22:20Z",
"stargazersCount": 277,
"topics": [],
"updatedAt": "2025-10-02T20:40:57Z",
"url": "https://github.com/sasa1977/parent"
}

hex.pm hexdocs.pm Build Status

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 and 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]),
]
)
  • if Child1 is restarted, Child2, Child3, and Child7 will be restarted too
  • if Child2, Child3, or Child7 is restarted, nothing else is restarted
  • if any of Child4, Child5, or Child6 is 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)

Dynamic supervisor with anonymous children

Section titled “Dynamic supervisor with anonymous children”
Parent.Supervisor.start_link([], name: MySup)
# 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.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
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

Section titled “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

Section titled “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

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.

[MIT]!(./LICENSE)