pub struct LifecycleGroup<L: LifecycleDaemon> { /* private fields */ }Expand description
N interchangeable replicas of a single LifecycleDaemon type
with a shared group_seed for deterministic identity
derivation.
L is the concrete daemon type — generic so callers retain
typed access to each replica’s state without dyn-erasure.
Implementations§
Source§impl<L: LifecycleDaemon> LifecycleGroup<L>
impl<L: LifecycleDaemon> LifecycleGroup<L>
Sourcepub async fn spawn<F>(
replica_count: u8,
group_seed: [u8; 32],
factory: F,
) -> Result<Self, LifecycleGroupError>
pub async fn spawn<F>( replica_count: u8, group_seed: [u8; 32], factory: F, ) -> Result<Self, LifecycleGroupError>
Spawn replica_count replicas of L. The factory is
called once per index 0..replica_count and must return
a fully-configured Arc<L> — the group wraps each in a
LifecycleHandle (which runs on_start synchronously).
Starts run concurrently via try_join_all. If any
on_start fails, every other in-flight start cancels and
the partially-started replicas drop their handles cleanly
via Drop (which schedules on_stop on a detached
task).
Sourcepub async fn spawn_with_placement<F>(
replica_count: u8,
group_seed: [u8; 32],
requirements: CapabilityFilter,
scheduler: &Scheduler,
factory: F,
) -> Result<Self, LifecycleGroupError>
pub async fn spawn_with_placement<F>( replica_count: u8, group_seed: [u8; 32], requirements: CapabilityFilter, scheduler: &Scheduler, factory: F, ) -> Result<Self, LifecycleGroupError>
Spawn replica_count replicas with cross-node placement
via Scheduler::place /
GroupCoordinator::place_with_spread.
Differences from Self::spawn:
- Caller supplies a
Scheduler+ aCapabilityFilterthe scheduler uses to find candidate nodes for each replica. - Replicas are spread across distinct nodes (spread
invariant) — failing if fewer than
replica_countcandidates match the filter. - The factory receives a
ReplicaContextcarrying the placement decision so daemons that bind to a specific node can read it.
Daemon construction happens after placement so a
factory can use ctx.placement.node_id to configure the
daemon for its target node. The placement decision is
recorded on the group for inspection.
§Note on single-process semantics
In a single-process deployment the scheduler may pick
the local node for every replica — place_with_spread
errors with PlacementFailed when fewer candidate nodes
than replicas match the filter. The group does not
actually move daemons across nodes; that is the
substrate’s remote-spawn responsibility, not the group
helper’s. Recording the placement decisions here lets a
future cross-node integration consume them without
re-deriving them.
Sourcepub fn replica_count(&self) -> usize
pub fn replica_count(&self) -> usize
Number of live replicas managed by the group.
Sourcepub fn group_seed(&self) -> &[u8; 32]
pub fn group_seed(&self) -> &[u8; 32]
The 32-byte seed used to derive per-replica identities.
Sourcepub fn replica_keypair(&self, index: u8) -> EntityKeypair
pub fn replica_keypair(&self, index: u8) -> EntityKeypair
Derive the deterministic per-replica keypair for index.
Same derivation
ReplicaGroup
uses for sync MeshDaemon replicas — so a future
cross-node lifecycle-daemon deployment can reuse this id.
Sourcepub fn replica(&self, index: usize) -> Option<Arc<L>>
pub fn replica(&self, index: usize) -> Option<Arc<L>>
Concrete, typed access to each replica’s daemon. Mirrors
replicas[index].clone() — preserves the underlying
L’s state surface so callers don’t have to downcast
from a trait object.
Sourcepub fn replicas(&self) -> Vec<Arc<L>>
pub fn replicas(&self) -> Vec<Arc<L>>
All replicas in declaration order. Cheap O(n) Arc clones.
Sourcepub fn placement(&self, index: usize) -> Option<&PlacementDecision>
pub fn placement(&self, index: usize) -> Option<&PlacementDecision>
Placement decision recorded for index, or None when
the group was created via the placement-free
Self::spawn.
Sourcepub fn placements(&self) -> &[PlacementDecision]
pub fn placements(&self) -> &[PlacementDecision]
All recorded placement decisions in declaration order.
Empty when the group was created via the placement-free
Self::spawn.
Sourcepub async fn health(&self) -> Vec<ReplicaHealth>
pub async fn health(&self) -> Vec<ReplicaHealth>
Per-replica health snapshot in declaration order.
Polls each replica’s
LifecycleDaemon::health in parallel via
join_all — the typical impl is cheap (atomic load or
short-RwLock read), so the parallelism is mostly future-
proofing for impls that need to await a lock.
Sourcepub async fn replace(
&mut self,
index: usize,
new_daemon: Arc<L>,
) -> Result<Arc<L>, LifecycleGroupError>
pub async fn replace( &mut self, index: usize, new_daemon: Arc<L>, ) -> Result<Arc<L>, LifecycleGroupError>
Replace the daemon at index with new_daemon. The old
handle is stopped + awaited before the new one is
installed, so the slot is briefly empty during the
transition. Returns the stopped handle’s underlying
daemon Arc — callers wanting to inspect the old state
(e.g. for forensics on what caused the unhealthy flip)
can hold onto it.
Errors:
InvalidConfigifindex >= replica_count.StartFailed { index, error }if the new handle’son_startfails. The slot is left empty in this case — caller must retry or shrink the group.
Sourcepub async fn add_replica<F>(
&mut self,
factory: F,
) -> Result<u8, LifecycleGroupError>
pub async fn add_replica<F>( &mut self, factory: F, ) -> Result<u8, LifecycleGroupError>
Append one replica to the group, growing it in place. The
factory receives the new replica’s index (= current
replica_count). Existing replicas keep their identities
and their handles — neither stops nor restarts. This is the
scale-up primitive for
crate::adapter::net::behavior::aggregator::AggregatorRegistry::scale_group
and the Scale RPC.
Errors:
InvalidConfigwhenreplica_count == u8::MAX(group-size hard cap; the index field isu8).StartFailed { index, error }when the new handle’son_startfails. The factory’sArc<L>is dropped before the error returns, so no zombie replica leaks.
§Placement
add_replica does not engage the scheduler. A group
originally created via Self::spawn_with_placement still
has its placement Vec — the new replica gets no placement
entry and runs on the local node. Operators who need
placement-aware scale-up wait for a future
add_replica_with_placement sibling; the single-process /
single-host deployment shipping today doesn’t engage that
surface.
Sourcepub async fn add_replicas<F>(
&mut self,
count: u8,
factory: F,
) -> Result<(), LifecycleGroupError>
pub async fn add_replicas<F>( &mut self, count: u8, factory: F, ) -> Result<(), LifecycleGroupError>
Bulk version of Self::add_replica. Constructs count
new daemons via the factory, then runs their on_start
handlers concurrently via try_join_all (same shape
as the initial-spawn path in start_replicas). If any
on_start fails, every successfully-started replica’s
handle is dropped — its LifecycleHandle::Drop schedules
on_stop on a detached task, so partial-start cleanup is
automatic. The group itself stays at its pre-call size on
error.
Used by super::super::aggregator::AggregatorRegistry::scale_group
so a 1→N grow doesn’t serialize N on_starts under the
entry mutex (which would block List / health /
HealthMonitor for the duration).
Sourcepub async fn remove_last(&mut self) -> Result<Arc<L>, LifecycleGroupError>
pub async fn remove_last(&mut self) -> Result<Arc<L>, LifecycleGroupError>
Stop and pop the last replica. Returns the stopped replica’s Arc so callers can inspect post-stop state (e.g. for forensic logging). The other replicas’ handles are untouched — neither stopped nor signalled — preserving their identity, generation counters, and any in-memory state.
Refuses to drop below one replica: callers that want to
dismantle the whole group should call Self::stop
instead. Returning an error rather than completing as a
no-op surfaces the typo at the caller (e.g. operator who
meant --replica-count 1 and wrote --replica-count 0).
If the group was created via
Self::spawn_with_placement, the last placement entry
is also popped so the parallel-Vec invariant
(placements.len() == replicas.len() when populated) is
preserved.
Sourcepub fn handles(&self) -> &[LifecycleHandle]
pub fn handles(&self) -> &[LifecycleHandle]
Borrow the underlying lifecycle handles. Operator
tooling that wants type-erased access (e.g. iterating
daemon().name() across heterogeneous groups in a
future registry) reaches in here.
Sourcepub async fn stop(self)
pub async fn stop(self)
Stop every replica in declaration order and await the teardown. Consumes the group.
Sourcepub fn into_parts(
self,
) -> (Vec<Arc<L>>, Vec<PlacementDecision>, Vec<LifecycleHandle>, [u8; 32])
pub fn into_parts( self, ) -> (Vec<Arc<L>>, Vec<PlacementDecision>, Vec<LifecycleHandle>, [u8; 32])
Surrender the group’s parts to the caller. Used by
process-level registries (e.g.
AggregatorRegistry::register) that take ownership of
the handles for shutdown but still want concrete-typed
access to the replicas + placement records.
Returns (replicas, placements, handles, group_seed) in
declaration order. After this call the group no longer
exists; lifecycle shutdown becomes the caller’s
responsibility (via the returned LifecycleHandles).