parent v0.11.0-rc.1 Parent View Source

Functions for implementing a parent process.

A parent process is a process that manages the lifecycle of its children. Typically the simplest approach is to use higher-level abstractions, such as Parent.Supervisor or Parent.GenServer. The common behaviour for every parent process is implemented in this module, and therefore it is described in this document.

Overview

A parent process has the following properties:

  1. It traps exits and uses the shutdown: :infinity shutdown strategy.
  2. It keeps track of its children.
  3. It presents itself to the rest of the OTP as a supervisor, which means that generic code walking the supervision tree, such as OTP release handler, will also iterate the parent's subtree.
  4. Before terminating, it stops its children synchronously, in the reverse startup order.

You can interact with the parent process from other processes using functions from the Parent.Client module. If you want to manipulate the parent from the inside, you can use the functions from this module.

Initialization

A parent process has to be initialized using initialize/1. This function takes the following initialization options:

  • :max_restarts and :max_seconds - same as with Supervisor, with the same defaults
  • :registry? - If true, the parent will manage its own ETS-based child registry. See the "Child discovery" section for details.

When using higher-level abstractions, these options are typically passed throguh start functions, such as Parent.Supervisor.start_link/2.

Child specification

Child specification describes how the parent starts and manages a child. This specification if passed to functions such as start_child/1, Parent.Client.start_child/2, or Parent.Supervisor.start_link/2 to start a child process.

The specification is a map which is a superset of the Supervisor child specifications. All the fields that are shared with Supervisor have the same effect.

It's worth noting that the :id field is optional. If not provided, the child will be anonymous, and you can only manage it via its pid. Therefore, the minimum required child specification is %{start: mfa_or_zero_arity_fun}.

Also, just like with Supervisor, you can provide module | {module, arg} when starting a child. See Supervisor.child_spec/1 for details.

To modify a child specification, Parent.child_spec/2 can be used.

Child restart

The :restart option controls what the parent will do if the child terminates. The following values are supported:

  • :permanent - A child is automatically restarted if it stops. This is the default value.
  • :transient - A child is automatically restarted only if it exits abnormally.
  • :temporary - A child is not automatically restarted.

If a child is taken down by the Parent because one of child's dependencies has terminated, the child will be always restarted, regardless of its restart strategy and ephemeral status.

See "Bound children" and "Ephemeral children" for more details.

Maximum restart frequency

Similarly to Supervisor, a parent process keeps track of the amount of restarts, and self-terminates if maximum threshold (defaults to 3 restarts in 5 seconds) is exceeded.

In addition, you can provide child specific thresholds by including :max_restarts and :max_seconds options in child specification. Finally, note that :max_restarts can be set to :infinity (both for the parent and each child). This can be useful if you want to disable the parent global limit, and use child-specific limits.

Bound children

You can bind the lifecycle of each child to the lifecycles of its older siblings. This is roughly similar to the :rest_for_one supervisor strategy.

For example, if you want to start two children, consumer and producer, and bind the producer's lifecycle to the consumer, you need the following child specifications:

consumer_spec = %{
  id: :consumer,
  # ...
}

producer_spec = %{
  id: :producer,
  binds_to: [:consumer]
}

This will make sure that if the consumer stops, the producer is taken down as well.

For this to work, you need to start the consumer before the producer. In other words, a child can only be bound to its older siblings.

It's worth noting that bindings are transitive. If a child A is bound to the child B, which is in turns bound to child C, then child A also depends on child C. If child C stops, B and A will be stopped to.

Finally, because of binding semantics (see Lifecycle dependency consequences), a child can only be bound to a sibling with the same or stronger restart option, where restart strengths can be defined as permanent > transient > temporary. So for example, a permanent child can't be bound to a temporary child.

Shutdown groups

A shutdown group is a mechanism that roughly emulates the :one_for_all supervisor strategy. For example, to set up a two-way lifecycle dependency between the consumer and the producer, we can use the following specifications:

consumer_spec = %{
  id: :consumer,
  shutdown_group: :consumer_and_producer
  # ...
}

producer_spec = %{
  id: :producer,
  shutdown_group: :consumer_and_producer
}

In this case, when any child of the group terminates, the other children will be taken down as well.

All children belonging to the same shutdown group must use the same :restart option.

Note that a child can be a member of some shutdown group, and bound to other older siblings.

