View Source Horde.Process behaviour (horde_process v0.1.0)
A module that uses Horde.Process
is a GenServer
process that is managed via Horde. Horde processes are started and supervised by a Horde Supervisor and registered with a Horde Registry. Many boilerplate functions providing interactions with Horde are automatically imported by this module when you use Horde.Process
.
Required options for use Horde.Process
are as follows:
:supervisor
- The name of the Horde Supervisor to use, e.g.MyApp.HordeSupervisor
.:registry
- The name of the Horde Registry to use, e.g.MyApp.HordeRegistry
.
Additionally, all Horde Process modules MUST implement the following functions:
process_id/1
which should return a unique process identifier. This is used for process lookup and registration.child_spec/1
returns the child spec used by the supervisor to start the process.
The above functions are defined via @callback
to ensure compiler feedback on the implementation of these functions.
Horde Registry Notice
When implementing a Horde Process, it is CRITICAL to use {:continue, term}
in the init/1
callback whenever non-trivial initialization is required. Horde CANNOT add the process to the registry until init/1
completes. The longer it takes init/1
to finish, the longer before the process can enter the registry and be used.
This will have a knock-on effect of causing fetch/1
to return nil
even though start/1
will return :ignore
. To reduce the likelihood of this scenario, it's recommended to use {:continue, term}
in init/1
to ensure the process is started and registered as quickly as possible, then performing all non-trivial initialization in handle_continue/2
.
Even if you have the fastest init/1
ever, there's still a possibility - within a distributed system - for Horde to be unable to fetch a process but also unable to start the process. This is because the local node ensures that you can't start a second process with the same unique process id, but the Horde registry is eventually consistent and so remote nodes won't know if a process is starting up elsewhere. And so this module implements "wait functions" to help retry the entire process a set number of times over a set period of time. It isn't guaranteed to always work, but it's configurable so that you can make it work best for your application.
Example
If you wanted to use this module to create a very simple Horde Process, you could do the following:
defmodule MyApp.User.Process do
use Horde.Process, supervisor: MyApp.User.HordeSupervisor, registry: MyApp.User.HordeRegistry
@impl Horde.Process
def process_id(%{"user_id" => user_id}), do: user_id
def process_id(%{user_id: user_id}), do: user_id
def process_id(user_id) when is_binary(user_id), do: user_id
@impl Horde.Process
def child_spec(user_id) do
%{
id: user_id,
start: {__MODULE__, :start_link, [user_id]},
restart: :transient,
shutdown: 10_000
}
end
@impl GenServer
# Set up the process state quickly and have `handle_continue/2` do the rest.
def init(user_id) do
Process.flag(:trap_exit, true)
{:ok, user_id, {:continue, :init}}
end
@impl GenServer
def handle_continue(:init, user_id) do
{:ok, user} = MyApp.User.fetch(user_id)
{:noreply, user}
end
end
The supervisor and registry must be started appropriately by your application to be used by the Horde Process.
We can see that process_id/1
can convert a map or a binary string to a user id, and that child_spec/1
takes in that user id to return a child process specification.
We can also see the use of {:continue, :init}
to ensure init/1
completes as quickly as possible. Since we're loading the user from the database, we don't want to block the process from being registered with the Horde registry until that's done. Instead, we set the process state to user_id
temporarily and pass along {:continue, :init}
so that we know to take that user id and grab the full user schema from the database in handle_continue/2
.
You can then implement handle_call/3
and handle_cast/2
as you would with any other GenServer
module. You may either invoke these directly or use the imported functions provided by Horde.Process
, e.g. MyApp.User.Process.call!/2
.
Summary
Callbacks
Given a unique process id, returns a child spec that will be used by the Horde Supervisor to start the process.
Given an arbitrary argument, return a unique process identifier. This will, among other things, be used to register the process with a Horde Registry.
Functions
When you use Horde.Process
you must specify at least the following options
Callbacks
@callback child_spec(term()) :: Supervisor.child_spec()
Given a unique process id, returns a child spec that will be used by the Horde Supervisor to start the process.
When using the functions provided by Horde.Process
, the process id given will come from process_id/1
.
Given an arbitrary argument, return a unique process identifier. This will, among other things, be used to register the process with a Horde Registry.
Functions
When you use Horde.Process
you must specify at least the following options:
:supervisor
- The name of the Horde Supervisor to use, e.g.MyApp.HordeSupervisor
.:registry
- The name of the Horde Registry to use, e.g.MyApp.HordeRegistry
.
And you may optionally specify:
:wait_sleep
- The number of milliseconds to sleep between attempts to fetch a process from the registry. Defaults to100
.:wait_max
- The maximum number of attempts to fetch a process from the registry. Defaults to5
.
In addition to generating functions that are tied to the given Horde Supervisor and Registry modules, this macro also performs use GenServer
and @behavior Horde.Process
.
Wait Options
The :wait_*
options are used by wait_for_init/2
to determine how long to wait for a process to be registered before giving up and returning {:error, :not_found}
. Since the registry is eventually consistent, it's possible to fetch a process that can't be started because it's already in the middle of starting, but also which has not yet been added to (or replicated by) the Horde Registry. This is a common scenario when using start/1
and/or fetch/1
at the same time from different nodes in a distributed system.
The maximum amount of time that wait_for_init/2
will wait before failing is wait_sleep * wait_max
milliseconds, which defaults to 500 milliseconds. If your application is creating and registering processes quickly, you can try decreasing :wait_sleep
while increasing :wait_max
; this means retries will happen more frequently. If, however, you application is creating and registering processes slowly, you can try increasing :wait_sleep
to avoid unnecessary retries. If you prefer to fail quickly when a process is not registered, you can decrease :wait_max
to a lower number.