1use std::sync::{Arc, Mutex};
4
5use crate::core::clock::{Clock, SystemClock};
6use crate::core::errors::PoolError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum BreakerState {
18 Closed,
20 Open,
22 HalfOpen,
24}
25
26impl BreakerState {
27 pub fn as_str(&self) -> &'static str {
29 match self {
30 BreakerState::Closed => "closed",
31 BreakerState::Open => "open",
32 BreakerState::HalfOpen => "half_open",
33 }
34 }
35}
36
37impl std::fmt::Display for BreakerState {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.write_str(self.as_str())
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct BreakerConfig {
46 pub failure_threshold: u32,
48 pub recovery_timeout: f64,
50 pub half_open_max_calls: u32,
52 pub success_threshold: u32,
54}
55
56impl Default for BreakerConfig {
57 fn default() -> Self {
58 Self {
59 failure_threshold: 5,
60 recovery_timeout: 30.0,
61 half_open_max_calls: 1,
62 success_threshold: 1,
63 }
64 }
65}
66
67impl BreakerConfig {
68 pub fn validate(&self) -> Result<(), PoolError> {
70 if self.failure_threshold < 1 || self.half_open_max_calls < 1 || self.success_threshold < 1
71 {
72 return Err(PoolError::Config("breaker thresholds must be >= 1".into()));
73 }
74 if self.recovery_timeout < 0.0 {
75 return Err(PoolError::Config(
76 "breaker.recovery_timeout must be >= 0".into(),
77 ));
78 }
79 Ok(())
80 }
81}
82
83pub type OnStateChange = Box<dyn Fn(BreakerState, BreakerState) + Send + Sync>;
85
86struct Inner {
87 state: BreakerState,
88 failures: u32,
89 opened_at: f64,
90 probes_inflight: u32,
91 probe_successes: u32,
92}
93
94pub struct CircuitBreaker {
99 cfg: BreakerConfig,
100 clock: Arc<dyn Clock>,
101 on_change: Option<OnStateChange>,
102 inner: Mutex<Inner>,
103}
104
105impl CircuitBreaker {
106 pub fn new(
108 config: BreakerConfig,
109 clock: Option<Arc<dyn Clock>>,
110 on_state_change: Option<OnStateChange>,
111 ) -> Self {
112 Self {
113 cfg: config,
114 clock: clock.unwrap_or_else(|| Arc::new(SystemClock::new())),
115 on_change: on_state_change,
116 inner: Mutex::new(Inner {
117 state: BreakerState::Closed,
118 failures: 0,
119 opened_at: 0.0,
120 probes_inflight: 0,
121 probe_successes: 0,
122 }),
123 }
124 }
125
126 pub fn state(&self) -> BreakerState {
128 let mut g = self.inner.lock().unwrap();
129 let transition = self.refresh(&mut g);
130 let state = g.state;
131 drop(g);
132 self.emit(transition);
133 state
134 }
135
136 pub fn before_call(&self) -> Result<(), PoolError> {
138 let mut call_err = None;
139 let mut g = self.inner.lock().unwrap();
140 let transition = self.refresh(&mut g);
141 match g.state {
142 BreakerState::Open => {
143 let wait = (self.cfg.recovery_timeout
145 - (self.clock.monotonic() - g.opened_at))
146 .max(0.0);
147 call_err = Some(PoolError::CircuitOpen { retry_after: wait });
148 }
149 BreakerState::HalfOpen => {
150 if g.probes_inflight >= self.cfg.half_open_max_calls {
151 call_err = Some(PoolError::CircuitOpen { retry_after: 0.0 });
152 } else {
153 g.probes_inflight += 1;
154 }
155 }
156 BreakerState::Closed => {}
157 }
158 drop(g);
159 self.emit(transition);
160 match call_err {
161 Some(e) => Err(e),
162 None => Ok(()),
163 }
164 }
165
166 pub fn record_success(&self) {
168 let mut transition = None;
169 let mut g = self.inner.lock().unwrap();
170 match g.state {
171 BreakerState::Closed => g.failures = 0,
172 BreakerState::HalfOpen => {
173 g.probes_inflight = g.probes_inflight.saturating_sub(1);
174 g.probe_successes += 1;
175 if g.probe_successes >= self.cfg.success_threshold {
176 transition = self.set_state(&mut g, BreakerState::Closed);
177 }
178 }
179 BreakerState::Open => {}
180 }
181 drop(g);
182 self.emit(transition);
183 }
184
185 pub fn record_failure(&self) {
187 let mut transition = None;
188 let mut g = self.inner.lock().unwrap();
189 match g.state {
190 BreakerState::Closed => {
191 g.failures += 1;
192 if g.failures >= self.cfg.failure_threshold {
193 transition = self.set_state(&mut g, BreakerState::Open);
194 }
195 }
196 BreakerState::HalfOpen => {
197 transition = self.set_state(&mut g, BreakerState::Open);
198 }
199 BreakerState::Open => {}
201 }
202 drop(g);
203 self.emit(transition);
204 }
205
206 pub fn record_abort(&self) {
209 let mut g = self.inner.lock().unwrap();
210 if g.state == BreakerState::HalfOpen {
211 g.probes_inflight = g.probes_inflight.saturating_sub(1);
212 }
213 }
214
215 pub fn call<F, R, E>(&self, f: F) -> Result<R, PoolError>
217 where
218 F: FnOnce() -> Result<R, E>,
219 PoolError: From<E>,
220 {
221 self.before_call()?;
222 match f() {
223 Ok(r) => {
224 self.record_success();
225 Ok(r)
226 }
227 Err(e) => {
228 self.record_failure();
229 Err(PoolError::from(e))
230 }
231 }
232 }
233
234 pub fn reset(&self) {
236 let mut g = self.inner.lock().unwrap();
237 let transition = self.set_state(&mut g, BreakerState::Closed);
238 drop(g);
239 self.emit(transition);
240 }
241
242 fn refresh(&self, g: &mut Inner) -> Option<(BreakerState, BreakerState)> {
245 if g.state == BreakerState::Open
246 && self.clock.monotonic() - g.opened_at >= self.cfg.recovery_timeout
247 {
248 return self.set_state(g, BreakerState::HalfOpen);
249 }
250 None
251 }
252
253 fn set_state(&self, g: &mut Inner, new: BreakerState) -> Option<(BreakerState, BreakerState)> {
254 let old = g.state;
255 if old == new {
256 return None;
257 }
258 g.state = new;
259 g.failures = 0;
260 g.probes_inflight = 0;
261 g.probe_successes = 0;
262 if new == BreakerState::Open {
263 g.opened_at = self.clock.monotonic();
264 }
265 Some((old, new))
266 }
267
268 fn emit(&self, transition: Option<(BreakerState, BreakerState)>) {
269 if let (Some((old, new)), Some(cb)) = (transition, &self.on_change) {
271 cb(old, new);
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use std::sync::atomic::{AtomicU32, Ordering};
280
281 fn breaker(cfg: BreakerConfig, clock: Arc<ManualClock>) -> CircuitBreaker {
282 CircuitBreaker::new(cfg, Some(clock), None)
283 }
284
285 use crate::core::clock::ManualClock;
286
287 #[test]
288 fn wire_values_and_config() {
289 assert_eq!(BreakerState::Closed.as_str(), "closed");
290 assert_eq!(BreakerState::Open.as_str(), "open");
291 assert_eq!(BreakerState::HalfOpen.as_str(), "half_open");
292 assert_eq!(BreakerState::Open.to_string(), "open");
293 assert_eq!(format!("{:?}", BreakerState::Open), "Open");
294 let c = BreakerConfig::default();
295 assert_eq!(c.failure_threshold, 5);
296 assert_eq!(c.recovery_timeout, 30.0);
297 assert_eq!(c.half_open_max_calls, 1);
298 assert_eq!(c.success_threshold, 1);
299 assert!(c.validate().is_ok());
300 assert!(
301 BreakerConfig {
302 failure_threshold: 0,
303 ..c
304 }
305 .validate()
306 .is_err()
307 );
308 assert!(
309 BreakerConfig {
310 recovery_timeout: -1.0,
311 ..c
312 }
313 .validate()
314 .is_err()
315 );
316 assert_eq!(c, c.clone());
317 }
318
319 #[test]
320 fn trips_after_threshold_and_recovers() {
321 let clock = Arc::new(ManualClock::new(1000.0));
322 let cfg = BreakerConfig {
323 failure_threshold: 2,
324 recovery_timeout: 10.0,
325 half_open_max_calls: 1,
326 success_threshold: 1,
327 };
328 let b = breaker(cfg, clock.clone());
329 assert_eq!(b.state(), BreakerState::Closed);
330 b.before_call().unwrap();
331 b.record_failure();
332 assert_eq!(b.state(), BreakerState::Closed); b.before_call().unwrap();
334 b.record_failure(); assert_eq!(b.state(), BreakerState::Open);
336 let err = b.before_call().unwrap_err();
338 assert!(matches!(err, PoolError::CircuitOpen { retry_after } if retry_after > 0.0));
339 clock.advance(10.0);
341 assert_eq!(b.state(), BreakerState::HalfOpen);
342 b.before_call().unwrap();
343 b.record_success();
344 assert_eq!(b.state(), BreakerState::Closed);
345 }
346
347 #[test]
348 fn half_open_limits_probes_and_failure_reopens() {
349 let clock = Arc::new(ManualClock::new(0.0));
350 let changes = Arc::new(AtomicU32::new(0));
351 let changes2 = changes.clone();
352 let cfg = BreakerConfig {
353 failure_threshold: 1,
354 recovery_timeout: 5.0,
355 half_open_max_calls: 1,
356 success_threshold: 2,
357 };
358 let b = CircuitBreaker::new(
359 cfg,
360 Some(clock.clone()),
361 Some(Box::new(move |_, _| {
362 changes2.fetch_add(1, Ordering::SeqCst);
363 })),
364 );
365 b.before_call().unwrap();
366 b.record_failure(); clock.advance(5.0);
368 b.before_call().unwrap(); assert!(matches!(
371 b.before_call().unwrap_err(),
372 PoolError::CircuitOpen { retry_after } if retry_after == 0.0
373 ));
374 b.record_failure(); assert_eq!(b.state(), BreakerState::Open);
376 clock.advance(5.0);
378 b.before_call().unwrap();
379 b.record_success(); assert_eq!(b.state(), BreakerState::HalfOpen);
381 b.before_call().unwrap();
382 b.record_success(); assert_eq!(b.state(), BreakerState::Closed);
384 assert!(changes.load(Ordering::SeqCst) >= 3);
385 }
386
387 #[test]
388 fn abort_releases_probe_without_counting() {
389 let clock = Arc::new(ManualClock::new(0.0));
390 let cfg = BreakerConfig {
391 failure_threshold: 1,
392 recovery_timeout: 1.0,
393 half_open_max_calls: 1,
394 success_threshold: 1,
395 };
396 let b = breaker(cfg, clock.clone());
397 b.before_call().unwrap();
398 b.record_failure();
399 clock.advance(1.0);
400 b.before_call().unwrap(); b.record_abort(); b.before_call().unwrap();
404 b.record_success();
405 assert_eq!(b.state(), BreakerState::Closed);
406 b.record_abort();
408 }
409
410 #[test]
411 fn reset_and_late_events_and_call_wrapper() {
412 let clock = Arc::new(ManualClock::new(0.0));
413 let b = breaker(BreakerConfig::default(), clock.clone());
414 b.record_success();
416 let cfg = BreakerConfig {
418 failure_threshold: 1,
419 ..BreakerConfig::default()
420 };
421 let b2 = breaker(cfg, clock.clone());
422 b2.before_call().unwrap();
423 b2.record_failure(); b2.record_failure(); b2.record_success(); assert_eq!(b2.state(), BreakerState::Open);
427 b2.reset();
428 assert_eq!(b2.state(), BreakerState::Closed);
429 b2.reset(); let ok: Result<i32, PoolError> = b.call(|| Ok::<_, PoolError>(5));
433 assert_eq!(ok.unwrap(), 5);
434 let err: Result<i32, PoolError> =
435 b.call(|| Err::<i32, _>(PoolError::Timeout("t".into())));
436 assert!(err.is_err());
437 }
438
439 #[test]
440 fn defaults_to_system_clock_when_none() {
441 let b = CircuitBreaker::new(BreakerConfig::default(), None, None);
442 assert_eq!(b.state(), BreakerState::Closed);
443 b.before_call().unwrap();
444 b.record_success();
445 }
446
447 #[test]
448 fn validate_each_threshold_branch() {
449 for bad in [
450 BreakerConfig { failure_threshold: 0, ..Default::default() },
451 BreakerConfig { half_open_max_calls: 0, ..Default::default() },
452 BreakerConfig { success_threshold: 0, ..Default::default() },
453 ] {
454 assert!(bad.validate().is_err());
455 }
456 }
457
458 #[test]
459 fn call_short_circuits_when_open() {
460 let clock = Arc::new(ManualClock::new(0.0));
461 let cfg = BreakerConfig {
462 failure_threshold: 1,
463 recovery_timeout: 100.0,
464 ..BreakerConfig::default()
465 };
466 let b = breaker(cfg, clock);
467 b.before_call().unwrap();
468 b.record_failure();
469 let r: Result<i32, PoolError> = b.call(|| Ok::<_, PoolError>(1));
470 assert!(matches!(r, Err(PoolError::CircuitOpen { .. })));
471 }
472}