oxios_kernel/resilience/
health.rs1use parking_lot::RwLock;
14use std::collections::HashMap;
15use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
16
17#[derive(Debug, Clone)]
19pub struct BreakerConfig {
20 pub failure_threshold: u32,
22 pub reset_after_secs: u64,
24}
25
26impl Default for BreakerConfig {
27 fn default() -> Self {
28 Self {
29 failure_threshold: 5,
30 reset_after_secs: 60,
31 }
32 }
33}
34
35#[derive(Debug)]
37struct ProviderState {
38 failures: AtomicU32,
39 first_failure_at: AtomicU64,
41 opened_at: AtomicU64,
43}
44
45impl ProviderState {
46 fn new() -> Self {
47 Self {
48 failures: AtomicU32::new(0),
49 first_failure_at: AtomicU64::new(0),
50 opened_at: AtomicU64::new(0),
51 }
52 }
53}
54
55pub struct ProviderHealthRegistry {
60 states: RwLock<HashMap<String, ProviderState>>,
61 config: BreakerConfig,
62}
63
64impl ProviderHealthRegistry {
65 pub fn new(config: BreakerConfig) -> Self {
67 Self {
68 states: RwLock::new(HashMap::new()),
69 config,
70 }
71 }
72
73 pub fn is_healthy(&self, provider: &str) -> bool {
80 let states = self.states.read();
81 let state = match states.get(provider) {
82 Some(s) => s,
83 None => return true, };
85 let opened = state.opened_at.load(Ordering::SeqCst);
86 if opened == 0 {
87 return true; }
89 let elapsed = now_ms().saturating_sub(opened);
91 elapsed >= self.config.reset_after_secs * 1000
92 }
93
94 pub fn record_success(&self, provider: &str) {
97 let mut states = self.states.write();
98 let state = states
99 .entry(provider.to_string())
100 .or_insert_with(ProviderState::new);
101 state.failures.store(0, Ordering::SeqCst);
102 state.first_failure_at.store(0, Ordering::SeqCst);
103 state.opened_at.store(0, Ordering::SeqCst);
104 }
105
106 pub fn record_failure(&self, provider: &str) {
109 let mut states = self.states.write();
110 let state = states
111 .entry(provider.to_string())
112 .or_insert_with(ProviderState::new);
113 let n = state.failures.fetch_add(1, Ordering::SeqCst) + 1;
114 let _ = state.first_failure_at.compare_exchange(
116 0,
117 now_ms(),
118 Ordering::SeqCst,
119 Ordering::SeqCst,
120 );
121 if n >= self.config.failure_threshold {
122 state.opened_at.store(now_ms(), Ordering::SeqCst);
124 state.first_failure_at.store(0, Ordering::SeqCst);
125 }
126 }
127}
128
129fn now_ms() -> u64 {
130 use std::time::SystemTime;
132 SystemTime::now()
133 .duration_since(SystemTime::UNIX_EPOCH)
134 .map(|d| d.as_millis() as u64)
135 .unwrap_or(0)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn healthy_by_default() {
144 let reg = ProviderHealthRegistry::new(BreakerConfig::default());
145 assert!(reg.is_healthy("unknown-provider"));
146 }
147
148 #[test]
149 fn opens_after_threshold_failures() {
150 let reg = ProviderHealthRegistry::new(BreakerConfig {
151 failure_threshold: 3,
152 reset_after_secs: 60,
153 });
154 reg.record_failure("p");
155 reg.record_failure("p");
156 assert!(reg.is_healthy("p"));
158 reg.record_failure("p");
159 assert!(!reg.is_healthy("p"));
161 }
162
163 #[test]
164 fn success_resets_failures() {
165 let reg = ProviderHealthRegistry::new(BreakerConfig {
166 failure_threshold: 3,
167 reset_after_secs: 3600, });
169 reg.record_failure("p");
170 reg.record_failure("p");
171 reg.record_success("p");
172 reg.record_failure("p");
174 assert!(reg.is_healthy("p"));
175 }
176}