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 =
145 (self.cfg.recovery_timeout - (self.clock.monotonic() - g.opened_at)).max(0.0);
146 call_err = Some(PoolError::CircuitOpen { retry_after: wait });
147 }
148 BreakerState::HalfOpen => {
149 if g.probes_inflight >= self.cfg.half_open_max_calls {
150 call_err = Some(PoolError::CircuitOpen { retry_after: 0.0 });
151 } else {
152 g.probes_inflight += 1;
153 }
154 }
155 BreakerState::Closed => {}
156 }
157 drop(g);
158 self.emit(transition);
159 match call_err {
160 Some(e) => Err(e),
161 None => Ok(()),
162 }
163 }
164
165 pub fn record_success(&self) {
167 let mut transition = None;
168 let mut g = self.inner.lock().unwrap();
169 match g.state {
170 BreakerState::Closed => g.failures = 0,
171 BreakerState::HalfOpen => {
172 g.probes_inflight = g.probes_inflight.saturating_sub(1);
173 g.probe_successes += 1;
174 if g.probe_successes >= self.cfg.success_threshold {
175 transition = self.set_state(&mut g, BreakerState::Closed);
176 }
177 }
178 BreakerState::Open => {}
179 }
180 drop(g);
181 self.emit(transition);
182 }
183
184 pub fn record_failure(&self) {
186 let mut transition = None;
187 let mut g = self.inner.lock().unwrap();
188 match g.state {
189 BreakerState::Closed => {
190 g.failures += 1;
191 if g.failures >= self.cfg.failure_threshold {
192 transition = self.set_state(&mut g, BreakerState::Open);
193 }
194 }
195 BreakerState::HalfOpen => {
196 transition = self.set_state(&mut g, BreakerState::Open);
197 }
198 BreakerState::Open => {}
200 }
201 drop(g);
202 self.emit(transition);
203 }
204
205 pub fn record_abort(&self) {
208 let mut g = self.inner.lock().unwrap();
209 if g.state == BreakerState::HalfOpen {
210 g.probes_inflight = g.probes_inflight.saturating_sub(1);
211 }
212 }
213
214 pub fn call<F, R, E>(&self, f: F) -> Result<R, PoolError>
216 where
217 F: FnOnce() -> Result<R, E>,
218 PoolError: From<E>,
219 {
220 self.before_call()?;
221 match f() {
222 Ok(r) => {
223 self.record_success();
224 Ok(r)
225 }
226 Err(e) => {
227 self.record_failure();
228 Err(PoolError::from(e))
229 }
230 }
231 }
232
233 pub fn reset(&self) {
235 let mut g = self.inner.lock().unwrap();
236 let transition = self.set_state(&mut g, BreakerState::Closed);
237 drop(g);
238 self.emit(transition);
239 }
240
241 fn refresh(&self, g: &mut Inner) -> Option<(BreakerState, BreakerState)> {
244 if g.state == BreakerState::Open
245 && self.clock.monotonic() - g.opened_at >= self.cfg.recovery_timeout
246 {
247 return self.set_state(g, BreakerState::HalfOpen);
248 }
249 None
250 }
251
252 fn set_state(&self, g: &mut Inner, new: BreakerState) -> Option<(BreakerState, BreakerState)> {
253 let old = g.state;
254 if old == new {
255 return None;
256 }
257 g.state = new;
258 g.failures = 0;
259 g.probes_inflight = 0;
260 g.probe_successes = 0;
261 if new == BreakerState::Open {
262 g.opened_at = self.clock.monotonic();
263 }
264 Some((old, new))
265 }
266
267 fn emit(&self, transition: Option<(BreakerState, BreakerState)>) {
268 if let (Some((old, new)), Some(cb)) = (transition, &self.on_change) {
270 cb(old, new);
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use std::sync::atomic::{AtomicU32, Ordering};
279
280 fn breaker(cfg: BreakerConfig, clock: Arc<ManualClock>) -> CircuitBreaker {
281 CircuitBreaker::new(cfg, Some(clock), None)
282 }
283
284 use crate::core::clock::ManualClock;
285
286 #[test]
287 fn wire_values_and_config() {
288 assert_eq!(BreakerState::Closed.as_str(), "closed");
289 assert_eq!(BreakerState::Open.as_str(), "open");
290 assert_eq!(BreakerState::HalfOpen.as_str(), "half_open");
291 assert_eq!(BreakerState::Open.to_string(), "open");
292 assert_eq!(format!("{:?}", BreakerState::Open), "Open");
293 let c = BreakerConfig::default();
294 assert_eq!(c.failure_threshold, 5);
295 assert_eq!(c.recovery_timeout, 30.0);
296 assert_eq!(c.half_open_max_calls, 1);
297 assert_eq!(c.success_threshold, 1);
298 assert!(c.validate().is_ok());
299 assert!(
300 BreakerConfig {
301 failure_threshold: 0,
302 ..c
303 }
304 .validate()
305 .is_err()
306 );
307 assert!(
308 BreakerConfig {
309 recovery_timeout: -1.0,
310 ..c
311 }
312 .validate()
313 .is_err()
314 );
315 assert_eq!(c, c.clone());
316 }
317
318 #[test]
319 fn trips_after_threshold_and_recovers() {
320 let clock = Arc::new(ManualClock::new(1000.0));
321 let cfg = BreakerConfig {
322 failure_threshold: 2,
323 recovery_timeout: 10.0,
324 half_open_max_calls: 1,
325 success_threshold: 1,
326 };
327 let b = breaker(cfg, clock.clone());
328 assert_eq!(b.state(), BreakerState::Closed);
329 b.before_call().unwrap();
330 b.record_failure();
331 assert_eq!(b.state(), BreakerState::Closed); b.before_call().unwrap();
333 b.record_failure(); assert_eq!(b.state(), BreakerState::Open);
335 let err = b.before_call().unwrap_err();
337 assert!(matches!(err, PoolError::CircuitOpen { retry_after } if retry_after > 0.0));
338 clock.advance(10.0);
340 assert_eq!(b.state(), BreakerState::HalfOpen);
341 b.before_call().unwrap();
342 b.record_success();
343 assert_eq!(b.state(), BreakerState::Closed);
344 }
345
346 #[test]
347 fn half_open_limits_probes_and_failure_reopens() {
348 let clock = Arc::new(ManualClock::new(0.0));
349 let changes = Arc::new(AtomicU32::new(0));
350 let changes2 = changes.clone();
351 let cfg = BreakerConfig {
352 failure_threshold: 1,
353 recovery_timeout: 5.0,
354 half_open_max_calls: 1,
355 success_threshold: 2,
356 };
357 let b = CircuitBreaker::new(
358 cfg,
359 Some(clock.clone()),
360 Some(Box::new(move |_, _| {
361 changes2.fetch_add(1, Ordering::SeqCst);
362 })),
363 );
364 b.before_call().unwrap();
365 b.record_failure(); clock.advance(5.0);
367 b.before_call().unwrap(); assert!(matches!(
370 b.before_call().unwrap_err(),
371 PoolError::CircuitOpen { retry_after } if retry_after == 0.0
372 ));
373 b.record_failure(); assert_eq!(b.state(), BreakerState::Open);
375 clock.advance(5.0);
377 b.before_call().unwrap();
378 b.record_success(); assert_eq!(b.state(), BreakerState::HalfOpen);
380 b.before_call().unwrap();
381 b.record_success(); assert_eq!(b.state(), BreakerState::Closed);
383 assert!(changes.load(Ordering::SeqCst) >= 3);
384 }
385
386 #[test]
387 fn abort_releases_probe_without_counting() {
388 let clock = Arc::new(ManualClock::new(0.0));
389 let cfg = BreakerConfig {
390 failure_threshold: 1,
391 recovery_timeout: 1.0,
392 half_open_max_calls: 1,
393 success_threshold: 1,
394 };
395 let b = breaker(cfg, clock.clone());
396 b.before_call().unwrap();
397 b.record_failure();
398 clock.advance(1.0);
399 b.before_call().unwrap(); b.record_abort(); b.before_call().unwrap();
403 b.record_success();
404 assert_eq!(b.state(), BreakerState::Closed);
405 b.record_abort();
407 }
408
409 #[test]
410 fn reset_and_late_events_and_call_wrapper() {
411 let clock = Arc::new(ManualClock::new(0.0));
412 let b = breaker(BreakerConfig::default(), clock.clone());
413 b.record_success();
415 let cfg = BreakerConfig {
417 failure_threshold: 1,
418 ..BreakerConfig::default()
419 };
420 let b2 = breaker(cfg, clock.clone());
421 b2.before_call().unwrap();
422 b2.record_failure(); b2.record_failure(); b2.record_success(); assert_eq!(b2.state(), BreakerState::Open);
426 b2.reset();
427 assert_eq!(b2.state(), BreakerState::Closed);
428 b2.reset(); let ok: Result<i32, PoolError> = b.call(|| Ok::<_, PoolError>(5));
432 assert_eq!(ok.unwrap(), 5);
433 let err: Result<i32, PoolError> = b.call(|| Err::<i32, _>(PoolError::Timeout("t".into())));
434 assert!(err.is_err());
435 }
436
437 #[test]
438 fn defaults_to_system_clock_when_none() {
439 let b = CircuitBreaker::new(BreakerConfig::default(), None, None);
440 assert_eq!(b.state(), BreakerState::Closed);
441 b.before_call().unwrap();
442 b.record_success();
443 }
444
445 #[test]
446 fn validate_each_threshold_branch() {
447 for bad in [
448 BreakerConfig {
449 failure_threshold: 0,
450 ..Default::default()
451 },
452 BreakerConfig {
453 half_open_max_calls: 0,
454 ..Default::default()
455 },
456 BreakerConfig {
457 success_threshold: 0,
458 ..Default::default()
459 },
460 ] {
461 assert!(bad.validate().is_err());
462 }
463 }
464
465 #[test]
466 fn call_short_circuits_when_open() {
467 let clock = Arc::new(ManualClock::new(0.0));
468 let cfg = BreakerConfig {
469 failure_threshold: 1,
470 recovery_timeout: 100.0,
471 ..BreakerConfig::default()
472 };
473 let b = breaker(cfg, clock);
474 b.before_call().unwrap();
475 b.record_failure();
476 let r: Result<i32, PoolError> = b.call(|| Ok::<_, PoolError>(1));
477 assert!(matches!(r, Err(PoolError::CircuitOpen { .. })));
478 }
479}