parent v0.8.0 Parent.GenServer behaviour View Source

A GenServer extension which simplifies parenting of child processes.

This behaviour helps implementing a GenServer which also needs to directly start child processes and handle their termination.

Starting the process

The usage is similar to GenServer. You need to use the module and start the process:

def MyParentProcess do
  use Parent.GenServer

  def start_link(arg) do
    Parent.GenServer.start_link(__MODULE__, arg, options \\ [])
  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, and writing the same callbacks:

def MyParentProcess do
  use Parent.GenServer

  def do_something(pid, arg), do: GenServer.call(pid, {:do_something, arg})

  ...

  @impl GenServer
  def init(arg), do: {:ok, initial_state(arg)}

  @impl GenServer
  def handle_call({:do_something, arg}, _from, state),
    do: {:reply, response(state, arg), next_state(state, arg)}
end

Compared to plain GenServer, there are following differences:

  • A Parent.GenServer traps exits by default.
  • The generated child_spec/1 has the :shutdown configured to :infinity.
  • The generated child_spec/1 specifies the :type configured to :supervisor

Starting child processes

To start a child process, you can invoke start_child/1 in the parent process:

def handle_call(...) do
  Parent.GenServer.start_child(child_spec)
  ...
end

The function takes a child spec map which is similar to Supervisor child specs. The map has the following keys:

  • :id (required) - a term uniquely identifying the child
  • :start (required) - an MFA, or a zero arity lambda invoked to start the child
  • :meta (optional) - a term associated with the started child, defaults to nil
  • :shutdown (optional) - same as with Supervisor, defaults to 5000
  • :timeout (optional) - timeout after which the child is killed by the parent, see the timeout section below, defaults to :infinity

The function described with :start needs to start a linked process and return the result as {:ok, pid}. For example:

Parent.GenServer.start_child(%{
  id: :hello_world,
  start: {Task, :start_link, [fn -> IO.puts "Hello, World!" end]}
})

You can also pass a zero-arity lambda for :start:

Parent.GenServer.start_child(%{
  id: :hello_world,
  start: fn -> Task.start_link(fn -> IO.puts "Hello, World!" end) end
})

Finally, a child spec can also be a module, or a {module, arg} function. This works similarly to supervisor specs, invoking module.child_spec/1 is which must provide the final child specification.

Handling child termination

When a child process terminates, handle_child_terminated/5 will be invoked. The default implementation is injected into the module, but you can of course override it:

@impl Parent.GenServer
def handle_child_terminated(id, child_meta, pid, reason, state) do
  ...
  {:noreply, state}
end

The return value of handle_child_terminated is the same as for handle_info.

Timeout

If a positive integer is provided via the :timeout option, the parent will terminate the child if it doesn't stop within the given time. In this case, handle_child_terminated/5 will be invoked with the exit reason :timeout.

Working with child processes

This module provide various functions for managing child processes. For example, you can enumerate running children with children/0, fetch child meta with child_meta/1, or terminate a child process with shutdown_child/1.

Termination

The behaviour takes down the child processes during termination, to ensure that no child process is running after the parent has terminated. This happens after the terminate/1 callback returns. Therefore in terminate/1 the child processes are still running, and you can interact with them.

Supervisor compliance

A process powered by Parent.GenServer can handle supervisor specific messages, which means that for all intents and purposes, such process is treated as a supervisor. As a result, children of parent will be included in the hot code reload process.

Link to this section Summary

Functions

Awaits for the child to terminate.

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.

Returns the count of running child processes.

Terminates all running child processes.

Terminates the child.

Starts the child described by the specification.

Starts the parent process.

Updates the meta of the given child process.

Link to this section Types

Link to this type

child_meta()

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

child_spec()

View Source
child_spec() :: %{
  :id => id(),
  :start => start(),
  optional(:modules) => [module()] | :dynamic,
  optional(:type) => :worker | :supervisor,
  optional(:meta) => child_meta(),
  optional(:shutdown) => shutdown(),
  optional(:timeout) => pos_integer() | :infinity
}
Link to this type

on_start_child()

View Source
on_start_child() :: on_start_child()
Link to this type

shutdown()

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

start()

View Source
start() :: (() -> on_start_child()) | {module(), atom(), [term()]}

Link to this section Functions

Link to this function

await_child_termination(id, timeout)

View Source
await_child_termination(id(), non_neg_integer() | :infinity) ::
  {pid(), child_meta(), reason :: term()} | :timeout

Awaits for the child to terminate.

If the function succeeds, handle_child_terminated/5 will not be invoked.

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.

Link to this function

child_id(pid)

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

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

Link to this function

child_meta(id)

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

Returns the meta associated with the given child id.

Link to this function

child_pid(id)

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

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

Link to this function

children()

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

Returns the list of running child processes.

Link to this function

num_children()

View Source
num_children() :: non_neg_integer()

Returns the count of running child processes.

Link to this function

shutdown_all(reason \\ :shutdown)

View Source
shutdown_all(reason :: term()) :: :ok

Terminates all running child processes.

The order in which processes are taken down is not guaranteed. The function returns after all of the processes have been terminated.

Link to this function

shutdown_child(child_id)

View Source
shutdown_child(id()) :: :ok

Terminates the child.

This function waits for the child to terminate. In the case of explicit termination, handle_child_terminated/5 will not be invoked.

Link to this function

start_child(child_spec)

View Source
start_child(child_spec() | module() | {module(), term()}) :: on_start_child()

Starts the child described by the specification.

Link to this function

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

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

Starts the parent process.

Link to this function

update_child_meta(id, updater)

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

Updates the meta of the given child process.

Link to this section Callbacks

Link to this callback

handle_child_terminated(id, child_meta, pid, reason, state)

View Source
handle_child_terminated(id(), child_meta(), pid(), reason :: term(), state()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate}
  | {:stop, reason :: term(), new_state}
when new_state: state()

Invoked when a child has terminated.