Skip to main content

paramodel_elements/
lifecycle.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Lifecycle metadata: health-check timing, operational state, and
5//! shutdown semantics.
6//!
7//! Per SRD-0007 D9–D11: `HealthCheckSpec` controls readiness polling;
8//! `OperationalState` is the 11-variant runtime lifecycle enum;
9//! `ShutdownSemantics` decides whether reducto emits a `Teardown` or
10//! an `Await` step.
11
12use std::time::Duration;
13
14use jiff::Timestamp;
15use serde::{Deserialize, Serialize};
16
17// ---------------------------------------------------------------------------
18// HealthCheckSpec.
19// ---------------------------------------------------------------------------
20
21/// Readiness-polling timing.
22///
23/// The host system owns the health-check mechanism (protocol, endpoint,
24/// acceptance); paramodel only needs the timing parameters so the
25/// runtime can coordinate.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct HealthCheckSpec {
28    /// Maximum total duration the runtime waits for readiness.
29    #[serde(with = "duration_secs")]
30    pub timeout:        Duration,
31    /// Number of retry attempts before giving up.
32    pub max_retries:    u32,
33    /// Delay between retry attempts.
34    #[serde(with = "duration_secs")]
35    pub retry_interval: Duration,
36}
37
38impl HealthCheckSpec {
39    /// Construct a health-check spec.
40    #[must_use]
41    pub const fn new(timeout: Duration, max_retries: u32, retry_interval: Duration) -> Self {
42        Self {
43            timeout,
44            max_retries,
45            retry_interval,
46        }
47    }
48}
49
50// Manual Duration serialisation as floating-point seconds keeps the
51// wire format compact and portable across languages.
52mod duration_secs {
53    use serde::{Deserializer, Serializer};
54    use std::time::Duration;
55
56    pub fn serialize<S: Serializer>(
57        d: &Duration,
58        s: S,
59    ) -> std::result::Result<S::Ok, S::Error> {
60        s.serialize_f64(d.as_secs_f64())
61    }
62
63    pub fn deserialize<'de, D: Deserializer<'de>>(
64        d: D,
65    ) -> std::result::Result<Duration, D::Error> {
66        use serde::Deserialize;
67        let secs = f64::deserialize(d)?;
68        if !secs.is_finite() || secs < 0.0 {
69            return Err(serde::de::Error::custom(
70                "duration seconds must be a finite non-negative f64",
71            ));
72        }
73        Ok(Duration::from_secs_f64(secs))
74    }
75}
76
77// ---------------------------------------------------------------------------
78// OperationalState.
79// ---------------------------------------------------------------------------
80
81/// Element runtime lifecycle state.
82///
83/// Normal progression: `Inactive → Provisioning → Starting →
84/// HealthCheck → Ready → Running → Stopping → Stopped → Terminated`.
85/// `Failed` and `Unknown` are non-sequential and reachable from any
86/// state.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum OperationalState {
90    /// Not yet started.
91    Inactive,
92    /// Infrastructure is being allocated.
93    Provisioning,
94    /// Process is starting up.
95    Starting,
96    /// Verifying readiness.
97    HealthCheck,
98    /// Available for use.
99    Ready,
100    /// Actively serving a trial.
101    Running,
102    /// Graceful shutdown in progress.
103    Stopping,
104    /// Stopped normally; resources still allocated.
105    Stopped,
106    /// Error state; cannot operate.
107    Failed,
108    /// Fully torn down; all resources released.
109    Terminated,
110    /// Status cannot be determined.
111    Unknown,
112}
113
114// ---------------------------------------------------------------------------
115// LiveStatusSummary.
116// ---------------------------------------------------------------------------
117
118/// One-shot state snapshot returned by
119/// [`ElementRuntime::status_check`](crate::ElementRuntime::status_check).
120#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
121pub struct LiveStatusSummary {
122    /// The observed state.
123    pub state:   OperationalState,
124    /// One-line human-readable evidence / explanation.
125    pub summary: String,
126}
127
128impl LiveStatusSummary {
129    /// Construct an `Unknown` summary with the given explanation.
130    #[must_use]
131    pub fn unknown(summary: impl Into<String>) -> Self {
132        Self {
133            state:   OperationalState::Unknown,
134            summary: summary.into(),
135        }
136    }
137
138    /// Construct an `Inactive` summary with a default message.
139    #[must_use]
140    pub fn inactive() -> Self {
141        Self {
142            state:   OperationalState::Inactive,
143            summary: "element not yet started".to_owned(),
144        }
145    }
146}
147
148// ---------------------------------------------------------------------------
149// ShutdownSemantics.
150// ---------------------------------------------------------------------------
151
152/// Whether an element terminates by explicit signal or by self-completion.
153///
154/// Reducto Rule 1 uses this to pick between `Teardown` and `Await`
155/// steps. Default is `Service`.
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum ShutdownSemantics {
159    /// Long-running; requires an explicit stop signal.
160    #[default]
161    Service,
162    /// Self-terminating; awaited rather than stopped.
163    Command,
164}
165
166// ---------------------------------------------------------------------------
167// StateTransition.
168// ---------------------------------------------------------------------------
169
170/// One observed transition between two [`OperationalState`]s.
171///
172/// Emitted by [`ElementRuntime::observe_state`](crate::ElementRuntime::observe_state)
173/// listeners. Implementations must deliver a synthetic initial
174/// transition from `Unknown` to the current state so registration acts
175/// as catch-up.
176#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
177pub struct StateTransition {
178    /// Previous state.
179    pub from:      OperationalState,
180    /// New state.
181    pub to:        OperationalState,
182    /// One-line evidence for the transition.
183    pub summary:   String,
184    /// Observation timestamp.
185    pub timestamp: Timestamp,
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn shutdown_default_is_service() {
194        assert_eq!(ShutdownSemantics::default(), ShutdownSemantics::Service);
195    }
196
197    #[test]
198    fn live_status_helpers() {
199        let u = LiveStatusSummary::unknown("probe failed");
200        assert_eq!(u.state, OperationalState::Unknown);
201        assert_eq!(u.summary, "probe failed");
202        let i = LiveStatusSummary::inactive();
203        assert_eq!(i.state, OperationalState::Inactive);
204    }
205
206    #[test]
207    fn operational_state_serde_is_snake_case() {
208        let s = serde_json::to_string(&OperationalState::HealthCheck).unwrap();
209        assert_eq!(s, "\"health_check\"");
210        let back: OperationalState = serde_json::from_str(&s).unwrap();
211        assert_eq!(back, OperationalState::HealthCheck);
212    }
213
214    #[test]
215    fn health_check_spec_serde_roundtrip() {
216        let spec = HealthCheckSpec::new(
217            Duration::from_secs(30),
218            5,
219            Duration::from_millis(500),
220        );
221        let json = serde_json::to_string(&spec).unwrap();
222        let back: HealthCheckSpec = serde_json::from_str(&json).unwrap();
223        assert_eq!(spec, back);
224    }
225
226    #[test]
227    fn state_transition_serde_roundtrip() {
228        let t = StateTransition {
229            from:      OperationalState::Unknown,
230            to:        OperationalState::Ready,
231            summary:   "probe passed".to_owned(),
232            timestamp: Timestamp::from_second(0).unwrap(),
233        };
234        let json = serde_json::to_string(&t).unwrap();
235        let back: StateTransition = serde_json::from_str(&json).unwrap();
236        assert_eq!(t, back);
237    }
238}