//! Typestate machine for worker lifecycle. use crate::{ fault::Fault, types::{Generation, InvariantViolation}, }; use serde::{Deserialize, Serialize}; /// A worker in cold state (no process). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Cold; /// A worker in startup handshake. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Starting; /// A healthy worker ready to serve requests. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Ready; /// A worker currently recovering from failure. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Recovering { last_fault: Fault, } impl Recovering { /// Constructs recovering state from the most recent fault. #[must_use] pub fn new(last_fault: Fault) -> Self { Self { last_fault } } /// Returns the most recent fault. #[must_use] pub fn last_fault(&self) -> &Fault { &self.last_fault } } /// Lifecycle state with a typestate payload. #[derive(Debug, Clone)] pub struct Lifecycle { generation: Generation, state: S, } impl Lifecycle { /// Constructs a cold lifecycle. #[must_use] pub fn cold() -> Self { Self { generation: Generation::genesis(), state: Cold, } } /// Begins startup sequence. #[must_use] pub fn ignite(self) -> Lifecycle { Lifecycle { generation: self.generation, state: Starting, } } } impl Lifecycle { /// Marks startup as successful. #[must_use] pub fn arm(self) -> Lifecycle { Lifecycle { generation: self.generation, state: Ready, } } /// Marks startup as failed and enters recovery. #[must_use] pub fn fracture(self, fault: Fault) -> Lifecycle { Lifecycle { generation: self.generation, state: Recovering::new(fault), } } } impl Lifecycle { /// Moves from ready to recovering after a fault. #[must_use] pub fn fracture(self, fault: Fault) -> Lifecycle { Lifecycle { generation: self.generation, state: Recovering::new(fault), } } } impl Lifecycle { /// Advances generation and retries startup. #[must_use] pub fn respawn(self) -> Lifecycle { Lifecycle { generation: self.generation.next(), state: Starting, } } /// Returns the most recent fault. #[must_use] pub fn last_fault(&self) -> &Fault { self.state.last_fault() } } impl Lifecycle { /// Returns the active generation. #[must_use] pub fn generation(&self) -> Generation { self.generation } /// Returns the typestate payload. #[must_use] pub fn state(&self) -> &S { &self.state } } /// Serializable lifecycle snapshot for diagnostics. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LifecycleSnapshot { /// No worker is currently running. Cold { /// Current generation counter. generation: Generation, }, /// Worker startup is in progress. Starting { /// Current generation counter. generation: Generation, }, /// Worker is ready for requests. Ready { /// Current generation counter. generation: Generation, }, /// Worker is recovering after a fault. Recovering { /// Current generation counter. generation: Generation, /// Most recent fault. last_fault: Fault, }, } /// Dynamically typed lifecycle state for runtime storage. #[derive(Debug, Clone)] pub enum DynamicLifecycle { /// Cold typestate wrapper. Cold(Lifecycle), /// Starting typestate wrapper. Starting(Lifecycle), /// Ready typestate wrapper. Ready(Lifecycle), /// Recovering typestate wrapper. Recovering(Lifecycle), } impl DynamicLifecycle { /// Creates a cold dynamic lifecycle. #[must_use] pub fn cold() -> Self { Self::Cold(Lifecycle::cold()) } /// Returns the serializable snapshot. #[must_use] pub fn snapshot(&self) -> LifecycleSnapshot { match self { Self::Cold(state) => LifecycleSnapshot::Cold { generation: state.generation(), }, Self::Starting(state) => LifecycleSnapshot::Starting { generation: state.generation(), }, Self::Ready(state) => LifecycleSnapshot::Ready { generation: state.generation(), }, Self::Recovering(state) => LifecycleSnapshot::Recovering { generation: state.generation(), last_fault: state.last_fault().clone(), }, } } /// Enters startup from cold or recovering. pub fn begin_startup(self) -> Result { match self { Self::Cold(state) => Ok(Self::Starting(state.ignite())), Self::Recovering(state) => Ok(Self::Starting(state.respawn())), Self::Starting(_) | Self::Ready(_) => Err(InvariantViolation::new( "invalid lifecycle transition to starting", )), } } /// Marks startup as complete. pub fn complete_startup(self) -> Result { match self { Self::Starting(state) => Ok(Self::Ready(state.arm())), _ => Err(InvariantViolation::new( "invalid lifecycle transition to ready", )), } } /// Records a fault and enters recovering state. pub fn fracture(self, fault: Fault) -> Result { match self { Self::Starting(state) => Ok(Self::Recovering(state.fracture(fault))), Self::Ready(state) => Ok(Self::Recovering(state.fracture(fault))), Self::Recovering(state) => Ok(Self::Recovering(Lifecycle { generation: state.generation(), state: Recovering::new(fault), })), Self::Cold(_) => Err(InvariantViolation::new("cannot fracture cold lifecycle")), } } } #[cfg(test)] mod tests { use super::{DynamicLifecycle, Lifecycle, LifecycleSnapshot}; use crate::fault::{Fault, FaultClass, FaultCode, FaultDetail}; #[test] fn typestate_chain_advances_generation_on_recovery() { let cold = Lifecycle::cold(); let starting = cold.ignite(); let ready = starting.arm(); let ready_generation = ready.generation(); let fault = Fault::new( ready_generation, FaultClass::Transport, FaultCode::BrokenPipe, FaultDetail::new("broken pipe"), ); let recovering = ready.fracture(fault); let restarted = recovering.respawn(); assert!(restarted.generation() > ready_generation); } #[test] fn dynamic_snapshot_of_recovering_is_infallible() { let cold = DynamicLifecycle::cold(); assert!(matches!(cold.snapshot(), LifecycleSnapshot::Cold { .. })); } }