agent_base/engine/
circuit_breaker.rs1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::sync::Mutex;
3use std::time::Instant;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum CircuitState {
8 Closed,
10 Open,
12 HalfOpen,
14}
15
16#[derive(Debug)]
29pub struct CircuitBreaker {
30 failure_threshold: usize,
31 open_duration_ms: u64,
32 state: Mutex<CircuitState>,
33 consecutive_failures: AtomicUsize,
34 opened_at: Mutex<Option<Instant>>,
35}
36
37impl CircuitBreaker {
38 pub fn new(failure_threshold: usize, open_duration_ms: u64) -> Self {
44 Self {
45 failure_threshold,
46 open_duration_ms,
47 state: Mutex::new(CircuitState::Closed),
48 consecutive_failures: AtomicUsize::new(0),
49 opened_at: Mutex::new(None),
50 }
51 }
52
53 pub fn record_success(&self) {
57 self.consecutive_failures.store(0, Ordering::SeqCst);
58 let mut state = self.state.lock().unwrap();
59 let prev = *state;
60 *state = CircuitState::Closed;
61 *self.opened_at.lock().unwrap() = None;
62 if prev != CircuitState::Closed {
63 tracing::info!(from = ?prev, to = ?CircuitState::Closed, "circuit breaker state changed");
64 }
65 }
66
67 pub fn record_failure(&self) {
72 let count = self.consecutive_failures.fetch_add(1, Ordering::SeqCst) + 1;
73 if count >= self.failure_threshold {
74 let mut state = self.state.lock().unwrap();
75 let prev = *state;
76 if prev != CircuitState::Open {
77 *state = CircuitState::Open;
78 *self.opened_at.lock().unwrap() = Some(Instant::now());
79 tracing::warn!(
80 from = ?prev,
81 to = ?CircuitState::Open,
82 consecutive_failures = count,
83 threshold = self.failure_threshold,
84 "circuit breaker opened"
85 );
86 }
87 }
88 }
89
90 pub fn is_available(&self) -> bool {
100 let mut state = self.state.lock().unwrap();
101 match *state {
102 CircuitState::Closed => true,
103 CircuitState::HalfOpen => true,
104 CircuitState::Open => {
105 let opened_at = self.opened_at.lock().unwrap();
107 if let Some(at) = *opened_at
108 && at.elapsed().as_millis() >= self.open_duration_ms as u128
109 {
110 *state = CircuitState::HalfOpen;
111 tracing::info!(
112 from = ?CircuitState::Open,
113 to = ?CircuitState::HalfOpen,
114 "circuit breaker half-open, allowing trial request"
115 );
116 return true;
117 }
118 false
119 }
120 }
121 }
122
123 pub fn state(&self) -> CircuitState {
125 *self.state.lock().unwrap()
126 }
127
128 pub fn consecutive_failures(&self) -> usize {
130 self.consecutive_failures.load(Ordering::SeqCst)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use std::sync::Arc;
138 use std::thread;
139 use std::time::Duration;
140
141 #[test]
142 fn new_breaker_is_closed() {
143 let cb = CircuitBreaker::new(3, 1000);
144 assert_eq!(cb.state(), CircuitState::Closed);
145 assert!(cb.is_available());
146 assert_eq!(cb.consecutive_failures(), 0);
147 }
148
149 #[test]
150 fn stays_closed_below_threshold() {
151 let cb = CircuitBreaker::new(3, 1000);
152 cb.record_failure();
153 assert_eq!(cb.state(), CircuitState::Closed);
154 assert!(cb.is_available());
155 cb.record_failure();
156 assert_eq!(cb.state(), CircuitState::Closed);
157 assert!(cb.is_available());
158 }
159
160 #[test]
161 fn opens_at_threshold() {
162 let cb = CircuitBreaker::new(3, 1000);
163 cb.record_failure();
164 cb.record_failure();
165 cb.record_failure();
166 assert_eq!(cb.state(), CircuitState::Open);
167 assert!(!cb.is_available());
168 }
169
170 #[test]
171 fn success_resets_counter() {
172 let cb = CircuitBreaker::new(3, 1000);
173 cb.record_failure();
174 cb.record_failure();
175 cb.record_success();
176 assert_eq!(cb.state(), CircuitState::Closed);
177 assert_eq!(cb.consecutive_failures(), 0);
178 }
179
180 #[test]
181 fn transitions_to_half_open_after_duration() {
182 let cb = CircuitBreaker::new(2, 50); cb.record_failure();
184 cb.record_failure();
185 assert_eq!(cb.state(), CircuitState::Open);
186 assert!(!cb.is_available());
187
188 thread::sleep(Duration::from_millis(60));
189 assert!(cb.is_available());
190 assert_eq!(cb.state(), CircuitState::HalfOpen);
191 }
192
193 #[test]
194 fn half_open_success_closes() {
195 let cb = CircuitBreaker::new(2, 50);
196 cb.record_failure();
197 cb.record_failure();
198 thread::sleep(Duration::from_millis(60));
199 assert!(cb.is_available()); cb.record_success();
201 assert_eq!(cb.state(), CircuitState::Closed);
202 }
203
204 #[test]
205 fn half_open_failure_reopens() {
206 let cb = CircuitBreaker::new(2, 50);
207 cb.record_failure();
208 cb.record_failure();
209 thread::sleep(Duration::from_millis(60));
210 assert!(cb.is_available()); cb.record_failure();
212 assert_eq!(cb.state(), CircuitState::Open);
213 assert!(!cb.is_available());
214 }
215
216 #[test]
217 fn weak_reference_allows_cleanup() {
218 let strong = Arc::new(CircuitBreaker::new(3, 1000));
219 let weak = Arc::downgrade(&strong);
220 assert!(weak.upgrade().is_some());
221
222 drop(strong);
223 assert!(weak.upgrade().is_none());
224 }
225}