Skip to main content

LifecycleGroup

Struct LifecycleGroup 

Source
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>

Source

pub async fn spawn<F>( replica_count: u8, group_seed: [u8; 32], factory: F, ) -> Result<Self, LifecycleGroupError>
where F: FnMut(u8) -> Arc<L>,

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).

Source

pub async fn spawn_with_placement<F>( replica_count: u8, group_seed: [u8; 32], requirements: CapabilityFilter, scheduler: &Scheduler, factory: F, ) -> Result<Self, LifecycleGroupError>
where F: FnMut(ReplicaContext) -> Arc<L>,

Spawn replica_count replicas with cross-node placement via Scheduler::place / GroupCoordinator::place_with_spread.

Differences from Self::spawn:

  • Caller supplies a Scheduler + a CapabilityFilter the scheduler uses to find candidate nodes for each replica.
  • Replicas are spread across distinct nodes (spread invariant) — failing if fewer than replica_count candidates match the filter.
  • The factory receives a ReplicaContext carrying 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.

Source

pub fn replica_count(&self) -> usize

Number of live replicas managed by the group.

Source

pub fn group_seed(&self) -> &[u8; 32]

The 32-byte seed used to derive per-replica identities.

Source

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.

Source

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.

Source

pub fn replicas(&self) -> Vec<Arc<L>>

All replicas in declaration order. Cheap O(n) Arc clones.

Source

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.

Source

pub fn placements(&self) -> &[PlacementDecision]

All recorded placement decisions in declaration order. Empty when the group was created via the placement-free Self::spawn.

Source

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.

Source

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:

  • InvalidConfig if index >= replica_count.
  • StartFailed { index, error } if the new handle’s on_start fails. The slot is left empty in this case — caller must retry or shrink the group.
Source

pub async fn add_replica<F>( &mut self, factory: F, ) -> Result<u8, LifecycleGroupError>
where F: FnOnce(u8) -> Arc<L>,

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:

  • InvalidConfig when replica_count == u8::MAX (group-size hard cap; the index field is u8).
  • StartFailed { index, error } when the new handle’s on_start fails. The factory’s Arc<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.

Source

pub async fn add_replicas<F>( &mut self, count: u8, factory: F, ) -> Result<(), LifecycleGroupError>
where F: FnMut(u8) -> Arc<L>,

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).

Source

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.

Source

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.

Source

pub async fn stop(self)

Stop every replica in declaration order and await the teardown. Consumes the group.

Source

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).

Auto Trait Implementations§

§

impl<L> !RefUnwindSafe for LifecycleGroup<L>

§

impl<L> !UnwindSafe for LifecycleGroup<L>

§

impl<L> Freeze for LifecycleGroup<L>

§

impl<L> Send for LifecycleGroup<L>

§

impl<L> Sync for LifecycleGroup<L>

§

impl<L> Unpin for LifecycleGroup<L>

§

impl<L> UnsafeUnpin for LifecycleGroup<L>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more