Parent.GenServer behaviour (parent v0.12.1) View Source
GenServer with parenting capabilities powered by Parent
.
This behaviour can be useful in situations where Parent.Supervisor
won't suffice.
Example
The following example is roughly similar to a standard callback-based Supervisor:
defmodule MyApp.Supervisor do
# Automatically defines child_spec/1
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
The expression use Parent.GenServer
will also inject use GenServer
into your code. Your
parent process is a GenServer, and this behaviour doesn't try to hide it. Except when starting
the process, you work with the parent exactly as you work with any GenServer, using the same
functions, such as GenServer.call/3
, and providing the same callbacks, such as init/1
, or
handle_call/3
.
Interacting with the parent from the outside
You can issue regular GenServer
calls and casts, and send messages to the parent, which can
be handled by corresponding GenServer
callbacks. In addition, you can use functions from
the Parent.Client
module to manipulate or query the parent state from other processes. As a
good practice, it's advised to wrap such invocations in the module which implements
Parent.GenServer
.
Interacting with children inside the parent
From within the parent process, you can interact with the child processes using functions from
the Parent
module. All child processes should be started using Parent
functions, such as
Parent.start_child/2
, because otherwise Parent
won't be aware of these processes and won't
be able to fulfill its guarantees.
Note that you can start children from any callback, not just during init/1
. In addition, you
don't need to start all children at once. Therefore, Parent.GenServer
can prove useful when
you need to make some runtime decisions:
{:ok, child1} = Parent.start_child(child1_spec)
if some_condition_met?,
do: Parent.start_child(child2_spec)
Parent.start_child(child3_spec)
However, bear in mind that this code won't be executed again if the processes are restarted.
Handling child termination
If a child process terminates and isn't restarted, the handle_stopped_children/2
callback is
invoked. The default implementation does nothing.
The following example uses handle_stopped_children/2
to start a child task and report if it
it crashes:
defmodule MyJob do
use Parent.GenServer, restart: :temporary
def start_link(arg), do: Parent.GenServer.start_link(__MODULE__, arg)
@impl GenServer
def init(_) do
{:ok, _} = Parent.start_child(%{
id: :job,
start: {Task, :start_link, [fn -> job(arg) end]},
restart: :temporary,
# handle_stopped_children won't be invoked without this
ephemeral?: true
})
{:ok, nil}
end
@impl Parent.GenServer
def handle_stopped_children(%{job: info}, state) do
if info.reason != :normal do
# report job failure
end
{:stop, reason, state}
end
end
handle_stopped_children
can be useful to implement arbitrary custom behaviour, such as
restarting after a delay, and using incremental backoff periods between two consecutive starts.
For example, this is how you could introduce a delay between two consecutive starts:
def handle_stopped_children(stopped_children, state) do
Process.send_after(self, {:restart, stopped_children}, delay)
{:noreply, state}
end
def handle_info({:restart, stopped_children}, state) do
Parent.return_children(stopped_children)
{:noreply, state}
end
Keep in mind that handle_stopped_children
is only invoked if the child crashed on its own,
and if it's not going to be restarted.
If the child was explicitly stopped via a Parent
function, such as Parent.shutdown_child/1
,
this callback will not be invoked. The same holds for Parent.Client
functions. If you want
to unconditionally react to a termination of a child process, setup a monitor with Process.monitor
and add a corresponding handle_info
clause.
If the child was taken down because its lifecycle is bound to some other process, the
corresponding handle_stopped_children
won't be invoked. For example, if process A is bound to
process B, and process B crashes, only one handle_stopped_children
will be invoked (for the
crash of process B). However, the corresponding info
will contain the list of all associated
siblings that have been taken down, and stopped_children
will include information necessary to
restart all of these siblings. Refer to Parent
documentation for details on lifecycles binding.
Parent termination
The behaviour takes down the child processes before it terminates, to ensure that no child process is running after the parent has terminated. The children are terminated synchronously, one by one, in the reverse start order.
The termination of the children is done after the terminate/1
callback returns. Therefore in
terminate/1
the child processes are still running, and you can interact with them, and even
start additional children.
Caveats
Like any other Parent
-based process, Parent.GenServer
traps exits and uses the :infinity
shutdown strategy. As a result, a parent process which blocks for a long time (e.g. because its
communicating with a remote service) won't be able to handle child termination, and your
fault-tolerance might be badly affected. In addition, a blocking parent might completely paralyze
the system (or a subtree) shutdown. Setting a shutdown strategy to a finite time is a hacky
workaround that will lead to lingering orphan processes, and might cause some strange race
conditions which will be very hard to debug.
Therefore, be wary of having too much logic inside a parent process. Try to push as much responsibilities as possible to other processes, such as children or siblings, and use parent only for coordination and reporting tasks.
Finally, since parent trap exits, it's possible to receive an occasional stray :EXIT
message
if the child crashes during its initialization.
By default use Parent.GenServer
receives such messages and ignores them. If you're implementing
your own handle_info
, make sure to include a clause for :EXIT
messages:
def handle_info({:EXIT, _pid, _reason}, state), do: {:noreply, state}
Link to this section Summary
Functions
Starts the parent process.
Callbacks
Invoked when some children have terminated.
Link to this section Types
Specs
options() :: [Parent.option() | GenServer.option()]
Specs
state() :: term()
Link to this section Functions
Specs
start_link(module(), arg :: term(), options()) :: GenServer.on_start()
Starts the parent process.
Link to this section Callbacks
Specs
handle_stopped_children(info :: Parent.stopped_children(), state()) :: {:noreply, new_state} | {:noreply, new_state, timeout() | :hibernate} | {:stop, reason :: term(), new_state} when new_state: state()
Invoked when some children have terminated.
The info
map will contain all the children which have been stopped together. For example,
if child A is bound to child B, and child B terminates, parent will also terminate the child
A. In this case, handle_stopped_children
is invoked only once, with the info
map containing
entries for both children.
This callback will not be invoked in the following cases:
- a child is terminated by invoking
Parent
functions such asParent.shutdown_child/1
- a child is restarted
- a child is not ephemeral (see "Ephemeral children" in
Parent
for details)