Lifecycle dependency consequences

As has been mentioned, a lifecycle dependency means that a child is taken down when its dependency stops. This will happen irrespective of how the child has been stopped. Even if you manually stop the child using functions such as shutdown_child/1 or Parent.Client.shutdown_child/2, the siblings bound to it will be taken down.

In general, parent doesn't permit the state which violates binding settings. If the process A is bound to the process B, you can never reach the state where A is running but B isn't. Of course, since things are taking place concurrently, such state might briefly exists until parent is able to shutdown all bound processes.

Restart flow

Process restarts happen automatically, if a permanent child stops or if a transient child crashes. They can also happen manually, when you invoke restart_child/1 or if you're manually returning terminated non-restarted children with the function return_children/2. In all these situations, the flow is the same.

When a child stops, parent will take down all the siblings bound to it, and then attempt to restart the child and its siblings. This is done by starting processes synchronously, one by one, in their startup order. If all processes are started successfully, restart has succeeded.

If some process fails to start, the parent won't try to start younger siblings. If some of the successfully started children are bound to non-started siblings, they will be taken down as well. This happens because parent won't permit the state which doesn't conform to the binding requirements.

Therefore, a restart may partially succeed, with some children not being started. In this case, the parent will retry to restart the remaining children.

An attempt to restart a child which failed to restart is considered as a crash and contributes to the restart intensity. Thus, if a child repeatedly fails to restart, the parent will give up at some point, according to restart intensity settings.

When the children are restarted, they will be started in the original startup order. The restarted children keep their original startup order with respect to non-restarted children. For example, suppose that four children are running: A, B, C, and D, and children B and D are restarted. If the parent process then stops, it will take the children down in the order D, C, B, and A.

Finally, it's worth noting that if termination of one child causes the restart of multiple children, parent will treat this as a single restart event when calculating the restart frequency and considering possible self-termination.

Child timeout

You can optionally include the :timeout option in the child specification to ask the parent to terminate the child if it doesn't stop in the given time. In this case, the child's shutdown strategy is ignore, and the child will be forcefully terminated (using the :kill exit signal).

A non-temporary child which timeouts will be restarted.

Child discovery

Children can be discovered by other processes using functions such as Parent.Client.child_pid/2, or Parent.Client.children/1. By default, these functions will perform a synchronous call into the parent process. This should work fine as long as the parent is not pressured by various events, such as frequent children stopping and starting, or some other custom logic.

In such cases you can consider setting the registry? option to true when initializing the parent process. When this option is set, parent will create an ETS table which will be used by the discovery functions.

In addition, parent supports maintaining the child-specific meta information. You can set this information by providing the :meta field in the child specification, update it through functions such as update_child_meta/2 or Parent.Client.update_child_meta/3, and query it through Parent.Client.child_meta/2.

Ephemeral children

By default children are not ephemeral, which means that the parent will keep the child entry in its list, even if the child is not running, which can happen in the following cases:

  • the child start function returns :ignore
  • a transient child terminates normally
  • a temporary child terminates

Functions such as children/0 will include non-running children, with their pid set to :undefined. You can remove the children from the list by invoking shutdown_child/1, or restart them with restart_child/1.

This behaviour is typically desired for "static" supervisors where the list of children is predefined and finite. However, when you're adding children dynamically, you typically want the child to be "forgotten" when it is not running. In such cases you can mark the child as ephemeral by including ephemeral?: true option in the child specification.

Building custom parent processes

If available parent behaviours don't fit your purposes, you can consider building your own behaviour or a concrete parent process. In this case, the functions of this module will provide the necessary plumbing.

The basic idea is presented in the following sketch:

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

More specifically, to build a parent process you need to do the following:

  1. Invoke initialize/0 when the process is started.
  2. Use functions such as start_child/1 to work with child processes.
  3. When a message is received, invoke handle_message/1 before handling the message yourself.
  4. If you receive a shutdown exit message from your parent, stop the process.
  5. Before terminating, invoke shutdown_all/1 to stop all the children.
  6. Use :infinity as the shutdown strategy for the parent process, and :supervisor for its type.
  7. If the process is a GenServer, handle supervisor calls (see supervisor_which_children/0 and supervisor_count_children/0).
  8. Implement format_status/2 (see Parent.GenServer for details) where applicable.

