diff options
| author | main <main@swarm.moe> | 2026-03-19 15:49:41 -0400 |
|---|---|---|
| committer | main <main@swarm.moe> | 2026-03-19 15:49:41 -0400 |
| commit | fa1bd32800b65aab31ea732dd240261b4047522c (patch) | |
| tree | 2fd08af6f36b8beb3c7c941990becc1a0a091d62 /crates/ra-mcp-domain/src/lifecycle.rs | |
| download | adequate-rust-mcp-fa1bd32800b65aab31ea732dd240261b4047522c.zip | |
Release adequate-rust-mcp 1.0.0v1.0.0
Diffstat (limited to 'crates/ra-mcp-domain/src/lifecycle.rs')
| -rw-r--r-- | crates/ra-mcp-domain/src/lifecycle.rs | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/crates/ra-mcp-domain/src/lifecycle.rs b/crates/ra-mcp-domain/src/lifecycle.rs new file mode 100644 index 0000000..91007ac --- /dev/null +++ b/crates/ra-mcp-domain/src/lifecycle.rs @@ -0,0 +1,259 @@ +//! 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<S> { + generation: Generation, + state: S, +} + +impl Lifecycle<Cold> { + /// 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<Starting> { + Lifecycle { + generation: self.generation, + state: Starting, + } + } +} + +impl Lifecycle<Starting> { + /// Marks startup as successful. + #[must_use] + pub fn arm(self) -> Lifecycle<Ready> { + Lifecycle { + generation: self.generation, + state: Ready, + } + } + + /// Marks startup as failed and enters recovery. + #[must_use] + pub fn fracture(self, fault: Fault) -> Lifecycle<Recovering> { + Lifecycle { + generation: self.generation, + state: Recovering::new(fault), + } + } +} + +impl Lifecycle<Ready> { + /// Moves from ready to recovering after a fault. + #[must_use] + pub fn fracture(self, fault: Fault) -> Lifecycle<Recovering> { + Lifecycle { + generation: self.generation, + state: Recovering::new(fault), + } + } +} + +impl Lifecycle<Recovering> { + /// Advances generation and retries startup. + #[must_use] + pub fn respawn(self) -> Lifecycle<Starting> { + 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<S> Lifecycle<S> { + /// 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<Cold>), + /// Starting typestate wrapper. + Starting(Lifecycle<Starting>), + /// Ready typestate wrapper. + Ready(Lifecycle<Ready>), + /// Recovering typestate wrapper. + Recovering(Lifecycle<Recovering>), +} + +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<Self, InvariantViolation> { + 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<Self, InvariantViolation> { + 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<Self, InvariantViolation> { + 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 { .. })); + } +} |