Skip to main content

appcore_ops/
availability.rs

1//! Stable Runtime liveness and readiness semantics.
2
3use crate::HealthStatus;
4use appcore_core::RuntimeOperationalMode;
5use serde::{Deserialize, Serialize};
6
7/// Coarse operational availability exposed to supervisors and routers.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum RuntimeAvailabilityState {
11    /// Runtime can serve the work allowed by its configured mode.
12    Ready,
13    /// Runtime serves a reduced local workload while a dependency is impaired.
14    Degraded,
15    /// Runtime is alive but policy or security prevents normal traffic.
16    Restricted,
17    /// Runtime is alive and local-first reads may continue without coordination.
18    Isolated,
19    /// Runtime is not alive and accepts no traffic.
20    Stopped,
21}
22
23/// Stable health projection for process, local, distributed, and write traffic.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub struct RuntimeAvailabilityReport {
26    /// Coarse availability state.
27    pub state: RuntimeAvailabilityState,
28    /// Process watchdog signal. False only after the Runtime stops.
29    pub liveness: bool,
30    /// Whether policy-authorized local queries can be served.
31    pub local_readiness: bool,
32    /// Whether work requiring cluster coordination can be served.
33    pub distributed_readiness: bool,
34    /// Whether state-changing commands can be accepted.
35    pub write_readiness: bool,
36}
37
38impl RuntimeAvailabilityReport {
39    /// Projects component health and operational mode into stable semantics.
40    pub fn evaluate(health: HealthStatus, mode: RuntimeOperationalMode) -> Self {
41        if health == HealthStatus::Stopped {
42            return Self::stopped();
43        }
44        if health == HealthStatus::Restricted {
45            return Self::restricted();
46        }
47        match mode {
48            RuntimeOperationalMode::Isolated => Self {
49                state: RuntimeAvailabilityState::Isolated,
50                liveness: true,
51                local_readiness: true,
52                distributed_readiness: false,
53                write_readiness: false,
54            },
55            RuntimeOperationalMode::Degraded => Self::degraded(true),
56            RuntimeOperationalMode::Starting
57            | RuntimeOperationalMode::Discovering
58            | RuntimeOperationalMode::Syncing => Self::degraded(false),
59            RuntimeOperationalMode::ReadOnly => Self::ready(false),
60            RuntimeOperationalMode::ReadWrite if health == HealthStatus::Degraded => {
61                Self::degraded(true)
62            }
63            RuntimeOperationalMode::ReadWrite => Self::ready(true),
64        }
65    }
66
67    fn ready(writes: bool) -> Self {
68        Self {
69            state: RuntimeAvailabilityState::Ready,
70            liveness: true,
71            local_readiness: true,
72            distributed_readiness: true,
73            write_readiness: writes,
74        }
75    }
76
77    fn degraded(local_ready: bool) -> Self {
78        Self {
79            state: RuntimeAvailabilityState::Degraded,
80            liveness: true,
81            local_readiness: local_ready,
82            distributed_readiness: false,
83            write_readiness: false,
84        }
85    }
86
87    fn restricted() -> Self {
88        Self {
89            state: RuntimeAvailabilityState::Restricted,
90            liveness: true,
91            local_readiness: false,
92            distributed_readiness: false,
93            write_readiness: false,
94        }
95    }
96
97    fn stopped() -> Self {
98        Self {
99            state: RuntimeAvailabilityState::Stopped,
100            liveness: false,
101            local_readiness: false,
102            distributed_readiness: false,
103            write_readiness: false,
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn read_write_health_is_fully_ready() {
114        let report = RuntimeAvailabilityReport::evaluate(
115            HealthStatus::Healthy,
116            RuntimeOperationalMode::ReadWrite,
117        );
118        assert_eq!(report.state, RuntimeAvailabilityState::Ready);
119        assert!(report.liveness);
120        assert!(report.local_readiness);
121        assert!(report.distributed_readiness);
122        assert!(report.write_readiness);
123    }
124
125    #[test]
126    fn isolated_runtime_keeps_only_local_reads_ready() {
127        let report = RuntimeAvailabilityReport::evaluate(
128            HealthStatus::Healthy,
129            RuntimeOperationalMode::Isolated,
130        );
131        assert_eq!(report.state, RuntimeAvailabilityState::Isolated);
132        assert!(report.liveness);
133        assert!(report.local_readiness);
134        assert!(!report.distributed_readiness);
135        assert!(!report.write_readiness);
136    }
137
138    #[test]
139    fn restricted_and_stopped_states_fail_readiness() {
140        let restricted = RuntimeAvailabilityReport::evaluate(
141            HealthStatus::Restricted,
142            RuntimeOperationalMode::ReadWrite,
143        );
144        let stopped = RuntimeAvailabilityReport::evaluate(
145            HealthStatus::Stopped,
146            RuntimeOperationalMode::ReadWrite,
147        );
148        assert!(restricted.liveness);
149        assert!(!restricted.local_readiness);
150        assert!(!stopped.liveness);
151    }
152}