If the parent process is powered by a non-interactive code (e.g. Task), make sure to receive messages sent to that process, and handle them properly (see points 3 and 4).

You can take a look at the code of Parent.GenServer for specific details.

Link to this section Summary

Functions

Returns true if the child process is still running, false otherwise.

Returns the id of a child process with the given pid.

Returns the meta associated with the given child id.

Returns the pid of a child process with the given id.

Returns the list of running child processes in the startup order.

Should be invoked by the parent process for each incoming message.

Initializes the state of the parent process.

Returns true if the parent state is initialized.

Returns the count of children.

Restarts the child and its siblings.

Starts new instances of stopped children.

Terminates all running child processes.

Shuts down the child and all siblings depending on it, and removes them from the parent state.

Synchronously starts all children.

Starts the child described by the specification.

Should be invoked by the behaviour when handling :count_children GenServer call.

Should be invoked by the behaviour when handling :get_childspec GenServer call.

Should be invoked by the behaviour when handling :which_children GenServer call.

Updates the meta of the given child process.

Link to this section Types

Link to this type

child()

View Source
child() :: %{id: child_id(), pid: pid() | :undefined, meta: child_meta()}
Link to this type

child_id()

View Source
child_id() :: term()
Link to this type

child_meta()

View Source
child_meta() :: term()
Link to this type

child_ref()

View Source
child_ref() :: child_id() | pid()
Link to this type

child_spec()

View Source
child_spec() :: %{
  :start => start(),
  optional(:id) => child_id(),
  optional(:modules) => [module()] | :dynamic,
  optional(:type) => :worker | :supervisor,
  optional(:meta) => child_meta(),
  optional(:shutdown) => shutdown(),
  optional(:timeout) => pos_integer() | :infinity,
  optional(:restart) => :temporary | :transient | :permanent,
  optional(:max_restarts) => non_neg_integer() | :infinity,
  optional(:max_seconds) => pos_integer(),
  optional(:binds_to) => [child_ref()],
  optional(:shutdown_group) => shutdown_group(),
  optional(:ephemeral?) => boolean()
}
Link to this type

handle_message_response()

View Source
handle_message_response() :: {:stopped_children, stopped_children()} | :ignore
Link to this type

on_start_child()

View Source
on_start_child() :: Supervisor.on_start_child() | {:error, start_error()}
Link to this type

option()

View Source
option() ::
  {:max_restarts, non_neg_integer() | :infinity}
  | {:max_seconds, pos_integer()}
  | {:registry?, boolean()}
Link to this type

shutdown()

View Source
shutdown() :: non_neg_integer() | :infinity | :brutal_kill
Link to this type

shutdown_group()

View Source
shutdown_group() :: term()
Link to this type

start_error()

View Source
start_error() ::
  :invalid_child_id
  | {:missing_deps, [child_ref()]}
  | {:forbidden_bindings, [from: child_id() | nil, to: [child_ref()]]}
  | {:non_uniform_shutdown_group, [shutdown_group()]}
Link to this type

start_spec()

View Source
start_spec() :: child_spec() | module() | {module(), term()}
Link to this type

stopped_children()

View Source
stopped_children() :: %{
  required(child_id()) => %{
    optional(atom()) => any(),
    :pid => pid() | :undefined,
    :meta => child_meta(),
    :exit_reason => term()
  }
}

Link to this section Functions

Returns true if the child process is still running, false otherwise.

Note that this function might return true even if the child has terminated. This can happen if the corresponding :EXIT message still hasn't been processed, and also if a non-ephemeral child is not running.

Link to this function

child_id(child_pid)

View Source
child_id(pid()) :: {:ok, child_id()} | :error

Returns the id of a child process with the given pid.

Link to this function

child_meta(child_ref)

View Source
child_meta(child_ref()) :: {:ok, child_meta()} | :error

Returns the meta associated with the given child id.

Link to this function

child_pid(child_id)

View Source
child_pid(child_id()) :: {:ok, pid()} | :error

Returns the pid of a child process with the given id.

Link to this function

child_spec(spec, overrides \\ [])

View Source
child_spec(start_spec(), Keyword.t() | child_spec()) :: child_spec()
Link to this function

children()

View Source
children() :: [child()]

Returns the list of running child processes in the startup order.

Link to this function

handle_message(message)

