oxios_kernel/
readiness.rs1use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
27use std::time::{SystemTime, UNIX_EPOCH};
28
29const STATE_WARMING: u8 = 0;
30const STATE_READY: u8 = 1;
31const STATE_DEGRADED: u8 = 2;
32const STATE_FAILED: u8 = 3;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum SubsystemState {
37 Warming,
39 Ready,
41 Degraded,
43 Failed,
45}
46
47impl SubsystemState {
48 fn to_u8(self) -> u8 {
49 match self {
50 Self::Warming => STATE_WARMING,
51 Self::Ready => STATE_READY,
52 Self::Degraded => STATE_DEGRADED,
53 Self::Failed => STATE_FAILED,
54 }
55 }
56 fn from_u8(v: u8) -> Self {
57 match v {
58 STATE_READY => Self::Ready,
59 STATE_DEGRADED => Self::Degraded,
60 STATE_FAILED => Self::Failed,
61 _ => Self::Warming,
62 }
63 }
64}
65
66use serde::{Deserialize, Serialize};
69
70pub struct ReadinessGate {
74 state_store: AtomicU8,
75 engine: AtomicU8,
76 deadline_secs: AtomicU64,
79}
80
81impl ReadinessGate {
82 pub fn new(deadline_secs: u64) -> Self {
86 Self {
87 state_store: AtomicU8::new(STATE_WARMING),
88 engine: AtomicU8::new(STATE_WARMING),
89 deadline_secs: AtomicU64::new(deadline_secs),
90 }
91 }
92
93 pub fn set_deadline_secs(&self, secs: u64) {
96 self.deadline_secs.store(secs, Ordering::SeqCst);
97 }
98
99 pub fn deadline_secs(&self) -> u64 {
101 self.deadline_secs.load(Ordering::SeqCst)
102 }
103
104 pub fn set_state_store(&self, s: SubsystemState) {
107 self.state_store.store(s.to_u8(), Ordering::SeqCst);
108 self.update_readiness_gauge();
109 }
110
111 pub fn set_engine(&self, s: SubsystemState) {
114 self.engine.store(s.to_u8(), Ordering::SeqCst);
115 self.update_readiness_gauge();
116 }
117
118 fn update_readiness_gauge(&self) {
121 let ready = self.is_ready();
122 crate::metrics::get_metrics()
123 .readiness_state
124 .set(if ready { 1.0 } else { 0.0 });
125 }
126
127 pub fn state_store_state(&self) -> SubsystemState {
129 SubsystemState::from_u8(self.state_store.load(Ordering::SeqCst))
130 }
131
132 pub fn engine_state(&self) -> SubsystemState {
134 SubsystemState::from_u8(self.engine.load(Ordering::SeqCst))
135 }
136
137 pub fn is_ready(&self) -> bool {
143 let s = self.state_store_state();
144 let e = self.engine_state();
145 let s_ok = s == SubsystemState::Ready || s == SubsystemState::Degraded;
146 let e_ok = e == SubsystemState::Ready || e == SubsystemState::Degraded;
147 s_ok && e_ok
148 }
149
150 pub fn enforce_deadline(&self) {
155 let deadline = self.deadline_secs.load(Ordering::SeqCst);
156 if deadline == 0 {
157 return;
158 }
159 if self.is_ready() {
160 return;
161 }
162 let now = SystemTime::now()
163 .duration_since(UNIX_EPOCH)
164 .unwrap_or_default()
165 .as_secs();
166 if now < deadline {
167 return;
168 }
169 if self.state_store_state() == SubsystemState::Warming {
170 self.set_state_store(SubsystemState::Degraded);
171 }
172 if self.engine_state() == SubsystemState::Warming {
173 self.set_engine(SubsystemState::Degraded);
174 }
175 }
176}
177
178impl std::fmt::Debug for ReadinessGate {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("ReadinessGate")
181 .field("state_store", &self.state_store_state())
182 .field("engine", &self.engine_state())
183 .field("is_ready", &self.is_ready())
184 .finish()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn new_starts_warming_and_not_ready() {
194 let g = ReadinessGate::new(0);
195 assert!(!g.is_ready());
196 assert_eq!(g.state_store_state(), SubsystemState::Warming);
197 assert_eq!(g.engine_state(), SubsystemState::Warming);
198 }
199
200 #[test]
201 fn both_ready_means_ready() {
202 let g = ReadinessGate::new(0);
203 g.set_state_store(SubsystemState::Ready);
204 g.set_engine(SubsystemState::Ready);
205 assert!(g.is_ready());
206 }
207
208 #[test]
209 fn engine_degraded_still_counts_as_ready() {
210 let g = ReadinessGate::new(0);
211 g.set_state_store(SubsystemState::Ready);
212 g.set_engine(SubsystemState::Degraded);
213 assert!(g.is_ready());
214 }
215
216 #[test]
217 fn engine_failed_keeps_gate_closed() {
218 let g = ReadinessGate::new(0);
219 g.set_state_store(SubsystemState::Ready);
220 g.set_engine(SubsystemState::Failed);
221 assert!(!g.is_ready());
222 }
223
224 #[test]
225 fn state_store_not_ready_keeps_gate_closed() {
226 let g = ReadinessGate::new(0);
227 g.set_engine(SubsystemState::Ready);
228 assert!(!g.is_ready());
229 }
230
231 #[test]
232 fn deadline_elapsed_promotes_warming_to_degraded() {
233 let g = ReadinessGate::new(1);
235 std::thread::sleep(std::time::Duration::from_millis(1100));
236 g.enforce_deadline();
237 assert_eq!(g.state_store_state(), SubsystemState::Degraded);
238 assert_eq!(g.engine_state(), SubsystemState::Degraded);
239 assert!(g.is_ready());
240 }
241
242 #[test]
243 fn deadline_not_yet_elapsed_keeps_warming() {
244 let deadline = SystemTime::now()
245 .duration_since(UNIX_EPOCH)
246 .unwrap()
247 .as_secs()
248 + 60;
249 let g = ReadinessGate::new(deadline);
250 g.enforce_deadline();
251 assert_eq!(g.state_store_state(), SubsystemState::Warming);
252 assert!(!g.is_ready());
253 }
254
255 #[test]
256 fn deadline_zero_disables_enforcement() {
257 let g = ReadinessGate::new(0);
258 g.enforce_deadline();
259 assert_eq!(g.state_store_state(), SubsystemState::Warming);
260 }
261
262 #[test]
269 fn readiness_gauge_tracks_gate_state() {
270 let before = current_readiness_gauge();
272
273 let g = ReadinessGate::new(0);
274 g.set_state_store(SubsystemState::Ready);
276 g.set_engine(SubsystemState::Ready);
277 let both_ready = current_readiness_gauge();
278 assert!(
279 (both_ready - 1.0).abs() < f64::EPSILON,
280 "both subsystems Ready should yield gauge=1.0, got {both_ready}"
281 );
282
283 g.set_engine(SubsystemState::Failed);
285 let one_failed = current_readiness_gauge();
286 assert!(
287 one_failed < both_ready,
288 "engine Failed should drop the gauge (before={before}, after={one_failed})"
289 );
290 }
291
292 fn current_readiness_gauge() -> f64 {
293 let export = crate::metrics::registry().export();
299 for line in export.lines() {
300 if let Some(rest) = line.strip_prefix("oxios_readiness_state ")
301 && let Ok(v) = rest.trim().parse::<f64>()
302 {
303 return v;
304 }
305 }
306 panic!(
307 "oxios_readiness_state gauge not found in registry export — \
308 did register_builtin_metrics run?"
309 );
310 }
311}