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

Callbacks

Invoked when some children have terminated.

Link to this section Types

Link to this section Functions

Link to this function

start_link(module, arg, options \\ [])

View Source

Specs

start_link(module(), arg :: term(), options()) :: GenServer.on_start()

Starts the parent process.

Link to this section Callbacks

Link to this callback

handle_stopped_children(info, state)

View Source

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 as Parent.shutdown_child/1
  • a child is restarted
  • a child is not ephemeral (see "Ephemeral children" in Parent for details)