oxios_kernel/a2a/
circuit_breaker.rs1use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18fn now_epoch_secs() -> u64 {
23 SystemTime::now()
24 .duration_since(UNIX_EPOCH)
25 .map(|d| d.as_secs())
26 .unwrap_or(0)
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CircuitState {
32 Closed,
34 Open,
36 HalfOpen,
38}
39
40impl CircuitState {
41 fn from_u8(v: u8) -> Self {
42 match v {
43 0 => CircuitState::Closed,
44 1 => CircuitState::Open,
45 2 => CircuitState::HalfOpen,
46 _ => CircuitState::Closed,
47 }
48 }
49}
50
51#[derive(Debug)]
56pub struct A2ACircuitBreaker {
57 state: AtomicU8,
58 failure_count: AtomicU32,
59 success_count: AtomicU32,
60 last_failure_time: AtomicU64,
61 threshold: u32,
62 reset_timeout: Duration,
63}
64
65impl A2ACircuitBreaker {
66 pub fn new(threshold: u32, reset_timeout_secs: u64) -> Self {
72 Self {
73 state: AtomicU8::new(CircuitState::Closed as u8),
74 failure_count: AtomicU32::new(0),
75 success_count: AtomicU32::new(0),
76 last_failure_time: AtomicU64::new(0),
77 threshold,
78 reset_timeout: Duration::from_secs(reset_timeout_secs),
79 }
80 }
81
82 pub fn state(&self) -> CircuitState {
88 CircuitState::from_u8(self.state.load(Ordering::SeqCst))
89 }
90
91 pub fn is_allowed(&self) -> bool {
93 match self.state() {
94 CircuitState::Closed => true,
95 CircuitState::Open => {
96 let last_failure = self.last_failure_time.load(Ordering::Acquire);
98 let elapsed = now_epoch_secs().saturating_sub(last_failure);
99 if elapsed > self.reset_timeout.as_secs() {
100 self.state
102 .store(CircuitState::HalfOpen as u8, Ordering::SeqCst);
103 self.success_count.store(0, Ordering::Release);
104 true
105 } else {
106 false
107 }
108 }
109 CircuitState::HalfOpen => {
110 self.success_count.load(Ordering::Acquire) < 2
112 }
113 }
114 }
115
116 pub fn record_success(&self) {
118 match self.state() {
119 CircuitState::HalfOpen => {
120 let successes = self.success_count.fetch_add(1, Ordering::AcqRel) + 1;
121 if successes >= 2 {
122 self.state
124 .store(CircuitState::Closed as u8, Ordering::SeqCst);
125 self.failure_count.store(0, Ordering::Release);
126 tracing::info!("A2A circuit breaker CLOSED (recovery successful)");
127 }
128 }
129 CircuitState::Closed => {
130 self.failure_count.store(0, Ordering::Release);
132 }
133 CircuitState::Open => {}
134 }
135 }
136
137 pub fn record_failure(&self) {
139 let failures = self.failure_count.fetch_add(1, Ordering::AcqRel) + 1;
140 self.last_failure_time
141 .store(now_epoch_secs(), Ordering::Release);
142
143 if failures >= self.threshold && self.state() != CircuitState::Open {
144 self.state.store(CircuitState::Open as u8, Ordering::SeqCst);
145 tracing::warn!(
146 failures,
147 threshold = self.threshold,
148 "A2A circuit breaker OPEN"
149 );
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_initial_state_is_closed() {
160 let cb = A2ACircuitBreaker::new(3, 10);
161 assert_eq!(cb.state(), CircuitState::Closed);
162 assert!(cb.is_allowed());
163 }
164
165 #[test]
166 fn test_opens_after_threshold() {
167 let cb = A2ACircuitBreaker::new(3, 10);
168
169 cb.record_failure();
170 assert_eq!(cb.state(), CircuitState::Closed);
171
172 cb.record_failure();
173 assert_eq!(cb.state(), CircuitState::Closed);
174
175 cb.record_failure(); assert_eq!(cb.state(), CircuitState::Open);
177 assert!(!cb.is_allowed());
178 }
179
180 #[test]
181 fn test_success_resets_failure_count() {
182 let cb = A2ACircuitBreaker::new(3, 10);
183
184 cb.record_failure();
185 cb.record_failure();
186 cb.record_success(); assert_eq!(cb.failure_count.load(Ordering::Relaxed), 0);
189 }
190}