View Source ProcessHub.Service.DeclaredChildren (ProcessHub v0.8.0)

The declared list: a versioned, durable, leader-written list of the children that SHALL exist on a hub.

Experimental

The declared-children feature is experimental and may change in future releases. Use in production at your own discretion.

start_child/3 with durable: true adds the child's spec, with the start's caller metadata kept beside it so a replay that finds no registry row still starts the child with it; a deliberate stop removes it, and nothing else writes it — list absence is the stop record and never expires. Mutations serialize through the hub's leader (:elector, with the lowest hub member as deterministic fallback), which bumps one version per mutation; adoption replaces the whole list, higher version wins. The list commits before the process action, so the orphan reconcile heals a crashed command in the next round.

The list persists in its own DETS-backed store beside the registry, one row per child, is read from that store's in-memory side, and optionally ships to an off-cluster ProcessHub.Storage.RemoteManifest. A missing or corrupt list with durable evidence behind it parks the reconcile instead of opening empty; clear/1 is the operator override. See guides/Persistence.md for the full model.

Summary

Types

A command held back by its precommit; it answers its caller.

The declared list with its version lineage, as persisted and shipped.

A change to the declared list, applied by the leader.

A batch's changes not yet written: the version it started from, the version it reaches, and the specs it adds or replaces and the ids it removes.

A precommit's answer: run the command now, park it behind the batch's write, ask the remote leader (answering unreachable when it cannot be reached), or refuse it.

Functions

Adopts an incoming manifest when it wins: a higher version wholesale, a tie with differing content by lowest mutating node (WARN + tiebreak hook). MUST run inside the coordinator process.

Announces the local list version to hub peers; a lower-version peer pulls the full list. A no-op while the gate is off, parked, or nothing declared.

Applies a mutation as the leader; MUST run inside the coordinator process so writes serialize. A mutation that changes nothing does not bump the version.

Asks the leader of a {:remote, ...} precommit to apply its mutation and answers as the precommit: :ok once the leader has written it, or an error. Runs outside the coordinator, so no coordinator waits on a leader.

Resolves the list on coordinator boot; see ProcessHub.Service.DeclaredChildren.Boot.

Operator call: clears the hub's declared list. Destructive — nothing remains declared and the reconcile stops running declared children. Written above every known version so it wins adoption everywhere; lifts the park state.

Returns the declared spec of child_id, or nil when the hub is not running or does not declare it. Reads one row, whatever the list's size.

Returns the hub's declared children and the list version from local storage. A hub without the feature in use returns %{version: 0, children: []}.

Parks continuation behind the write of pending, the batch's changes. One :flush_declared is queued per batch (Batch.add/3) and lands behind every request already in the coordinator's mailbox, so concurrent durable commands share one write and sync — and none of them runs before the write that covers its entry. MUST run inside the coordinator process.

Starts elector participation. Elector is node-global; the strategy module is set only when unset so another user of it keeps its configuration.

Writes and syncs the batch's rows once, announces the version it reached (peers behind it pull the list, so the batch's intermediate versions need no announce of their own), then runs every parked command in arrival order and replies to its caller. A write that fails answers every parked caller with the error and runs none of them — no child starts on an intent that did not persist. MUST run inside the coordinator process; the coordinator flushes before it considers a peer's copy, so an adoption never overwrites a batch in flight.

The manifest wire/storage format this release reads and writes.

Resolves the hub's current leader: the elector leader when it is a hub member, otherwise the lexicographically lowest hub member — deterministic within a connected component, so exactly one node accepts writes.

Returns the stored manifest, or nil when none exists.

Handles a peer's version announce: when the local copy is older, fetches the peer's manifest in a supervised task and casts it back for adoption.

Builds a manifest at version with entries, mutated by this node.

Opens the list's durable store for the initializer; see Store.open/2.

Returns whether the hub's reconcile is parked over a lost declared list.

Commits the list additions a durable: true start requires, before any process starts. Refuses when the gate is off, the list is parked, a spec is :temporary, or no leader is reachable. :ok for non-durable starts.