View Source
handle_message(term()) :: handle_message_response() | nil

Should be invoked by the parent process for each incoming message.

If the given message is not handled, this function returns nil. In such cases, the client code should perform standard message handling. Otherwise, the message has been handled by the parent, and the client code shouldn't treat this message as a standard message (e.g. by calling handle_info of the callback module).

If :ignore is returned, the message has been processed, and the client code should ignore it. Finally, if the return value is {:stopped_children, info}, it indicates that some ephemeral processes have stopped and have been removed from parent. A client may do some extra processing in this case.

Note that you don't need to invoke this function in a Parent.GenServer callback module.

Link to this function

initialize(opts \\ [])

View Source
initialize(opts()) :: :ok

Initializes the state of the parent process.

This function should be invoked once inside the parent process before other functions from this module are used. If a parent behaviour, such as Parent.GenServer, is used, this function must not be invoked.

Link to this function

initialized?()

View Source
initialized?() :: boolean()

Returns true if the parent state is initialized.

Link to this function

num_children()

View Source
num_children() :: non_neg_integer()

Returns the count of children.

Link to this function

parent_spec(overrides \\ [])

View Source
parent_spec(Keyword.t() | child_spec()) :: child_spec()
Link to this function

restart_child(child_ref)

View Source
restart_child(child_ref()) :: :ok | :error

Restarts the child and its siblings.

See "Restart flow" for details on restarting procedure.

Link to this function

return_children(stopped_children)

View Source
return_children(stopped_children()) :: :ok

Starts new instances of stopped children.

This function can be invoked to return stopped children back to the parent. Essentially, this function works the same as automatic restarts.

The stopped_children information is obtained via functions such as shutdown_child/1 or shutdown_all/1. In addition, Parent will provide this info via handle_message/1 if an ephemeral child stops and is not restarted.

Link to this function

shutdown_all(reason \\ :shutdown)

View Source
shutdown_all(term()) :: stopped_children()

Terminates all running child processes.

Children are terminated synchronously, in the reverse order from the order they have been started in. All corresponding :EXIT messages will be pulled from the mailbox.

Link to this function

shutdown_child(child_ref)

View Source
shutdown_child(child_ref()) :: {:ok, stopped_children()} | :error

Shuts down the child and all siblings depending on it, and removes them from the parent state.

This function will also shut down all siblings directly and transitively bound to the given child. The function will wait for the child to terminate, and pull the :EXIT message from the mailbox.

All terminated children are removed from the parent state. The stopped_children structure describes all of these children, and can be used with return_children/1 to manually restart these processes.

Link to this function

start_all_children!(child_specs)

View Source
start_all_children!([start_spec()]) :: [pid() | :undefined]

Synchronously starts all children.

If some child fails to start, all of the children will be taken down and the parent process will exit.

Link to this function

start_child(child_spec)

View Source
start_child(start_spec()) :: on_start_child()

Starts the child described by the specification.

Link to this function

supervisor_count_children()

View Source
supervisor_count_children() :: [
  specs: non_neg_integer(),
  active: non_neg_integer(),
  supervisors: non_neg_integer(),
  workers: non_neg_integer()
]

Should be invoked by the behaviour when handling :count_children GenServer call.

See supervisor_which_children/0 for details.

Link to this function

supervisor_get_childspec(child_ref)

View Source
supervisor_get_childspec(child_ref()) ::
  {:ok, child_spec()} | {:error, :not_found}

Should be invoked by the behaviour when handling :get_childspec GenServer call.

See :supervisor.get_childspec/2 for details.

Link to this function

supervisor_which_children()

View Source
supervisor_which_children() :: [
  {term(), pid(), :worker, [module()] | :dynamic}
]

Should be invoked by the behaviour when handling :which_children GenServer call.

You only need to invoke this function if you're implementing a parent process using a behaviour which forwards GenServer call messages to the handle_call callback. In such cases you need to respond to the client with the result of this function. Note that parent behaviours such as Parent.GenServer will do this automatically.

If no translation of GenServer messages is taking place, i.e. if you're handling all messages in their original shape, this function will be invoked through handle_message/1.

Link to this function

update_child_meta(child_ref, updater)

View Source
update_child_meta(child_ref(), (child_meta() -> child_meta())) :: :ok | :error

Updates the meta of the given child process.