use std::collections::{HashMap, HashSet}; use std::fmt; use lazaret_core::identity::{ConfigurationFingerprint, SessionId}; use lazaret_core::records::{DurableDecision, Generation, Session, SessionPhase}; use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LifecycleAction { StartNewDomain, StartReadySession, WaitForDomain, RestartWaitingDomain, CreateTrustAnchor, StartRuntimeSupervisor, LeasePreparedLower, MountStateAnchor, StartSidecar, StartConnectivityHelper, MarkNetworkReady, ArmRuntimeCausalEdge, StartSandbox, PublishDomain, AdvanceWaitingSession, RejectConfigurationMismatch, RegisterSession, DisconnectSession, BeginTeardown, StopSandbox, StopSidecar, UnmountStateAnchor, ReleasePreparedLowerLease, GracefulSupervisorExit, FinishTeardown, } pub trait LifecycleAdapter { type Error; /// # Errors /// Returns the adapter's physical or durable action failure. fn execute( &mut self, action: LifecycleAction, generation: &Generation, ) -> Result<(), Self::Error>; /// Completes a physical boundary that must follow its durable lifecycle commit. /// /// # Errors /// Returns the adapter's post-commit publication or cleanup failure. fn after_commit( &mut self, _action: LifecycleAction, _generation: &Generation, ) -> Result<(), Self::Error> { Ok(()) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SessionAdmissionIdentity { pub generation_id: lazaret_core::identity::GenerationId, pub session_id: SessionId, pub configuration_fingerprint: ConfigurationFingerprint, } pub trait SessionLifecycleAdapter { type Error; type Admission; fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity; /// # Errors /// Returns credential, identity, duplicate-session, or durable registration failure. fn register_session( &self, store: &HostCoordinatorDomainStore, entry_action: LifecycleAction, admission: Self::Admission, ) -> Result; /// # Errors /// Returns exact-session or durable disconnect failure. fn disconnect_session( &self, store: &HostCoordinatorDomainStore, session: &Session, ) -> Result<(), Self::Error>; } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DomainPhase { Absent, Starting, Ready, Stopping, Failed, } #[derive(Debug)] pub enum LifecycleError { Adapter(E), Store(DomainStoreError), InvalidTransition { action: LifecycleAction, reason: &'static str, }, } impl fmt::Display for LifecycleError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Adapter(error) => write!(formatter, "lifecycle adapter failed: {error}"), Self::Store(error) => { write!(formatter, "Domain Store lifecycle update failed: {error}") } Self::InvalidTransition { action, reason } => { write!(formatter, "{action:?} refused: {reason}") } } } } impl std::error::Error for LifecycleError {} impl From for LifecycleError { fn from(error: DomainStoreError) -> Self { Self::Store(error) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] enum RuntimeMilestone { TrustAnchor, RuntimeSupervisor, PreparedLowerLease, StateAnchor, Sidecar, ConnectivityHelper, NetworkReady, CausalEdge, Sandbox, } #[derive(Clone, Debug, Default, Eq, PartialEq)] struct RuntimeProgress { completed: HashSet, } impl RuntimeProgress { fn has(&self, milestone: RuntimeMilestone) -> bool { self.completed.contains(&milestone) } fn mark(&mut self, milestone: RuntimeMilestone) { self.completed.insert(milestone); } fn clear(&mut self, milestone: RuntimeMilestone) { self.completed.remove(&milestone); } } pub struct HostCoordinatorLifecycle<'store, A, S> { store: &'store HostCoordinatorDomainStore, adapter: A, session_adapter: S, phase: DomainPhase, generation: Option, bootstrap_owner: Option, progress: RuntimeProgress, published: bool, sessions: HashMap, connected: HashSet, registered: HashSet, } impl<'store, A, S> HostCoordinatorLifecycle<'store, A, S> where A: LifecycleAdapter, S: SessionLifecycleAdapter, { #[must_use] pub fn new(store: &'store HostCoordinatorDomainStore, adapter: A, session_adapter: S) -> Self { Self { store, adapter, session_adapter, phase: DomainPhase::Absent, generation: None, bootstrap_owner: None, progress: RuntimeProgress::default(), published: false, sessions: HashMap::new(), connected: HashSet::new(), registered: HashSet::new(), } } #[must_use] pub const fn phase(&self) -> DomainPhase { self.phase } #[must_use] pub const fn is_published(&self) -> bool { self.published } #[must_use] pub fn session_phase(&self, session_id: SessionId) -> Option { self.sessions .get(&session_id) .map(|session| session.session_phase) } #[must_use] pub fn registered_sessions(&self) -> usize { self.registered.len() } #[must_use] pub const fn adapter(&self) -> &A { &self.adapter } #[must_use] pub const fn adapter_mut(&mut self) -> &mut A { &mut self.adapter } /// # Errors /// Refuses a non-absent Domain, invalid initial records, or adapter/store failure. pub fn start_new_domain( &mut self, generation: Generation, mut session: Session, ) -> Result<(), LifecycleError> { Self::require( LifecycleAction::StartNewDomain, self.phase == DomainPhase::Absent, "Domain is not absent", )?; Self::require( LifecycleAction::StartNewDomain, generation.durable_decision == DurableDecision::None, "new generation decision is not None", )?; Self::require( LifecycleAction::StartNewDomain, session.generation_id == generation.generation_id, "session generation differs", )?; Self::require( LifecycleAction::StartNewDomain, session.session_phase == SessionPhase::New, "session is not New", )?; self.require_new_generation(LifecycleAction::StartNewDomain, &generation, &session)?; self.execute(LifecycleAction::StartNewDomain, &generation)?; session.session_phase = SessionPhase::Waiting; self.store.apply(&[ DomainStoreMutation::PutGeneration(generation.clone()), DomainStoreMutation::PutSession(session.clone()), ])?; self.phase = DomainPhase::Starting; self.bootstrap_owner = Some(session.session_id); self.sessions.insert(session.session_id, session); self.generation = Some(generation); self.progress = RuntimeProgress::default(); Ok(()) } /// Drives one cold-start leader through the complete normal publication boundary. /// /// # Errors /// Returns the first invalid transition, durable update, or lifecycle adapter failure. pub fn bootstrap_new_domain( &mut self, generation: Generation, session: Session, ) -> Result<(), LifecycleError> { self.bootstrap_new_domain_while_owner_live(generation, session, || true) .map(|_| ()) } /// Drives cold start while checking the bootstrap owner's exact lifetime authority. /// /// Returns `false` after applying the modeled bootstrap-owner disconnect transition. /// /// # Errors /// Returns the first invalid transition, durable update, or lifecycle adapter failure. pub fn bootstrap_new_domain_while_owner_live( &mut self, generation: Generation, session: Session, owner_live: impl FnMut() -> bool, ) -> Result> { self.bootstrap_new_domain_while_owner_live_and_service( generation, session, owner_live, |_| Ok(()), |_| Ok(()), ) } /// Drives cold start while servicing bounded pre-publication waiters between actions. /// /// # Errors /// Returns the first invalid transition, durable update, adapter, or waiter-service failure. pub fn bootstrap_new_domain_while_owner_live_and_service( &mut self, generation: Generation, session: Session, mut owner_live: impl FnMut() -> bool, mut service_waiters: impl FnMut(&mut Self) -> Result<(), A::Error>, mut withdraw_waiter_admission: impl FnMut(&mut Self) -> Result<(), A::Error>, ) -> Result> { let session_id = session.session_id; self.start_new_domain(generation, session)?; macro_rules! checkpoint { () => { service_waiters(self).map_err(LifecycleError::Adapter)?; if !owner_live() { self.disconnect_session(session_id)?; return Ok(false); } }; } checkpoint!(); self.create_trust_anchor()?; checkpoint!(); self.start_runtime_supervisor()?; checkpoint!(); self.lease_prepared_lower()?; checkpoint!(); self.mount_state_anchor()?; checkpoint!(); self.start_sidecar()?; checkpoint!(); self.start_connectivity_helper()?; checkpoint!(); self.mark_network_ready()?; checkpoint!(); self.arm_runtime_causal_edge()?; checkpoint!(); self.start_sandbox()?; checkpoint!(); withdraw_waiter_admission(self).map_err(LifecycleError::Adapter)?; if !owner_live() { self.disconnect_session(session_id)?; return Ok(false); } self.publish_domain()?; Ok(true) } /// # Errors /// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session. pub fn admit_session( &mut self, admission: S::Admission, ) -> Result<(), LifecycleError> { let identity = self.session_adapter.admission_identity(&admission); let entry_action = match self.sessions.get(&identity.session_id) { None => LifecycleAction::StartReadySession, Some(session) if session.session_phase == SessionPhase::Waiting => { LifecycleAction::AdvanceWaitingSession } Some(_) => { return Err(LifecycleError::InvalidTransition { action: LifecycleAction::RegisterSession, reason: "session is not New or Waiting", }); } }; let generation = self.current_generation(entry_action)?; Self::require( entry_action, self.admission_ready() && identity.generation_id == generation.generation_id && identity.configuration_fingerprint == generation.configuration_fingerprint, "Domain is not published for the admission generation", )?; let attached = self .session_adapter .register_session(self.store, entry_action, admission) .map_err(LifecycleError::Adapter)?; Self::require( LifecycleAction::RegisterSession, attached.generation_id == identity.generation_id && attached.session_id == identity.session_id && attached.session_phase == SessionPhase::Attached, "session adapter returned mismatched registration evidence", )?; self.connected.insert(attached.session_id); self.registered.insert(attached.session_id); self.sessions.insert(attached.session_id, attached); Ok(()) } /// # Errors /// Refuses a session unless the Domain is starting or stopping. pub fn wait_for_domain(&mut self, session: Session) -> Result<(), LifecycleError> { Self::require( LifecycleAction::WaitForDomain, matches!(self.phase, DomainPhase::Starting | DomainPhase::Stopping), "Domain is neither starting nor stopping", )?; Self::require( LifecycleAction::WaitForDomain, session.session_phase == SessionPhase::New, "session is not New", )?; Self::require( LifecycleAction::WaitForDomain, session.generation_id == self .current_generation(LifecycleAction::WaitForDomain)? .generation_id && !self.sessions.contains_key(&session.session_id), "session generation differs or Session ID is already known", )?; self.execute_current(LifecycleAction::WaitForDomain)?; self.put_new_session( LifecycleAction::WaitForDomain, session, SessionPhase::Waiting, false, ) } /// # Errors /// Refuses a restart unless the Domain is absent and the session is waiting. pub fn restart_waiting_domain( &mut self, generation: Generation, session_id: SessionId, ) -> Result<(), LifecycleError> { Self::require( LifecycleAction::RestartWaitingDomain, self.phase == DomainPhase::Absent, "Domain is not absent", )?; Self::require( LifecycleAction::RestartWaitingDomain, generation.durable_decision == DurableDecision::None, "new generation decision is not None", )?; let session = self .session(session_id, LifecycleAction::RestartWaitingDomain)? .clone(); Self::require( LifecycleAction::RestartWaitingDomain, session.session_phase == SessionPhase::Waiting, "session is not Waiting", )?; let successor_session = Session { generation_id: generation.generation_id, ..session.clone() }; self.require_new_generation( LifecycleAction::RestartWaitingDomain, &generation, &successor_session, )?; self.execute(LifecycleAction::RestartWaitingDomain, &generation)?; let waiters = self .sessions .values() .filter(|candidate| candidate.session_phase == SessionPhase::Waiting) .cloned() .collect::>(); let mut mutations = Vec::with_capacity(waiters.len() * 2 + 1); for waiter in &waiters { mutations.push(DomainStoreMutation::PutSession(Session { session_phase: SessionPhase::Closed, ..waiter.clone() })); } mutations.push(DomainStoreMutation::PutGeneration(generation.clone())); for waiter in &waiters { mutations.push(DomainStoreMutation::PutSession(Session { generation_id: generation.generation_id, ..waiter.clone() })); } self.store.apply(&mutations)?; self.phase = DomainPhase::Starting; self.bootstrap_owner = Some(session_id); let generation_id = generation.generation_id; self.generation = Some(generation); self.progress = RuntimeProgress::default(); for mut waiter in waiters { waiter.generation_id = generation_id; self.sessions.insert(waiter.session_id, waiter); } Ok(()) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn create_trust_anchor(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::CreateTrustAnchor, !self.progress.has(RuntimeMilestone::TrustAnchor), |p| p.mark(RuntimeMilestone::TrustAnchor), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn start_runtime_supervisor(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::StartRuntimeSupervisor, self.progress.has(RuntimeMilestone::TrustAnchor) && !self.progress.has(RuntimeMilestone::RuntimeSupervisor), |p| p.mark(RuntimeMilestone::RuntimeSupervisor), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn lease_prepared_lower(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::LeasePreparedLower, self.progress.has(RuntimeMilestone::RuntimeSupervisor) && !self.progress.has(RuntimeMilestone::PreparedLowerLease), |p| p.mark(RuntimeMilestone::PreparedLowerLease), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn mount_state_anchor(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::MountStateAnchor, self.progress.has(RuntimeMilestone::PreparedLowerLease) && !self.progress.has(RuntimeMilestone::StateAnchor), |p| p.mark(RuntimeMilestone::StateAnchor), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn start_sidecar(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::StartSidecar, self.progress.has(RuntimeMilestone::StateAnchor) && !self.progress.has(RuntimeMilestone::Sidecar), |p| p.mark(RuntimeMilestone::Sidecar), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn start_connectivity_helper(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::StartConnectivityHelper, self.progress.has(RuntimeMilestone::Sidecar) && !self.progress.has(RuntimeMilestone::ConnectivityHelper), |p| p.mark(RuntimeMilestone::ConnectivityHelper), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn mark_network_ready(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::MarkNetworkReady, self.progress.has(RuntimeMilestone::ConnectivityHelper) && !self.progress.has(RuntimeMilestone::NetworkReady), |p| p.mark(RuntimeMilestone::NetworkReady), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn arm_runtime_causal_edge(&mut self) -> Result<(), LifecycleError> { self.startup_action( LifecycleAction::ArmRuntimeCausalEdge, self.progress.has(RuntimeMilestone::NetworkReady) && !self.progress.has(RuntimeMilestone::CausalEdge), |p| p.mark(RuntimeMilestone::CausalEdge), ) } /// # Errors /// Refuses launch before network readiness and the causal edge are established. pub fn start_sandbox(&mut self) -> Result<(), LifecycleError> { let allowed = self.progress.has(RuntimeMilestone::CausalEdge) && self.progress.has(RuntimeMilestone::NetworkReady) && !self.progress.has(RuntimeMilestone::Sandbox); Self::require( LifecycleAction::StartSandbox, self.phase == DomainPhase::Starting && allowed, "runtime prerequisites are incomplete", )?; let mut generation = self .current_generation(LifecycleAction::StartSandbox)? .clone(); generation.durable_decision = DurableDecision::Preparing; self.store .apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?; self.generation = Some(generation.clone()); self.execute(LifecycleAction::StartSandbox, &generation)?; self.progress.mark(RuntimeMilestone::Sandbox); Ok(()) } /// # Errors /// Refuses publication until every ordered startup prerequisite is complete. pub fn publish_domain(&mut self) -> Result<(), LifecycleError> { let allowed = self.phase == DomainPhase::Starting && self.progress.has(RuntimeMilestone::TrustAnchor) && self.progress.has(RuntimeMilestone::RuntimeSupervisor) && self.progress.has(RuntimeMilestone::PreparedLowerLease) && self.progress.has(RuntimeMilestone::StateAnchor) && self.progress.has(RuntimeMilestone::Sidecar) && self.progress.has(RuntimeMilestone::ConnectivityHelper) && self.progress.has(RuntimeMilestone::NetworkReady) && self.progress.has(RuntimeMilestone::CausalEdge) && self.progress.has(RuntimeMilestone::Sandbox); Self::require( LifecycleAction::PublishDomain, allowed, "publication prerequisites are incomplete", )?; self.execute_current(LifecycleAction::PublishDomain)?; let mut generation = self .current_generation(LifecycleAction::PublishDomain)? .clone(); generation.durable_decision = DurableDecision::Published; self.store .apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?; self.generation = Some(generation.clone()); if let Err(error) = self .adapter .after_commit(LifecycleAction::PublishDomain, &generation) { self.phase = DomainPhase::Failed; self.bootstrap_owner = None; self.published = false; return Err(LifecycleError::Adapter(error)); } self.phase = DomainPhase::Ready; self.bootstrap_owner = None; self.published = true; Ok(()) } /// # Errors /// Refuses unless a new or waiting session differs from the ready generation. pub fn reject_new_configuration_mismatch( &mut self, mut session: Session, fingerprint: ConfigurationFingerprint, ) -> Result<(), LifecycleError> { Self::require( LifecycleAction::RejectConfigurationMismatch, self.phase == DomainPhase::Ready && session.session_phase == SessionPhase::New, "session is not eligible for mismatch refusal", )?; let generation = self.current_generation(LifecycleAction::RejectConfigurationMismatch)?; Self::require( LifecycleAction::RejectConfigurationMismatch, session.generation_id == generation.generation_id && fingerprint != generation.configuration_fingerprint, "configuration does not differ from the current generation", )?; Self::require( LifecycleAction::RejectConfigurationMismatch, !self.sessions.contains_key(&session.session_id), "Session ID is already known", )?; self.execute_current(LifecycleAction::RejectConfigurationMismatch)?; let new = session.clone(); session.session_phase = SessionPhase::Failed; self.store.apply(&[ DomainStoreMutation::PutSession(new), DomainStoreMutation::PutSession(session.clone()), ])?; self.sessions.insert(session.session_id, session); Ok(()) } /// # Errors /// Refuses unless a new or waiting session differs from the ready generation. pub fn reject_configuration_mismatch( &mut self, session_id: SessionId, fingerprint: ConfigurationFingerprint, ) -> Result<(), LifecycleError> { let mut session = self .session(session_id, LifecycleAction::RejectConfigurationMismatch)? .clone(); Self::require( LifecycleAction::RejectConfigurationMismatch, self.phase == DomainPhase::Ready && matches!( session.session_phase, SessionPhase::New | SessionPhase::Waiting ), "session is not eligible for mismatch refusal", )?; let generation = self.current_generation(LifecycleAction::RejectConfigurationMismatch)?; Self::require( LifecycleAction::RejectConfigurationMismatch, fingerprint != generation.configuration_fingerprint, "configuration matches", )?; self.execute_current(LifecycleAction::RejectConfigurationMismatch)?; session.session_phase = SessionPhase::Failed; self.store .apply(&[DomainStoreMutation::PutSession(session.clone())])?; self.connected.remove(&session_id); self.registered.remove(&session_id); self.sessions.insert(session_id, session); Ok(()) } /// # Errors /// Refuses an absent or terminal session, or an adapter/store failure. pub fn disconnect_session( &mut self, session_id: SessionId, ) -> Result<(), LifecycleError> { let mut session = self .session(session_id, LifecycleAction::DisconnectSession)? .clone(); Self::require( LifecycleAction::DisconnectSession, matches!( session.session_phase, SessionPhase::Waiting | SessionPhase::Registering | SessionPhase::Attached | SessionPhase::Prompting | SessionPhase::HostExecuting ), "session is not live", )?; let leader_lost = self.phase == DomainPhase::Starting && self.bootstrap_owner == Some(session_id); self.session_adapter .disconnect_session(self.store, &session) .map_err(LifecycleError::Adapter)?; session.session_phase = SessionPhase::Closed; self.connected.remove(&session_id); self.registered.remove(&session_id); self.sessions.insert(session_id, session); if leader_lost { self.phase = DomainPhase::Failed; self.bootstrap_owner = None; self.published = false; } Ok(()) } #[must_use] pub fn is_teardown_ready(&self) -> bool { self.phase == DomainPhase::Ready && self.registered.is_empty() && self.connected.is_empty() && self.sessions.values().all(|session| { !matches!( session.session_phase, SessionPhase::Waiting | SessionPhase::Registering | SessionPhase::Attached | SessionPhase::Prompting | SessionPhase::HostExecuting ) }) } /// Performs the complete ordered normal teardown when the final session has disconnected. /// /// # Errors /// Returns the first physical or durable teardown failure. pub fn teardown_if_idle(&mut self) -> Result> { if !self.is_teardown_ready() { return Ok(false); } self.begin_teardown()?; self.stop_sandbox()?; self.stop_sidecar()?; self.unmount_state_anchor()?; self.release_prepared_lower_lease()?; self.graceful_supervisor_exit()?; self.finish_teardown()?; Ok(true) } /// Transfers a published cold-start owner's lifetime edge to exact control admission. /// /// # Errors /// Refuses an unpublished Domain, unknown owner, or durable disconnect/teardown failure. pub fn reconcile_published_bootstrap_owner( &mut self, session_id: SessionId, bootstrap_owner_live: bool, ) -> Result<(), LifecycleError> { Self::require( LifecycleAction::DisconnectSession, self.phase == DomainPhase::Ready && self.published, "Domain is not published", )?; let phase = self .session(session_id, LifecycleAction::DisconnectSession)? .session_phase; if phase == SessionPhase::Waiting && !bootstrap_owner_live { self.disconnect_session(session_id)?; self.teardown_if_idle()?; } Ok(()) } /// # Errors /// Refuses while any session retains Domain interest. pub fn begin_teardown(&mut self) -> Result<(), LifecycleError> { Self::require( LifecycleAction::BeginTeardown, self.is_teardown_ready(), "sessions still have interest", )?; self.execute_current(LifecycleAction::BeginTeardown)?; self.phase = DomainPhase::Stopping; self.published = false; Ok(()) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn stop_sandbox(&mut self) -> Result<(), LifecycleError> { self.teardown_action( LifecycleAction::StopSandbox, self.progress.has(RuntimeMilestone::Sandbox), |p| { p.clear(RuntimeMilestone::Sandbox); p.clear(RuntimeMilestone::CausalEdge); }, ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn stop_sidecar(&mut self) -> Result<(), LifecycleError> { self.teardown_action( LifecycleAction::StopSidecar, !self.progress.has(RuntimeMilestone::Sandbox) && self.progress.has(RuntimeMilestone::Sidecar), |p| { p.clear(RuntimeMilestone::Sidecar); p.clear(RuntimeMilestone::ConnectivityHelper); p.clear(RuntimeMilestone::NetworkReady); }, ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn unmount_state_anchor(&mut self) -> Result<(), LifecycleError> { self.teardown_action( LifecycleAction::UnmountStateAnchor, !self.progress.has(RuntimeMilestone::Sidecar) && self.progress.has(RuntimeMilestone::StateAnchor), |p| p.clear(RuntimeMilestone::StateAnchor), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn release_prepared_lower_lease(&mut self) -> Result<(), LifecycleError> { self.teardown_action( LifecycleAction::ReleasePreparedLowerLease, !self.progress.has(RuntimeMilestone::StateAnchor) && self.progress.has(RuntimeMilestone::PreparedLowerLease), |p| p.clear(RuntimeMilestone::PreparedLowerLease), ) } /// # Errors /// Refuses an out-of-order action or adapter failure. pub fn graceful_supervisor_exit(&mut self) -> Result<(), LifecycleError> { self.teardown_action( LifecycleAction::GracefulSupervisorExit, !self.progress.has(RuntimeMilestone::PreparedLowerLease) && self.progress.has(RuntimeMilestone::RuntimeSupervisor), |p| p.clear(RuntimeMilestone::RuntimeSupervisor), ) } /// # Errors /// Refuses while runtime resources remain or the final durable update fails. pub fn finish_teardown(&mut self) -> Result<(), LifecycleError> { let allowed = self.phase == DomainPhase::Stopping && !self.progress.has(RuntimeMilestone::RuntimeSupervisor) && !self.progress.has(RuntimeMilestone::PreparedLowerLease) && !self.progress.has(RuntimeMilestone::StateAnchor) && !self.progress.has(RuntimeMilestone::Sidecar) && !self.progress.has(RuntimeMilestone::Sandbox); Self::require( LifecycleAction::FinishTeardown, allowed, "runtime resources remain", )?; self.execute_current(LifecycleAction::FinishTeardown)?; let mut generation = self .current_generation(LifecycleAction::FinishTeardown)? .clone(); generation.durable_decision = DurableDecision::Clean; self.store .apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?; self.generation = Some(generation); self.phase = DomainPhase::Absent; self.progress = RuntimeProgress::default(); self.bootstrap_owner = None; Ok(()) } pub fn into_adapters(self) -> (A, S) { (self.adapter, self.session_adapter) } fn startup_action( &mut self, action: LifecycleAction, allowed: bool, update: impl FnOnce(&mut RuntimeProgress), ) -> Result<(), LifecycleError> { Self::require( action, self.phase == DomainPhase::Starting && allowed, "startup ordering refused", )?; self.execute_current(action)?; update(&mut self.progress); Ok(()) } fn teardown_action( &mut self, action: LifecycleAction, allowed: bool, update: impl FnOnce(&mut RuntimeProgress), ) -> Result<(), LifecycleError> { Self::require( action, self.phase == DomainPhase::Stopping && allowed, "teardown ordering refused", )?; self.execute_current(action)?; update(&mut self.progress); Ok(()) } fn put_new_session( &mut self, action: LifecycleAction, mut session: Session, phase: SessionPhase, connected: bool, ) -> Result<(), LifecycleError> { Self::require( action, session.session_phase == SessionPhase::New, "session is not New", )?; Self::require( action, !self.sessions.contains_key(&session.session_id), "Session ID is already known", )?; session.session_phase = phase; self.store .apply(&[DomainStoreMutation::PutSession(session.clone())])?; if connected { self.connected.insert(session.session_id); } self.sessions.insert(session.session_id, session); Ok(()) } fn require_new_generation( &self, action: LifecycleAction, generation: &Generation, session: &Session, ) -> Result<(), LifecycleError> { let snapshot = self.store.snapshot()?; let latest = snapshot .generations .iter() .map(|candidate| candidate.generation_id) .max(); Self::require( action, generation.domain_id == snapshot.domain.domain_id && session.generation_id == generation.generation_id && generation.coordinator_id.as_bytes() != generation.network_sidecar_id.as_bytes() && generation.coordinator_id.as_bytes() != generation.sandbox_id.as_bytes() && generation.network_sidecar_id.as_bytes() != generation.sandbox_id.as_bytes() && latest.is_none_or(|current| generation.generation_id > current), "generation is not a fresh successor for this Domain", ) } fn admission_ready(&self) -> bool { self.phase == DomainPhase::Ready && self.published } fn current_generation( &self, action: LifecycleAction, ) -> Result<&Generation, LifecycleError> { self.generation .as_ref() .ok_or(LifecycleError::InvalidTransition { action, reason: "generation is absent", }) } fn session( &self, session_id: SessionId, action: LifecycleAction, ) -> Result<&Session, LifecycleError> { self.sessions .get(&session_id) .ok_or(LifecycleError::InvalidTransition { action, reason: "session is absent", }) } fn execute_current(&mut self, action: LifecycleAction) -> Result<(), LifecycleError> { let generation = self.current_generation(action)?.clone(); self.execute(action, &generation) } fn execute( &mut self, action: LifecycleAction, generation: &Generation, ) -> Result<(), LifecycleError> { self.adapter .execute(action, generation) .map_err(LifecycleError::Adapter) } fn require( action: LifecycleAction, condition: bool, reason: &'static str, ) -> Result<(), LifecycleError> { if condition { Ok(()) } else { Err(LifecycleError::InvalidTransition { action, reason }) } } }