Commits the list removals a stop requires, before any child terminates. The leader's copy is authoritative; with no leader reachable the stop is refused only when the local copy shows a declared child among child_ids.

Applies a re-fetched remote copy as boot would; MUST run inside the coordinator.

Answers a command held back by its precommit: runs it and replies with its answer once the precommit is :ok, or replies with the precommit's error without running it. MUST run inside the coordinator process.

Returns the full manifest, or nil when none exists.

Types

@type command() :: (ProcessHub.Coordinator.State.t() ->
                {:reply, term(), ProcessHub.Coordinator.State.t()})

A command held back by its precommit; it answers its caller.

@type manifest() :: %{
  format: pos_integer(),
  version: non_neg_integer(),
  mutated_by: node(),
  entries: %{required(ProcessHub.child_id()) => ProcessHub.child_spec()}
}

The declared list with its version lineage, as persisted and shipped.

@type mutation() ::
  {:add, [ProcessHub.child_spec()]} | {:remove, [ProcessHub.child_id()]}

A change to the declared list, applied by the leader.

@type pending() :: %{
  base: non_neg_integer(),
  version: non_neg_integer(),
  puts: %{required(ProcessHub.child_id()) => ProcessHub.child_spec()},
  removes: MapSet.t(ProcessHub.child_id())
}

A batch's changes not yet written: the version it started from, the version it reaches, and the specs it adds or replaces and the ids it removes.

@type precommit() ::
  :ok
  | {:pending, pending()}
  | {:remote, node(), mutation(), :ok | {:error, :no_leader}}
  | {:error, term()}

A precommit's answer: run the command now, park it behind the batch's write, ask the remote leader (answering unreachable when it cannot be reached), or refuse it.

Functions

@spec adopt(ProcessHub.Hub.t(), manifest()) :: :ok

Adopts an incoming manifest when it wins: a higher version wholesale, a tie with differing content by lowest mutating node (WARN + tiebreak hook). MUST run inside the coordinator process.

@spec announce_version(ProcessHub.Hub.t()) :: :ok

Announces the local list version to hub peers; a lower-version peer pulls the full list. A no-op while the gate is off, parked, or nothing declared.

Link to this function

apply_mutation(state, mutation)

View Source
@spec apply_mutation(ProcessHub.Coordinator.State.t(), mutation()) ::
  :ok | {:pending, pending()} | {:error, term()}

Applies a mutation as the leader; MUST run inside the coordinator process so writes serialize. A mutation that changes nothing does not bump the version.

A mutation that changes the list answers {:pending, pending}: the batch's changes so far, not yet written. The coordinator parks the command behind defer/4 and runs it from flush/1, after the one write and sync that persists the whole batch — so N commands cost one write of their own rows, and none of them rewrites the list.

@spec ask_leader(ProcessHub.hub_id(), precommit()) :: :ok | {:error, term()}

Asks the leader of a {:remote, ...} precommit to apply its mutation and answers as the precommit: :ok once the leader has written it, or an error. Runs outside the coordinator, so no coordinator waits on a leader.

@spec boot(ProcessHub.Hub.t()) ::
  {:ok, :ready | :parked | {:remote_error, term()}} | {:error, term()}

Resolves the list on coordinator boot; see ProcessHub.Service.DeclaredChildren.Boot.

@spec clear(ProcessHub.hub_id()) :: :ok | {:error, term()}

Operator call: clears the hub's declared list. Destructive — nothing remains declared and the reconcile stops running declared children. Written above every known version so it wins adoption everywhere; lifts the park state.

Link to this function

declared_child(hub_id, child_id)

View Source
@spec declared_child(ProcessHub.hub_id(), ProcessHub.child_id()) ::
  ProcessHub.child_spec() | nil

Returns the declared spec of child_id, or nil when the hub is not running or does not declare it. Reads one row, whatever the list's size.

Link to this function

declared_children(hub_id)

View Source
@spec declared_children(ProcessHub.hub_id()) :: %{
  version: non_neg_integer(),
  children: [ProcessHub.child_spec()]
}

