swarm repositories / source
summaryrefslogtreecommitdiff
path: root/crates/ra-mcp-domain/src/lifecycle.rs
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-19 15:49:41 -0400
committermain <main@swarm.moe>2026-03-19 15:49:41 -0400
commitfa1bd32800b65aab31ea732dd240261b4047522c (patch)
tree2fd08af6f36b8beb3c7c941990becc1a0a091d62 /crates/ra-mcp-domain/src/lifecycle.rs
downloadadequate-rust-mcp-310b00d40db7639654bdc1d416ae222de481e8fc.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.rs259
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 { .. }));
+ }
+}