Current section
Files
Jump to
Current section
Files
lib/process_hub.ex
defmodule ProcessHub do
@moduledoc """
This is the main public API module for the `ProcessHub` library. It is recommended to use
only the functions defined in this module to interact with the `ProcessHub` library.
ProcessHub is a library that distributes processes within the BEAM cluster. It is designed to
be used as a building block for distributed applications that require process distribution
and synchronization across multiple nodes.
"""
alias ProcessHub.Service.HookManager
alias ProcessHub.Service.ProcessRegistry
alias ProcessHub.Service.Distributor
alias ProcessHub.Future
@typedoc """
The `hub_id` defines the name of the hub. It is used to identify the hub.
"""
@type hub_id() :: atom()
@typedoc """
The `child_id` defines the name of the child. It is used to identify the child.
Each child must have a unique `child_id()` in the cluster. A single child may have
multiple `pid()`s across the cluster.
"""
@type child_id() :: atom() | binary()
@typedoc """
The `reply_to` defines the `pid()`s that will receive the response from the hub
when a child is started or stopped.
"""
@type reply_to() :: [pid()]
@typedoc """
The `child_spec` defines the specification of a child process.
"""
@type child_spec() :: Supervisor.child_spec()
@typedoc """
Defines the metadata that can be passed to the child process when it is started.
"""
@type child_metadata() :: %{
:tag => String.t()
}
@typedoc """
Defines a map of child_id to metadata for per-child metadata configuration.
Used with the `:child_metadata` option in `init_opts()`.
"""
@type child_metadata_map() :: %{child_id() => child_metadata()}
@typedoc """
The `init_opts()` defines the options that can be passed to the `start_children/3`, `start_child/3`,
`stop_children/3`, and `stop_child/3` functions.
- `:awaitable` - is optional and can be used to specify if the returned value should be an awaitable struct `ProcessHub.Future.t()`.
The awaitable struct provides a way to handle the result of the operation synchronously by passing it to the `ProcessHub.Future.await/1` function.
- `:async_wait` - (**deprecated** use `:awaitable`) is optional and is used to define whether the function should return a function that
can be used to wait for the children to start or stop. The default is `false`.
- `:timeout` - is optional and is used to define the timeout for the function. This timeout option
should be used with `async_wait: true`. The default is `5000` (5 seconds).
- `:check_existing` - is optional and is used to define whether the function should check if the children
are already started. The default is `true`.
- `:on_failure` - is optional and is used to define the action to take when the start operation fails.
The default is `:continue` which will continue to start the next child. The other option is `:rollback`
which will stop all the children that have been started.
- `:metadata` - is optional and is used to define the metadata that will be stored in the process registry
when the child is started. The metadata can be used to store any information that is needed to identify
the child process. This metadata will be applied to all children started in the batch.
- `:child_metadata` - is optional and is used to define metadata on a per-child basis. It should be a map
where keys are child_id values and values are metadata maps. When both `:metadata` and `:child_metadata` are
provided, children specified in `:child_metadata` will use their specific metadata, while other children will
fall back to the global `:metadata` value.
- `:disable_logging` - is optional and is used to define whether logging should be disabled for the child process
startup or shutdown. Mostly used for testing purposes.
- `:await_timeout` - is optional and is used to define the maximum lifetime for the spawned collector process.
After this time, the collector process will be terminated and attempts to collect the results using `ProcessHub.await/1` will fail.
The await_timeout option should be used with `awaitable: true`. The default is `60000` (60 seconds).
- `:call_timeout` - is optional and is used to override the auto-calculated GenServer.call timeout
for the initial request to the coordinator. This timeout applies to bulk operations such as starting children,
stopping children, and migrations. By default, the timeout is calculated as `5000ms + (1ms × child_count)`,
e.g., 10k children = 15s, 30k children = 35s. Set to `:infinity` for no timeout, or a specific integer in milliseconds.
"""
@type init_opts() :: [
awaitable: boolean(),
async_wait: boolean(),
timeout: non_neg_integer(),
check_existing: boolean(),
on_failure: :continue | :rollback,
metadata: child_metadata(),
child_metadata: child_metadata_map(),
disable_logging: boolean(),
await_timeout: non_neg_integer(),
call_timeout: timeout()
]
@typedoc """
The `stop_opts()` defines the options that can be passed to the `stop_children/3` and `stop_child/3` functions.
- `:awaitable` - is optional and can be used to specify if the returned value should be an awaitable struct `ProcessHub.Future.t()`.
The awaitable struct provides a way to handle the result of the operation synchronously by passing it to the `ProcessHub.Future.await/1` function.
- `:async_wait` - (**deprecated** use `:awaitable`) is optional and is used to define whether the function should return a function that
can be used to wait for the children to stop. The default is `false`.
- `:timeout` - is optional and is used to define the maximum time for the function to complete.
This timeout option should be used with `async_wait: true`. The default is `5000` (5 seconds).
- `:await_timeout` - is optional and is used to define the maximum lifetime for the spawned collector process.
After this time, the collector process will be terminated and attempts to collect the results using `ProcessHub.await/1` will fail.
The await_timeout option should be used with `awaitable: true`. The default is `60000` (60 seconds).
- `:call_timeout` - is optional and is used to override the auto-calculated GenServer.call timeout
for the initial request to the coordinator. This timeout applies to bulk operations such as starting children,
stopping children, and migrations. By default, the timeout is calculated as `5000ms + (1ms × child_count)`,
e.g., 10k children = 15s, 30k children = 35s. Set to `:infinity` for no timeout, or a specific integer in milliseconds.
"""
@type stop_opts() :: [
awaitable: boolean(),
async_wait: boolean(),
timeout: non_neg_integer(),
await_timeout: non_neg_integer(),
call_timeout: timeout()
]
@typedoc """
Defines the success result for a single child start operation.
"""
@type start_result() :: {child_id(), [{node(), pid()}]}
@typedoc """
Defines the failure result for a single child start operation.
The `term()` can be any term that describes the failure reason.
"""
@type start_failure() :: {child_id(), node(), term()}
@typedoc """
Defines the success result for a single child stop operation.
"""
@type stop_result() :: {child_id(), [node()]}
@typedoc """
Defines the failure result for a single child stop operation.
The `term()` can be any term that describes the failure reason.
"""
@type stop_failure() :: {child_id(), [term()]}
@typedoc """
This is the base configuration structure for the hub and has to be passed to the `start_link/1` function.
- `:hub_id` is the name of the hub and is required.
- `:child_specs` is optional and is used to define the child processes that will be started with the hub statically.
- `:hooks` are optional and are used to define the hooks that can be triggered on specific events.
- `:redundancy_strategy` is optional and is used to define the strategy for redundancy.
The default is `ProcessHub.Strategy.Redundancy.Singularity`.
- `:migration_strategy` is optional and is used to define the strategy for migration.
The default is `ProcessHub.Strategy.Migration.ColdSwap`.
- `:synchronization_strategy` is optional and is used to define the strategy for synchronization.
The default is `ProcessHub.Strategy.Synchronization.PubSub`.
- `:partition_tolerance_strategy` is optional and is used to define the strategy for partition tolerance.
The default is `ProcessHub.Strategy.PartitionTolerance.Divergence`.
- `:distribution_strategy` is optional and is used to define the strategy for process distribution.
The default is `ProcessHub.Strategy.Distribution.ConsistentHashing`.
- `:hubs_discover_interval` is optional and is used to define the interval in milliseconds
for hubs to start the discovery process. The default is `10000` (10 seconds).
- `:storage_purge_interval` is optional and is used to define the interval in milliseconds
for the janitor to clean up the old cache records when the TTL expires. The default is `15000` (15 seconds).
- `:migr_base_timeout` is optional and is used to define the base timeout in milliseconds
for the migration process to complete before the hub considers it as a failure.
The default is `15000` (15 seconds).
- `:dsup_max_restarts` is optional and is used to define the maximum number of restarts
for the distributed supervisor. See `Supervisor` child specification for more information.
The default is `100`.
- `:dsup_max_seconds` is optional and is used to define the maximum number of seconds
for the distributed supervisor to restart the child process.
See `Supervisor` child specification for more information. The default is `4`.
- `:dsup_shutdown_timeout` is optional and is used to define the timeout in milliseconds
for the distributed supervisor to wait before forcefully terminating itself
when receiving a shutdown signal.
- `:cluster_event_debounce` is optional and defines the debounce delay in milliseconds
for cluster events (nodedown, cluster_join). Each new event resets the timer, and events
are processed only after no new events arrive for this duration. Events are validated
against `Node.list()` before processing to filter stale events. The default is `500` (0.5 seconds).
- `:cross_node_request_timeout` is optional and defines the timeout in milliseconds
for each cross-node request in the batch. The default is `5000` (5 seconds).
- `:req_cleanup_interval` is optional and defines the interval in milliseconds
for cleaning up expired pending requests. The default is `60000` (1 minute).
- `:nodeup_reconcile_interval` is optional and defines the delay in milliseconds before a
per-node membership reconciliation fail-safe runs after a node connects (`:nodeup`). It is a
backstop for a missed `pg` cluster-join notification: when it fires, `pg` has settled, so the
node is merged via the normal snapshot-merge if (and only if) it is running the same hub.
This reconciles cluster *membership* and is unrelated to process-placement reconciliation.
The default is `3000` (3 seconds); `0` disables the fail-safe.
This fail-safe is **experimental** and may change in future releases.
- `:registry_backend` is optional and selects the storage backend for the
process registry table. The default is `:ets`. Accepted shapes:
- `:ets` — in-memory only. Registry contents are lost when the
coordinator restarts.
- `{:dets, opts}` — on-disk persistence via `ProcessHub.Service.Storage.Dets`.
Recognised opts: `path: String.t()` (file path; defaults to
`priv/process_hub/<hub_id>/registry.dets`). Reads are served from
DETS (slower than ETS).
- `{:durable_ets, opts}` — hybrid backend via
`ProcessHub.Service.Storage.DurableEts`. Reads are served from an
in-memory ETS table; mutations are mirrored synchronously to a
DETS file (`:dets.sync/1` per write) for restart-survival. The DETS
file is replayed into ETS on open, so reads are immediately
authoritative. Same `:path` opt and default location as `{:dets, _}`;
switching a hub between the two against the same path picks up the
existing rows. Recommended for read-heavy workloads that also need
durability.
- `{Module, opts}` — a custom module implementing
`ProcessHub.Service.Storage.Behaviour`.
See `guides/Persistence.md` for the recovery semantics, telemetry
events, and operational profile.
- `:auto_recovery` is optional and enables a marker-gated coordinator
boot lifecycle (`:recovering → :normal`). **Experimental** and may
change in future releases. The default is `false`. Requires a
persistent `:registry_backend` (e.g. `{:dets, _}`) to be useful; with
the default `:ets` backend the replay step finds an empty registry.
Accepted shapes:
- `false` — disabled (default).
- `true` — enabled with default options.
- `keyword()` — explicit options:
- `:marker_path` — operator override for the marker file location.
`nil` (default) resolves to
`:filename.basedir(:user_data, "process_hub")/<hub_id>/cluster.healthy`.
Marker present on boot → straight to `:normal`; absent →
`:recovering` with a cspecs-only replay, then `:normal`.
- `:recovery_timeout_ms` — safety ceiling on the `:recovering` state:
bounds the replay loop and force-opens the cluster-event queue gate
after this many ms. Default `30_000`. Range `[1_000, 600_000]`.
See `ProcessHub.Service.Recovery` (`recovery_state/1`, `await_normal/2`,
`prepare_recovery/1`, `prepare_recovery_cluster/1`) for the
recovery/operator API.
See `guides/Persistence.md` for the full state-machine, the marker
gate, the hooks, and the recommended pairing with
`registry_backend: {:dets, _}` or `{:durable_ets, _}` for full
restart-survival.
"""
@type t() :: %__MODULE__{
hub_id: hub_id(),
child_specs: [child_spec()],
hooks: ProcessHub.Service.HookManager.hook_handlers(),
redundancy_strategy:
ProcessHub.Strategy.Redundancy.Singularity.t()
| ProcessHub.Strategy.Redundancy.Replication.t(),
migration_strategy:
ProcessHub.Strategy.Migration.ColdSwap.t() | ProcessHub.Strategy.Migration.HotSwap.t(),
synchronization_strategy:
ProcessHub.Strategy.Synchronization.PubSub.t()
| ProcessHub.Strategy.Synchronization.Gossip.t(),
partition_tolerance_strategy:
ProcessHub.Strategy.PartitionTolerance.Divergence.t()
| ProcessHub.Strategy.PartitionTolerance.StaticQuorum.t()
| ProcessHub.Strategy.PartitionTolerance.DynamicQuorum.t()
| ProcessHub.Strategy.PartitionTolerance.MajorityQuorum.t(),
distribution_strategy:
ProcessHub.Strategy.Distribution.ConsistentHashing.t()
| ProcessHub.Strategy.Distribution.Guided.t()
| ProcessHub.Strategy.Distribution.CentralizedLoadBalancer.t(),
hubs_discover_interval: pos_integer(),
storage_purge_interval: pos_integer(),
migr_base_timeout: pos_integer(),
dsup_max_restarts: pos_integer(),
dsup_max_seconds: pos_integer(),
dsup_shutdown_timeout: pos_integer(),
cluster_event_debounce: pos_integer(),
cross_node_request_timeout: pos_integer(),
req_cleanup_interval: pos_integer(),
nodeup_reconcile_interval: non_neg_integer(),
registry_backend:
:ets
| {:dets, keyword()}
| {:durable_ets, keyword()}
| {module(), keyword()},
auto_recovery: false | true | keyword()
}
@enforce_keys [:hub_id]
defstruct [
:hub_id,
child_specs: [],
hooks: %{},
redundancy_strategy: %ProcessHub.Strategy.Redundancy.Singularity{},
migration_strategy: %ProcessHub.Strategy.Migration.ColdSwap{},
synchronization_strategy: %ProcessHub.Strategy.Synchronization.PubSub{},
partition_tolerance_strategy: %ProcessHub.Strategy.PartitionTolerance.Divergence{},
distribution_strategy: %ProcessHub.Strategy.Distribution.ConsistentHashing{},
hubs_discover_interval: 10000,
storage_purge_interval: 15000,
migr_base_timeout: 15000,
dsup_max_restarts: 100,
dsup_max_seconds: 4,
dsup_shutdown_timeout: 60000,
cluster_event_debounce: 500,
cross_node_request_timeout: 5000,
req_cleanup_interval: 60000,
nodeup_reconcile_interval: 3000,
registry_backend: :ets,
auto_recovery: false
]
@doc """
Starts a single child process that will be distributed across the cluster based on the configured distribution strategy.
The child process will be started on one or more nodes in the cluster depending on the redundancy configuration.
The `t:child_spec()` `:id` field must be unique across the entire cluster.
## Basic Usage
iex> child_spec = %{id: :my_child, start: {MyProcess, :start_link, [nil]}}
iex> ProcessHub.start_child(:my_hub, child_spec)
{:ok, :start_initiated}
By default, the `start_child/3` function is **asynchronous** and returns immediately with a confirmation
that the start operation has been initiated. The actual child process startup happens in the background.
To wait for the child to start and get detailed results, you can pass `awaitable: true` to the `opts` argument and
use the `ProcessHub.Future.await/1` function on the returned future.
See `t:init_opts/0` for all available options including timeout control, failure handling, and metadata.
## Synchronous Operation Example
When using the awaitable option, you get detailed information about the startup process including
the final status, started processes with their PIDs and node locations, any errors encountered,
and rollback information if applicable.
iex> child_spec = %{id: "my_child", start: {MyProcess, :start_link, [nil]}}
iex> future = ProcessHub.start_child(:my_hub, child_spec, [awaitable: true])
{
:ok,
%ProcessHub.Future{
future_resolver: #PID<0.253.0>,
ref: #Reference<0.717952655.487849985.214641>,
timeout: 60000
}
}
# Wait for the child to start and get the result.
iex> ProcessHub.Future.await(future)
%ProcessHub.StartResult{
status: :ok,
started: [{"my_child", ["node2@127.0.0.1": #PID<23618.225.0>]}],
errors: [],
rollback: false
}
# Get the status.
iex> ProcessHub.StartResult.status(result)
:ok
# Get the pid.
iex> ProcessHub.StartResult.pid(result)
#PID<23618.225.0>
> #### Handling startup results {: .info}
>
> See more about how to handle startup results at [Starting and stopping processes](startstop.html#startresult-api-functions)
"""
@spec start_child(hub_id(), child_spec(), init_opts()) ::
{:ok, :start_initiated}
| {:ok, Future.t()}
| {:error, :no_children | {:already_started, [atom | binary, ...]}}
def start_child(hub_id, child_spec, opts \\ []) do
start_children(hub_id, [child_spec], Keyword.put(opts, :return_first, true))
end
@doc """
Starts multiple child processes that will be distributed across the cluster.
This function works similarly to `start_child/3`, but it handles multiple children simultaneously,
making it much more efficient than calling `start_child/3` multiple times. Each child process
will be distributed according to the configured distribution strategy.
All child specifications must have unique `:id` fields across the entire cluster.
See `t:init_opts/0` for all available options including failure handling strategies.
## Examples
iex> child_specs = [
iex> %{id: "child1", start: {MyProcess, :start_link, [nil]}},
iex> %{id: "child2", start: {MyProcess, :start_link, [nil]}}
iex> ]
iex> # Start the child processes.
iex> future = ProcessHub.start_children(:my_hub, child_specs, [awaitable: true])
{
:ok,
%ProcessHub.Future{
future_resolver: #PID<0.222.0>,
ref: #Reference<0.2401267543.3981705217.217572>,
timeout: 60000
}
}
iex> # Wait for the children to start and get the result.
iex> result = ProcessHub.Future.await(future)
%ProcessHub.StartResult{
status: :ok,
started: [
{"child2", ["node1@127.0.0.1": #PID<0.237.0>]},
{"child1", ["node1@127.0.0.1": #PID<0.236.0>]}
],
errors: [],
rollback: false
}
iex> # Get the pids.
iex> ProcessHub.StartResult.pids(result)
[#PID<0.237.0>, #PID<0.236.0>]
## Per-child metadata
You can attach metadata to individual children using the `:child_metadata` option.
This is useful when you need to tag or categorize different children with specific metadata.
iex> child_specs = [
iex> %{id: "worker1", start: {MyWorker, :start_link, [1]}},
iex> %{id: "worker2", start: {MyWorker, :start_link, [2]}},
iex> %{id: "worker3", start: {MyWorker, :start_link, [3]}}
iex> ]
iex> child_metadata = %{
iex> "worker1" => %{tag: "critical"},
iex> "worker2" => %{tag: "standard"},
iex> "worker3" => %{tag: "batch"}
iex> }
iex> ProcessHub.start_children(:my_hub, child_specs, child_metadata: child_metadata)
{:ok, :start_initiated}
You can also combine per-child metadata with global metadata. Children specified in
`:child_metadata` will use their specific metadata, while others fall back to the global
`:metadata` value:
iex> child_specs = [
iex> %{id: "priority_worker", start: {MyWorker, :start_link, [1]}},
iex> %{id: "regular_worker", start: {MyWorker, :start_link, [2]}}
iex> ]
iex> ProcessHub.start_children(
iex> :my_hub,
iex> child_specs,
iex> metadata: %{tag: "default"},
iex> child_metadata: %{"priority_worker" => %{tag: "high_priority"}}
iex> )
{:ok, :start_initiated}
# In this example, "priority_worker" gets tag "high_priority"
# while "regular_worker" gets the default tag "default"
> #### Handling startup results {: .info}
>
> See more about how to handle startup results at [Starting and stopping processes](startstop.html#startresult-api-functions)
> #### Warning {: .warning}
>
> Using `start_children/3` with `awaitable: true` can lead to timeout errors,
> especially when the number of children is large.
"""
@spec start_children(hub_id(), [child_spec()], init_opts()) ::
{:ok, :start_initiated}
| {:ok, Future.t()}
| {:error,
:no_children
| {:error, :children_not_list}
| {:already_started, [atom | binary, ...]}}
def start_children(hub_id, child_specs, opts \\ []) when is_list(child_specs) do
call_timeout = Distributor.calculate_call_timeout(length(child_specs), opts)
GenServer.call(hub_id, {:init_children_start, child_specs, opts}, call_timeout)
end
@doc """
Stops a single child process across the entire cluster.
This function will terminate the specified child process on all nodes where it is currently running.
By default, this function is **asynchronous** and returns immediately with a confirmation
that the stop operation has been initiated. The actual process termination happens in the background.
To wait for the child to stop and get detailed results about the termination, you can pass
`awaitable: true` in the `opts` argument and use the `ProcessHub.Future.await/1` function
on the returned future.
## Basic Usage
iex> ProcessHub.stop_child(:my_hub, :my_child)
{:ok, :stop_initiated}
See `t:stop_opts/0` for all available options including timeout control.
## Example with synchronous wait
iex> ProcessHub.stop_child(:my_hub, "child1", [awaitable: true]) |> ProcessHub.Future.await()
%ProcessHub.StopResult{
status: :ok,
stopped: [{"child1", [:"node1@127.0.0.1"]}],
errors: []
}
> #### Handling stop results {: .info}
>
> See more about how to handle stop results at [Starting and stopping processes](startstop.html#stopresult-api-functions)
"""
@spec stop_child(hub_id(), child_id(), stop_opts()) ::
{:ok, :stop_initiated} | {:ok, Future.t()}
def stop_child(hub_id, child_id, opts \\ []) do
stop_children(hub_id, [child_id], Keyword.put(opts, :return_first, true))
end
@doc """
Stops multiple child processes across the entire cluster.
This function works similarly to `stop_child/3`, but handles multiple children simultaneously,
making it much more efficient than calling `stop_child/3` multiple times. Each specified
child process will be terminated on all nodes where it is currently running.
See `t:stop_opts/0` for all available options including timeout control.
## Examples
iex> ProcessHub.stop_children(:my_hub, ["child1", "child2"], [])
{:ok, :stop_initiated}
> #### Warning {: .warning}
>
> Using `stop_children/3` with `awaitable: true` can lead to timeout errors
> when stopping a large number of child processes.
> #### Handling stop results {: .info}
>
> See more how to handle stop results at [Starting and stopping processes](startstop.html#stopresult-api-functions)
"""
@spec stop_children(hub_id(), [child_id()], stop_opts()) ::
{:ok, :stop_initiated}
| {:ok, Future.t()}
| {:error, list()}
def stop_children(hub_id, child_ids, opts \\ []) do
call_timeout = Distributor.calculate_call_timeout(length(child_ids), opts)
GenServer.call(hub_id, {:init_children_stop, child_ids, opts}, call_timeout)
end
@doc """
Returns information about child processes in a format similar to `Supervisor.which_children/1`,
but wraps the result in a tuple containing the node name and the children.
> #### Performance Warning {: .warning}
>
> The `Supervisor.which_children/1` function is known to consume a lot of memory
> and can significantly affect performance. This problem is even more relevant when
> using the `:global` option, as it will make network calls to all nodes in the cluster.
>
> It is **highly recommended** to use `ProcessHub.process_list/2` instead, which is more efficient.
## Available Options:
- `:global` - returns a list of all child processes started by all nodes in the cluster.
The return result will be in the format of `[{:node, children}]`.
- `:local` - returns a list of all child processes started by the local node.
The return result will be in the format of `{:node, children}`.
"""
@deprecated "Will be removed from the next minor release. Use `ProcessHub.process_list/2` instead."
@spec which_children(hub_id(), [:global | :local] | nil) ::
list()
| {node(),
[{any, :restarting | :undefined | pid, :supervisor | :worker, :dynamic | list}]}
def which_children(hub_id, opts \\ []) do
GenServer.call(hub_id, {:get_dist_children, opts})
end
@doc """
Checks if a `ProcessHub` instance with the given `t:hub_id/0` is currently alive and operational.
A hub is considered alive if the `ProcessHub.Coordinator` process is
running and the hub is ready to handle requests and manage processes.
## Example
iex> ProcessHub.is_alive?(:not_existing)
false
"""
@spec is_alive?(hub_id()) :: boolean
def is_alive?(hub_id) do
case Process.whereis(hub_id) do
nil -> false
_ -> true
end
end
@deprecated "Use `ProcessHub.Future.await/1` instead. The implementation of this function will be replaced in the 0.5.x version. The `:rollback` option does not work with this function."
@doc """
This function can be used to wait for the `ProcessHub` child start or stop
functions to complete.
The `await/1` function should only be used with the `async_wait: true` option.
Keep in mind that the `await/1` function will block the calling process until
the response is received. If the response is not received within the timeout
period, the function will return `{:error, term()}`.
## Example
iex> {:ok, future} = ProcessHub.start_child(:my_hub, child_spec, [async_wait: true])
iex> ProcessHub.await(future)
{:ok, {:my_child, [{:mynode, #PID<0.123.0>}]}}
"""
@spec await(Future.t() | {:ok, Future.t()} | {:error, term()} | term()) ::
{:ok, start_result() | [start_result() | stop_result()]}
| {:error, {[start_failure() | stop_failure()], [start_result() | stop_result()]}}
| {:error, {[start_failure() | stop_failure()], [start_result() | stop_result()]},
:rollback}
def await({:ok, future}) when is_struct(future) do
Future.await(future)
end
def await(future) when is_struct(future) do
Future.await(future)
end
def await({:error, msg}), do: {:error, msg}
def await(_), do: {:error, :invalid_await_input}
@doc """
Returns the child specification for the `ProcessHub.Initializer` supervisor.
This function generates the proper supervisor child specification needed to start
a ProcessHub instance under a supervision tree. The returned specification follows
the standard Supervisor child_spec format and can be used directly in supervisor
child lists.
## Examples
iex> ProcessHub.child_spec(%ProcessHub{hub_id: :my_hub})
%{
id: :my_hub,
start: {
ProcessHub.Initializer,
:start_link,
[
%ProcessHub{
hub_id: :my_hub,
child_specs: [],
hooks: %{},
redundancy_strategy: %ProcessHub.Strategy.Redundancy.Singularity{},
migration_strategy: %ProcessHub.Strategy.Migration.ColdSwap{},
synchronization_strategy: %ProcessHub.Strategy.Synchronization.PubSub{
sync_interval: 15000
},
partition_tolerance_strategy: %ProcessHub.Strategy.PartitionTolerance.Divergence{},
distribution_strategy: %ProcessHub.Strategy.Distribution.ConsistentHashing{},
hubs_discover_interval: 10000,
storage_purge_interval: 15000,
migr_base_timeout: 15000,
dsup_max_restarts: 100,
dsup_max_seconds: 4,
dsup_shutdown_timeout: 60000
}
]
},
type: :supervisor
}
"""
@spec child_spec(t()) :: %{
id: ProcessHub,
start: {ProcessHub.Initializer, :start_link, [t()]},
type: :supervisor
}
def child_spec(opts) do
%{
id: opts.hub_id,
start: {ProcessHub.Initializer, :start_link, [opts]},
type: :supervisor
}
end
@doc """
Starts a new `ProcessHub` instance with the specified configuration.
This function initializes a complete ProcessHub system including the coordinator,
distributed supervisor, and all necessary supporting processes. The hub will be
ready to accept and manage child processes according to the configured strategies.
It is **strongly recommended** to start the ProcessHub under a supervision tree
to ensure proper fault tolerance and automatic restart capabilities.
## Example
iex> ProcessHub.start_link(%ProcessHub{hub_id: :my_hub})
{:ok, #PID<0.123.0>}
"""
@spec start_link(ProcessHub.t()) :: {:ok, pid()} | {:error, term()}
defdelegate start_link(hub_settings), to: ProcessHub.Initializer, as: :start_link
@doc """
Gracefully stops a running `ProcessHub` instance.
This function will cleanly shut down the ProcessHub system, including terminating
all managed child processes and cleaning up associated resources. The shutdown
process follows proper OTP shutdown protocols.
## Example
iex> ProcessHub.stop(:my_hub)
:ok
"""
@spec stop(atom) :: :ok | {:error, :not_alive}
defdelegate stop(hub_id), to: ProcessHub.Initializer, as: :stop
@doc """
Returns detailed information about a specific child process registered with the given `t:child_id/0`.
This function performs a fast local lookup by querying the local `ets` table and does not make
any network calls. The lookup returns the original child specification and location information
for all instances of the process across the cluster.
The return value is a tuple containing the `t:child_spec/0` and a list of `{node, pid}` tuples
indicating where the child process is running and its corresponding process ID.
Optionally, you can pass the `with_metadata: true` option to include any metadata
that was provided when the child process was started.
## Examples
# Lookup a child process by its ID.
iex> {child_spec, node_pids_tuples} = ProcessHub.child_lookup(:my_hub, "child1")
{
%{id: "child1", start: {MyProcess, :start_link, [nil]}},
["node1@127.0.0.1": #PID<0.1487.0>]
}
# Lookup a child process by its ID and include metadata.
iex> {child_spec, node_pid_tuples, metadata} = ProcessHub.child_lookup(:my_hub, "child1", with_metadata: true)
{
%{id: "child1", start: {MyProcess, :start_link, [nil]}},
["node1@127.0.0.1": #PID<0.1487.0>],
%{}
}
"""
@spec child_lookup(hub_id(), child_id(), with_metadata: boolean()) ::
{child_spec(), [{node(), pid()}]} | nil
defdelegate child_lookup(hub_id, child_id, opts \\ []), to: ProcessRegistry, as: :lookup
@doc """
Returns all processes that are registered with a specific tag.
This function queries the process registry and returns all children that
are registered with the given tag. This is particularly useful when you need
to group and query processes by category or type using custom tags.
To register a child with a tag, pass the `metadata` option to `start_child/3` or `start_children/3`
with a `:tag` key containing the desired tag value.
## Example
iex> ProcessHub.start_children(:my_hub, child_specs, [metadata: %{tag: "my_tag"}]) |> ProcessHub.Future.await()
iex>
iex> # get the processes by tag name.
iex> ProcessHub.tag_query(:my_hub, "my_tag")
[
{:my_child, [{:mynode, #PID<0.123.0>}],
{:my_child2, [{:mynode2, #PID<0.124.0>}]}
]
"""
@spec tag_query(hub_id(), String.t()) :: [{child_id(), [{node(), pid()}]}]
defdelegate tag_query(hub_id, tag), to: ProcessRegistry, as: :match_tag
@doc """
Dumps all information currently stored in the process registry.
This function returns a comprehensive view of the registry, including child specifications,
process IDs, node locations, and metadata for all registered processes. This is useful
for debugging, monitoring, or administrative tasks where you need a complete overview
of all managed processes in the hub.
## Example
iex> ProcessHub.registry_dump(:my_hub)
%{
"child1" => {
%{id: "child1", start: {MyProcess, :start_link, [nil]}},
[
"node1@127.0.0.1": #PID<0.1487.0>
],
%{}
},
"child2" => {
%{id: "child2", start: {MyProcess, :start_link, [nil]}},
[
"node1@127.0.0.1": #PID<0.1488.0>
],
%{}
}
}
"""
@spec registry_dump(hub_id()) :: ProcessHub.Service.ProcessRegistry.registry_dump()
defdelegate registry_dump(hub_id), to: ProcessRegistry, as: :dump
@doc """
Returns all information stored in the process registry.
This function performs a local query against the `ets` table and does not make any network calls.
> #### Deprecation Notice {: .warning}
>
> This function is deprecated and will be removed in a future version.
"""
@deprecated "Use `ProcessHub.registry_dump/1` instead"
@spec process_registry(hub_id()) :: ProcessHub.Service.ProcessRegistry.registry()
defdelegate process_registry(hub_id), to: ProcessRegistry, as: :registry
@doc """
Returns all process IDs (PIDs) for a specific child process.
This function retrieves all PIDs associated with a given child ID across the cluster.
If the child is running on multiple nodes (due to replication), all PIDs will be returned.
## Example
iex> ProcessHub.get_pids(:my_hub, :my_child)
[#PID<0.123.0>]
"""
@spec get_pids(ProcessHub.hub_id(), ProcessHub.child_id()) :: [pid()]
defdelegate get_pids(hub_id, child_id), to: ProcessRegistry, as: :get_pids
@doc """
Returns the first available process ID (PID) for a specific child process.
This function provides a convenient way to quickly get a PID for a child process.
However, when using replication strategies, this function will only return the first
available PID, which may not be suitable for all use cases where you need to interact
with all replicas.
> #### Replication Warning {: .warning}
>
> When using replication strategies, consider using `get_pids/2` instead to get all PIDs.
## Example
iex> ProcessHub.get_pid(:my_hub, :my_child)
#PID<0.123.0>
"""
@spec get_pid(ProcessHub.hub_id(), ProcessHub.child_id()) :: pid() | nil
defdelegate get_pid(hub_id, child_id), to: ProcessRegistry, as: :get_pid
@doc """
Returns a list of all registered processes in the hub.
This function provides an efficient way to enumerate all child processes managed by the hub.
The returned list contains child IDs and, depending on the scope option, may include
node locations and process IDs.
This function performs a fast local query against the `ets` table and does not make any network calls.
## Available Options:
- `:global` - returns a list of all child processes across all nodes in the cluster.
The return result will be in the format of `[{child_id, [{:node, pid}]}]`.
- `:local` - returns only child processes that are running on the local node.
The return result will be in the format of `[{child_id, [pid]}]`.
## Example
iex> ProcessHub.process_list(:my_hub, :global)
[
{:my_child1, [{:node1, #PID<0.123.0>}, {:node2, #PID<2.129.0>}]},
{:my_child2, [{:node1, #PID<0.126.0>}, {:node2, #PID<2.124.0>}]}
]
iex> ProcessHub.process_list(:my_hub, :local)
[{:my_child1, #PID<0.123.0>}]
"""
@spec process_list(hub_id(), :global | :local) :: [
{ProcessHub.child_id(), [{node(), pid()}] | pid()}
]
defdelegate process_list(hub_id, scope), to: ProcessRegistry, as: :process_list
@doc """
Checks if a `ProcessHub` instance with the given `t:hub_id/0` is currently in a network-partitioned state.
A hub is considered partitioned when the configured `ProcessHub.Strategy.PartitionTolerance` strategy
has detected a network partition event. When a network partition is detected, the hub will
terminate the `ProcessHub.DistributedSupervisor` process along with all its managed child processes
to maintain data consistency and prevent split-brain scenarios.
## Example
iex> ProcessHub.is_partitioned?(:my_hub)
false
"""
@spec is_partitioned?(hub_id()) :: boolean()
def is_partitioned?(hub_id) do
GenServer.call(hub_id, :is_partitioned?)
end
@doc """
Returns whether the hub has pending worker queue operations.
This is informational and does not block event processing. It indicates
whether the coordinator has delegated work to the worker queue that
has not yet completed.
## Example
iex> ProcessHub.is_locked?(:my_hub)
false
"""
@spec is_locked?(hub_id()) :: boolean()
def is_locked?(hub_id) do
GenServer.call(hub_id, :is_locked?)
end
@doc """
Returns a list of all nodes where a `ProcessHub` instance with the given `t:hub_id/0` is currently running.
All nodes running the same `ProcessHub` instance (identified by the same `t:hub_id/0`) are considered
to be part of the same logical cluster and can coordinate process distribution and management.
## Options:
- `[:include_local]` - includes the local node in the returned list (default: excludes local node)
## Example
iex> ProcessHub.nodes(:my_hub, [:include_local])
[:"node1@127.0.0.1", :"node2@127.0.0.1"]
"""
@spec nodes(hub_id(), [:include_local] | nil) :: [node()]
def nodes(hub_id, opts \\ []) do
GenServer.call(hub_id, {:get_nodes, opts})
end
@doc """
Promotes a `ProcessHub` instance to run in distributed node mode.
This function should be used when the `ProcessHub` was initially started in
non-distributed mode (for example, during development or testing) and you want
to promote it to participate in a distributed cluster.
The promotion process will update all existing child process registrations
in the registry to reflect the new node name, ensuring proper cluster coordination.
## Parameters:
- `node_name` (optional) - specifies the node name to use. If not provided,
the current node name (`Node.self()`) will be used.
"""
@spec promote_to_node(hub_id()) :: :ok | {:error, :not_alive}
def promote_to_node(hub_id, node_name \\ node()) do
GenServer.call(hub_id, {:promote_to_node, node_name})
end
@doc """
Dynamically registers hook handlers for specific hub events.
Hook handlers allow you to execute custom logic when specific events occur
within the ProcessHub lifecycle, such as process startup, shutdown, node joining,
or cluster events. This provides a powerful extension mechanism for monitoring,
logging, or custom business logic.
## Parameters:
- `hub_id` - the hub to register handlers for
- `hook_key` - the specific event type to handle (use `ProcessHub.Constant.Hook` constants)
- `hook_handlers` - a list of `ProcessHub.Service.HookManager` structs defining the handlers
## Examples
iex> ProcessHub.register_hook_handlers(
iex> :my_hub,
iex> ProcessHub.Constant.Hook.pre_node_join(),
iex> [
iex> %ProcessHub.Service.HookManager{
iex> id: :my_hook_id,
iex> m: SomeModule,
iex> f: :some_function,
iex> a: [:my_first_argument]
iex> }
iex> ]
iex> )
[:ok]
"""
@spec register_hook_handlers(hub_id(), HookManager.hook_key(), [HookManager.t()]) ::
:ok | {:error, term()}
def register_hook_handlers(hub_id, hook_key, hook_handlers) do
results = GenServer.call(hub_id, {:register_hook_handlers, hook_key, hook_handlers})
case Enum.all?(results, &(&1 == :ok)) do
true -> :ok
false -> {:error, :failed_to_register_some_handlers}
end
end
@doc """
Dynamically removes previously registered hook handlers for specific hub events.
This function allows you to unregister hook handlers that were previously
registered using `register_hook_handlers/3`. This is useful for cleanup,
reconfiguration, or when certain handlers are no longer needed.
The function delegates to the coordinator process, which will call the
`ProcessHub.Service.HookManager.cancel_handler/3` function for each handler ID.
## Parameters:
- `hub_id` - the hub to remove handlers from
- `hook_key` - the specific event type (use `ProcessHub.Constant.Hook` constants)
- `handler_ids` - a list of handler IDs to remove
## Examples
iex> ProcessHub.cancel_hook_handlers(
iex> :my_hub,
iex> ProcessHub.Constant.Hook.pre_node_join(),
iex> [:my_hook_id, :another_hook_id]
iex> )
:ok
"""
@spec cancel_hook_handlers(hub_id(), HookManager.hook_key(), [HookManager.handler_id()]) ::
:ok | {:error, term()}
def cancel_hook_handlers(hub_id, hook_key, handler_ids) do
results = GenServer.call(hub_id, {:cancel_hook_handlers, hook_key, handler_ids})
case Enum.all?(results, &(&1 == :ok)) do
true -> :ok
false -> {:error, :failed_to_cancel_some_handlers}
end
end
end