Skip to main content

meerkat_runtime/
runtime_state.rs

1//! §22 RuntimeState — the runtime's public state projection.
2//!
3//! Canonical live transition legality lives in the checked-in `MeerkatMachine`
4//! plus the runtime driver that realizes its coarse control transitions.
5
6use serde::{Deserialize, Serialize};
7
8/// The state of a runtime instance.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum RuntimeState {
13    /// Initializing (first state after creation).
14    Initializing,
15    /// Idle — no executor attached, no run in progress, ready to accept input.
16    Idle,
17    /// Attached — executor attached, runtime loop alive, waiting for input.
18    Attached,
19    /// A run is in progress.
20    Running,
21    /// Retired — no longer accepting new input, draining existing.
22    Retired,
23    /// Stopped by runtime control; generated lifecycle authority classifies
24    /// this as non-terminal while the runtime can still be recovered or
25    /// destroyed.
26    Stopped,
27    /// Destroyed (terminal).
28    Destroyed,
29}
30
31impl std::fmt::Display for RuntimeState {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Initializing => write!(f, "initializing"),
35            Self::Idle => write!(f, "idle"),
36            Self::Attached => write!(f, "attached"),
37            Self::Running => write!(f, "running"),
38            Self::Retired => write!(f, "retired"),
39            Self::Stopped => write!(f, "stopped"),
40            Self::Destroyed => write!(f, "destroyed"),
41        }
42    }
43}
44
45/// Error when an invalid runtime state transition is attempted.
46#[derive(Debug, Clone, thiserror::Error)]
47#[error("Invalid runtime state transition: {from} -> {to}")]
48pub struct RuntimeStateTransitionError {
49    pub from: RuntimeState,
50    pub to: RuntimeState,
51}
52
53#[cfg(test)]
54#[allow(clippy::unwrap_used)]
55mod tests {
56    use super::*;
57    use crate::meerkat_machine::{self, dsl};
58
59    #[test]
60    fn lifecycle_facts_come_from_machine_authority() {
61        let stopped = meerkat_machine::classify_runtime_lifecycle_state(RuntimeState::Stopped)
62            .expect("stopped classification");
63        assert_eq!(
64            stopped.terminality,
65            dsl::RuntimeLifecycleTerminality::NonTerminal
66        );
67        assert_eq!(
68            stopped.ingress_admission,
69            dsl::RuntimeIngressAdmission::NotReady
70        );
71
72        let destroyed = meerkat_machine::classify_runtime_lifecycle_state(RuntimeState::Destroyed)
73            .expect("destroyed classification");
74        assert_eq!(
75            destroyed.terminality,
76            dsl::RuntimeLifecycleTerminality::Terminal
77        );
78        assert_eq!(
79            destroyed.ingress_admission,
80            dsl::RuntimeIngressAdmission::Destroyed
81        );
82
83        let idle = meerkat_machine::classify_runtime_lifecycle_state(RuntimeState::Idle)
84            .expect("idle classification");
85        assert!(idle.can_accept_input());
86        assert!(idle.can_process_queue());
87        assert!(idle.can_prepare_run());
88
89        let running = meerkat_machine::classify_runtime_lifecycle_state(RuntimeState::Running)
90            .expect("running classification");
91        assert!(running.can_accept_input());
92        assert!(!running.can_process_queue());
93        assert!(!running.can_prepare_run());
94
95        let running_without_binding =
96            meerkat_machine::classify_runtime_loop_queue_admission(RuntimeState::Running, false)
97                .expect("running without binding queue admission");
98        assert!(!running_without_binding.can_process_queue());
99        assert_eq!(
100            running_without_binding.run_binding,
101            dsl::RuntimeLoopRunBinding::Blocked
102        );
103
104        let running_with_binding =
105            meerkat_machine::classify_runtime_loop_queue_admission(RuntimeState::Running, true)
106                .expect("running with binding queue admission");
107        assert!(running_with_binding.can_process_queue());
108        assert_eq!(
109            running_with_binding.run_binding,
110            dsl::RuntimeLoopRunBinding::UsePrebound
111        );
112
113        let idle_queue =
114            meerkat_machine::classify_runtime_loop_queue_admission(RuntimeState::Idle, false)
115                .expect("idle queue admission");
116        assert!(idle_queue.can_process_queue());
117        assert_eq!(
118            idle_queue.run_binding,
119            dsl::RuntimeLoopRunBinding::AllocateNew
120        );
121
122        assert_eq!(
123            meerkat_machine::classify_runtime_lifecycle_durable_state(RuntimeState::Attached)
124                .expect("attached durability classification"),
125            RuntimeState::Idle,
126            "generated durability classification owns the process-local Attached recovery projection"
127        );
128        assert_eq!(
129            meerkat_machine::classify_runtime_lifecycle_durable_state(RuntimeState::Running)
130                .expect("running durability classification"),
131            RuntimeState::Running
132        );
133    }
134
135    #[test]
136    fn serde_roundtrip_all_states() {
137        for state in [
138            RuntimeState::Initializing,
139            RuntimeState::Idle,
140            RuntimeState::Attached,
141            RuntimeState::Running,
142            RuntimeState::Retired,
143            RuntimeState::Stopped,
144            RuntimeState::Destroyed,
145        ] {
146            let json = serde_json::to_value(state).unwrap();
147            let parsed: RuntimeState = serde_json::from_value(json).unwrap();
148            assert_eq!(state, parsed);
149        }
150    }
151
152    #[test]
153    fn display() {
154        assert_eq!(RuntimeState::Idle.to_string(), "idle");
155        assert_eq!(RuntimeState::Attached.to_string(), "attached");
156        assert_eq!(RuntimeState::Running.to_string(), "running");
157        assert_eq!(RuntimeState::Destroyed.to_string(), "destroyed");
158    }
159}