Returns the hub's declared children and the list version from local storage. A hub without the feature in use returns %{version: 0, children: []}.

Link to this function

defer(state, pending, from, continuation)

View Source

Parks continuation behind the write of pending, the batch's changes. One :flush_declared is queued per batch (Batch.add/3) and lands behind every request already in the coordinator's mailbox, so concurrent durable commands share one write and sync — and none of them runs before the write that covers its entry. MUST run inside the coordinator process.

@spec ensure_election() :: :ok

Starts elector participation. Elector is node-global; the strategy module is set only when unset so another user of it keeps its configuration.

Elector comes along as a process_hub dependency, but it is left out when you build a release. Add {:elector, "~> 0.3.4"} to your own dependencies so the release includes it. Without it there is no election and leader/1 picks the first node by name instead, which is fine on a single node and still gives one leader across a cluster.

Writes and syncs the batch's rows once, announces the version it reached (peers behind it pull the list, so the batch's intermediate versions need no announce of their own), then runs every parked command in arrival order and replies to its caller. A write that fails answers every parked caller with the error and runs none of them — no child starts on an intent that did not persist. MUST run inside the coordinator process; the coordinator flushes before it considers a peer's copy, so an adoption never overwrites a batch in flight.

@spec format() :: pos_integer()

The manifest wire/storage format this release reads and writes.

@spec leader(ProcessHub.Hub.t()) :: node()

Resolves the hub's current leader: the elector leader when it is a hub member, otherwise the lexicographically lowest hub member — deterministic within a connected component, so exactly one node accepts writes.

@spec manifest(ProcessHub.Hub.t()) :: manifest() | nil

Returns the stored manifest, or nil when none exists.

Link to this function

maybe_pull(hub, from_node, version)

View Source
@spec maybe_pull(ProcessHub.Hub.t(), node(), non_neg_integer()) :: :ok

Handles a peer's version announce: when the local copy is older, fetches the peer's manifest in a supervised task and casts it back for adoption.

Link to this function

new_manifest(version, entries)

View Source
@spec new_manifest(non_neg_integer(), %{
  required(ProcessHub.child_id()) => ProcessHub.child_spec()
}) ::
  manifest()

Builds a manifest at version with entries, mutated by this node.

Link to this function

open_storage(hub_id, registry_backend)

View Source
@spec open_storage(ProcessHub.hub_id(), term()) :: %{
  declared_backend: {module(), term()},
  declared_path: String.t()
}

Opens the list's durable store for the initializer; see Store.open/2.

@spec parked?(ProcessHub.Hub.t()) :: boolean()

Returns whether the hub's reconcile is parked over a lost declared list.

Link to this function

precommit_start(state, child_specs, opts)

View Source
@spec precommit_start(
  ProcessHub.Coordinator.State.t(),
  [ProcessHub.child_spec()],
  keyword()
) ::
  precommit()

Commits the list additions a durable: true start requires, before any process starts. Refuses when the gate is off, the list is parked, a spec is :temporary, or no leader is reachable. :ok for non-durable starts.

Link to this function

precommit_stop(state, child_ids)

View Source

Commits the list removals a stop requires, before any child terminates. The leader's copy is authoritative; with no leader reachable the stop is refused only when the local copy shows a declared child among child_ids.

Fetches the remote copy; see ProcessHub.Service.DeclaredChildren.Boot.

Link to this function

remote_recompare(hub, fetched)

View Source
@spec remote_recompare(
  ProcessHub.Hub.t(),
  ProcessHub.Service.DeclaredChildren.Boot.fetched()
) ::
  :ok | {:error, term()}

Applies a re-fetched remote copy as boot would; MUST run inside the coordinator.

Link to this function

resume(state, error, from, command)

View Source

Answers a command held back by its precommit: runs it and replies with its answer once the precommit is :ok, or replies with the precommit's error without running it. MUST run inside the coordinator process.

@spec snapshot(ProcessHub.hub_id()) :: manifest() | nil

Returns the full manifest, or nil when none exists.