1use std::{
2 collections::VecDeque,
3 convert::Infallible,
4 fmt,
5 future::Future,
6 sync::{
7 atomic::{AtomicU64, Ordering},
8 Arc, Mutex,
9 },
10 time::{Duration, Instant},
11};
12
13#[derive(Debug, Clone, Copy)]
15pub struct CircuitBreakerConfig {
16 pub max_failures: u32,
17 pub reset_timeout: Duration,
18 pub half_open_max_calls: u32,
19 pub policy: CircuitBreakerPolicy,
20}
21
22impl CircuitBreakerConfig {
23 pub fn new(max_failures: u32, reset_timeout: Duration) -> Self {
25 assert!(
26 max_failures > 0,
27 "maximum failures must be greater than zero"
28 );
29 assert!(
30 !reset_timeout.is_zero(),
31 "reset timeout must be greater than zero"
32 );
33
34 Self {
35 max_failures,
36 reset_timeout,
37 half_open_max_calls: 1,
38 policy: CircuitBreakerPolicy::Consecutive,
39 }
40 }
41
42 pub fn rolling(config: RollingCircuitBreakerConfig) -> Self {
45 Self {
46 max_failures: 5,
47 reset_timeout: Duration::from_secs(30),
48 half_open_max_calls: 1,
49 policy: CircuitBreakerPolicy::Rolling(config),
50 }
51 }
52
53 pub fn with_half_open_max_calls(mut self, calls: u32) -> Self {
54 assert!(calls > 0, "half-open calls must be greater than zero");
55 self.half_open_max_calls = calls;
56 self
57 }
58
59 pub fn with_policy(mut self, policy: CircuitBreakerPolicy) -> Self {
60 self.policy = policy;
61 self
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
67pub enum CircuitBreakerPolicy {
68 Consecutive,
70 Rolling(RollingCircuitBreakerConfig),
72}
73
74#[derive(Debug, Clone, Copy)]
76pub struct RollingCircuitBreakerConfig {
77 pub window: Duration,
78 pub buckets: usize,
79 pub sensitivity: f64,
80 pub minimum_requests: u64,
81 pub probe_interval: u64,
82 pub random_seed: u64,
83}
84
85impl RollingCircuitBreakerConfig {
86 pub fn new() -> Self {
89 Self {
90 window: Duration::from_secs(5),
91 buckets: 40,
92 sensitivity: 1.5,
93 minimum_requests: 5,
94 probe_interval: 100,
95 random_seed: 0,
96 }
97 }
98
99 pub fn with_window(mut self, window: Duration, buckets: usize) -> Self {
100 assert!(
101 !window.is_zero(),
102 "rolling window must be greater than zero"
103 );
104 assert!(
105 buckets > 0,
106 "rolling bucket count must be greater than zero"
107 );
108 assert!(
109 window.as_nanos() >= buckets as u128,
110 "rolling buckets must have non-zero width"
111 );
112 self.window = window;
113 self.buckets = buckets;
114 self
115 }
116
117 pub fn with_sensitivity(mut self, sensitivity: f64) -> Self {
118 assert!(
119 sensitivity.is_finite() && sensitivity > 0.0,
120 "rolling sensitivity must be finite and greater than zero"
121 );
122 self.sensitivity = sensitivity;
123 self
124 }
125
126 pub fn with_minimum_requests(mut self, requests: u64) -> Self {
127 self.minimum_requests = requests;
128 self
129 }
130
131 pub fn with_probe_interval(mut self, requests: u64) -> Self {
133 assert!(requests > 0, "probe interval must be greater than zero");
134 self.probe_interval = requests;
135 self
136 }
137
138 pub fn with_random_seed(mut self, seed: u64) -> Self {
140 self.random_seed = seed.max(1);
141 self
142 }
143
144 fn validate(self) {
145 assert!(
146 !self.window.is_zero(),
147 "rolling window must be greater than zero"
148 );
149 assert!(
150 self.buckets > 0,
151 "rolling bucket count must be greater than zero"
152 );
153 assert!(
154 self.window.as_nanos() >= self.buckets as u128,
155 "rolling buckets must have non-zero width"
156 );
157 assert!(
158 self.sensitivity.is_finite() && self.sensitivity > 0.0,
159 "rolling sensitivity must be finite and greater than zero"
160 );
161 assert!(
162 self.probe_interval > 0,
163 "probe interval must be greater than zero"
164 );
165 }
166}
167
168impl Default for RollingCircuitBreakerConfig {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum BreakerState {
177 Closed,
178 Open,
179 HalfOpen,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum CircuitOutcome {
185 Success,
186 Failure,
187 Cancellation,
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq)]
192pub struct CircuitBreakerSnapshot {
193 pub accepted: u64,
194 pub total: u64,
195 pub drop_ratio: f64,
196 pub successes: u64,
197 pub failures: u64,
198 pub rejections: u64,
199 pub cancellations: u64,
200}
201
202enum ConsecutiveState {
203 Closed { consecutive_failures: u32 },
204 Open { opened_at: Instant },
205 HalfOpen { attempts: u32, successes: u32 },
206}
207
208impl ConsecutiveState {
209 fn status(&self) -> BreakerState {
210 match self {
211 Self::Closed { .. } => BreakerState::Closed,
212 Self::Open { .. } => BreakerState::Open,
213 Self::HalfOpen { .. } => BreakerState::HalfOpen,
214 }
215 }
216}
217
218enum BreakerMode {
219 Consecutive(Mutex<ConsecutiveState>),
220 Rolling(RollingBreaker),
221}
222
223#[derive(Default)]
224struct OutcomeCounters {
225 successes: AtomicU64,
226 failures: AtomicU64,
227 rejections: AtomicU64,
228 cancellations: AtomicU64,
229}
230
231pub struct CircuitBreaker {
233 config: CircuitBreakerConfig,
234 mode: BreakerMode,
235 counters: OutcomeCounters,
236}
237
238impl CircuitBreaker {
239 pub fn new(config: CircuitBreakerConfig) -> Self {
240 let mode = match config.policy {
241 CircuitBreakerPolicy::Consecutive => {
242 BreakerMode::Consecutive(Mutex::new(ConsecutiveState::Closed {
243 consecutive_failures: 0,
244 }))
245 }
246 CircuitBreakerPolicy::Rolling(rolling) => {
247 rolling.validate();
248 BreakerMode::Rolling(RollingBreaker::new(rolling))
249 }
250 };
251 Self {
252 config,
253 mode,
254 counters: OutcomeCounters::default(),
255 }
256 }
257
258 pub fn state(&self) -> BreakerState {
259 match &self.mode {
260 BreakerMode::Consecutive(state) => state
261 .lock()
262 .expect("circuit breaker state lock poisoned")
263 .status(),
264 BreakerMode::Rolling(rolling) => {
265 if rolling.history().2 > 0.0 {
266 BreakerState::Open
267 } else {
268 BreakerState::Closed
269 }
270 }
271 }
272 }
273
274 pub fn snapshot(&self) -> CircuitBreakerSnapshot {
275 let (accepted, total, drop_ratio) = match &self.mode {
276 BreakerMode::Consecutive(_) => (0, 0, 0.0),
277 BreakerMode::Rolling(rolling) => rolling.history(),
278 };
279 CircuitBreakerSnapshot {
280 accepted,
281 total,
282 drop_ratio,
283 successes: self.counters.successes.load(Ordering::Relaxed),
284 failures: self.counters.failures.load(Ordering::Relaxed),
285 rejections: self.counters.rejections.load(Ordering::Relaxed),
286 cancellations: self.counters.cancellations.load(Ordering::Relaxed),
287 }
288 }
289
290 pub fn acquire(self: &Arc<Self>) -> Option<CircuitBreakerPermit> {
293 if self.before_call::<Infallible>().is_err() {
294 return None;
295 }
296 Some(CircuitBreakerPermit {
297 breaker: Arc::clone(self),
298 finished: false,
299 })
300 }
301
302 pub fn execute<T, E, F>(&self, operation: F) -> Result<T, CircuitBreakerError<E>>
303 where
304 F: FnOnce() -> Result<T, E>,
305 {
306 self.execute_with_accept(operation, Result::is_ok)
307 }
308
309 pub fn execute_with_accept<T, E, F, A>(
310 &self,
311 operation: F,
312 acceptable: A,
313 ) -> Result<T, CircuitBreakerError<E>>
314 where
315 F: FnOnce() -> Result<T, E>,
316 A: FnOnce(&Result<T, E>) -> bool,
317 {
318 self.execute_with_outcome(operation, |result| {
319 if acceptable(result) {
320 CircuitOutcome::Success
321 } else {
322 CircuitOutcome::Failure
323 }
324 })
325 }
326
327 pub fn execute_with_outcome<T, E, F, A>(
328 &self,
329 operation: F,
330 outcome: A,
331 ) -> Result<T, CircuitBreakerError<E>>
332 where
333 F: FnOnce() -> Result<T, E>,
334 A: FnOnce(&Result<T, E>) -> CircuitOutcome,
335 {
336 self.before_call()?;
337 let mut completion = CompletionGuard::new(self);
338 let result = operation();
339 completion.finish(outcome(&result));
340 result.map_err(CircuitBreakerError::Operation)
341 }
342
343 pub async fn execute_async<T, E, F, Fut>(
344 &self,
345 operation: F,
346 ) -> Result<T, CircuitBreakerError<E>>
347 where
348 F: FnOnce() -> Fut,
349 Fut: Future<Output = Result<T, E>>,
350 {
351 self.execute_async_with_accept(operation, Result::is_ok)
352 .await
353 }
354
355 pub async fn execute_async_with_accept<T, E, F, Fut, A>(
356 &self,
357 operation: F,
358 acceptable: A,
359 ) -> Result<T, CircuitBreakerError<E>>
360 where
361 F: FnOnce() -> Fut,
362 Fut: Future<Output = Result<T, E>>,
363 A: FnOnce(&Result<T, E>) -> bool,
364 {
365 self.execute_async_with_outcome(operation, |result| {
366 if acceptable(result) {
367 CircuitOutcome::Success
368 } else {
369 CircuitOutcome::Failure
370 }
371 })
372 .await
373 }
374
375 pub async fn execute_async_with_outcome<T, E, F, Fut, A>(
376 &self,
377 operation: F,
378 outcome: A,
379 ) -> Result<T, CircuitBreakerError<E>>
380 where
381 F: FnOnce() -> Fut,
382 Fut: Future<Output = Result<T, E>>,
383 A: FnOnce(&Result<T, E>) -> CircuitOutcome,
384 {
385 self.before_call()?;
386 let mut completion = CompletionGuard::new(self);
387 let result = operation().await;
388 completion.finish(outcome(&result));
389 result.map_err(CircuitBreakerError::Operation)
390 }
391
392 fn before_call<E>(&self) -> Result<(), CircuitBreakerError<E>> {
393 let admitted = match &self.mode {
394 BreakerMode::Consecutive(state) => self.before_consecutive(state),
395 BreakerMode::Rolling(rolling) => !rolling.should_drop(),
396 };
397 if admitted {
398 Ok(())
399 } else {
400 self.counters.rejections.fetch_add(1, Ordering::Relaxed);
401 Err(CircuitBreakerError::Open)
402 }
403 }
404
405 fn before_consecutive(&self, state: &Mutex<ConsecutiveState>) -> bool {
406 let mut state = state.lock().expect("circuit breaker state lock poisoned");
407 if let ConsecutiveState::Open { opened_at } = *state {
408 if opened_at.elapsed() < self.config.reset_timeout {
409 return false;
410 }
411 *state = ConsecutiveState::HalfOpen {
412 attempts: 0,
413 successes: 0,
414 };
415 }
416 if let ConsecutiveState::HalfOpen { attempts, .. } = &mut *state {
417 if *attempts >= self.config.half_open_max_calls {
418 return false;
419 }
420 *attempts += 1;
421 }
422 true
423 }
424
425 fn record_outcome(&self, outcome: CircuitOutcome) {
426 match outcome {
427 CircuitOutcome::Success => {
428 self.counters.successes.fetch_add(1, Ordering::Relaxed);
429 }
430 CircuitOutcome::Failure => {
431 self.counters.failures.fetch_add(1, Ordering::Relaxed);
432 }
433 CircuitOutcome::Cancellation => {
434 self.counters.cancellations.fetch_add(1, Ordering::Relaxed);
435 }
436 }
437 match &self.mode {
438 BreakerMode::Consecutive(state) => self.record_consecutive(state, outcome),
439 BreakerMode::Rolling(rolling) => rolling.record(outcome),
440 }
441 }
442
443 fn record_consecutive(&self, state: &Mutex<ConsecutiveState>, outcome: CircuitOutcome) {
444 let mut state = state.lock().expect("circuit breaker state lock poisoned");
445 match outcome {
446 CircuitOutcome::Success => match &mut *state {
447 ConsecutiveState::Closed {
448 consecutive_failures,
449 } => *consecutive_failures = 0,
450 ConsecutiveState::Open { .. } => {}
451 ConsecutiveState::HalfOpen { successes, .. } => {
452 *successes += 1;
453 if *successes == self.config.half_open_max_calls {
454 *state = ConsecutiveState::Closed {
455 consecutive_failures: 0,
456 };
457 }
458 }
459 },
460 CircuitOutcome::Failure => match &mut *state {
461 ConsecutiveState::Closed {
462 consecutive_failures,
463 } => {
464 *consecutive_failures += 1;
465 if *consecutive_failures >= self.config.max_failures {
466 *state = ConsecutiveState::Open {
467 opened_at: Instant::now(),
468 };
469 }
470 }
471 ConsecutiveState::Open { .. } => {}
472 ConsecutiveState::HalfOpen { .. } => {
473 *state = ConsecutiveState::Open {
474 opened_at: Instant::now(),
475 };
476 }
477 },
478 CircuitOutcome::Cancellation => {
479 if let ConsecutiveState::HalfOpen { attempts, .. } = &mut *state {
480 *attempts = attempts.saturating_sub(1);
481 }
482 }
483 }
484 }
485}
486
487struct CompletionGuard<'a> {
488 breaker: &'a CircuitBreaker,
489 finished: bool,
490}
491
492impl<'a> CompletionGuard<'a> {
493 fn new(breaker: &'a CircuitBreaker) -> Self {
494 Self {
495 breaker,
496 finished: false,
497 }
498 }
499
500 fn finish(&mut self, outcome: CircuitOutcome) {
501 self.breaker.record_outcome(outcome);
502 self.finished = true;
503 }
504}
505
506impl Drop for CompletionGuard<'_> {
507 fn drop(&mut self) {
508 if !self.finished {
509 self.breaker.record_outcome(CircuitOutcome::Cancellation);
510 }
511 }
512}
513
514struct RollingBreaker {
515 config: RollingCircuitBreakerConfig,
516 bucket_width: Duration,
517 state: Mutex<RollingState>,
518 random: AtomicU64,
519 drop_sequence: AtomicU64,
520}
521
522impl RollingBreaker {
523 fn new(config: RollingCircuitBreakerConfig) -> Self {
524 static NEXT_SEED: AtomicU64 = AtomicU64::new(0x9e37_79b9_7f4a_7c15);
525 let bucket_width = Duration::from_nanos(
526 u64::try_from(config.window.as_nanos() / config.buckets as u128)
527 .unwrap_or(u64::MAX)
528 .max(1),
529 );
530 Self {
531 config,
532 bucket_width,
533 state: Mutex::new(RollingState {
534 current_started: Instant::now(),
535 buckets: VecDeque::from([OutcomeBucket::default()]),
536 }),
537 random: AtomicU64::new(if config.random_seed == 0 {
538 NEXT_SEED.fetch_add(0x9e37_79b9_7f4a_7c15, Ordering::Relaxed)
539 } else {
540 config.random_seed
541 }),
542 drop_sequence: AtomicU64::new(0),
543 }
544 }
545
546 fn should_drop(&self) -> bool {
547 let (_, _, ratio) = self.history();
548 if ratio <= 0.0 {
549 return false;
550 }
551 let sequence = self.drop_sequence.fetch_add(1, Ordering::Relaxed) + 1;
552 if sequence.is_multiple_of(self.config.probe_interval) {
553 return false;
554 }
555 self.random_unit() < ratio
556 }
557
558 fn record(&self, outcome: CircuitOutcome) {
559 if outcome == CircuitOutcome::Cancellation {
560 return;
561 }
562 let mut state = self.state.lock().expect("rolling breaker mutex poisoned");
563 self.rotate(&mut state, Instant::now());
564 let bucket = state
565 .buckets
566 .back_mut()
567 .expect("rolling breaker always has a current bucket");
568 bucket.total = bucket.total.saturating_add(1);
569 if outcome == CircuitOutcome::Success {
570 bucket.accepted = bucket.accepted.saturating_add(1);
571 }
572 }
573
574 fn history(&self) -> (u64, u64, f64) {
575 let mut state = self.state.lock().expect("rolling breaker mutex poisoned");
576 self.rotate(&mut state, Instant::now());
577 let (accepted, total) = state.buckets.iter().fold((0_u64, 0_u64), |sum, bucket| {
578 (
579 sum.0.saturating_add(bucket.accepted),
580 sum.1.saturating_add(bucket.total),
581 )
582 });
583 let unprotected = total.saturating_sub(self.config.minimum_requests) as f64;
584 let weighted_accepted = self.config.sensitivity * accepted as f64;
585 let drop_ratio = ((unprotected - weighted_accepted) / (total as f64 + 1.0)).max(0.0);
586 (accepted, total, drop_ratio)
587 }
588
589 fn rotate(&self, state: &mut RollingState, now: Instant) {
590 let elapsed = now.saturating_duration_since(state.current_started);
591 let elapsed_buckets = usize::try_from(elapsed.as_nanos() / self.bucket_width.as_nanos())
592 .unwrap_or(self.config.buckets);
593 if elapsed_buckets == 0 {
594 return;
595 }
596 if elapsed_buckets >= self.config.buckets {
597 state.buckets.clear();
598 state.buckets.push_back(OutcomeBucket::default());
599 state.current_started = now;
600 return;
601 }
602 for _ in 0..elapsed_buckets {
603 state.buckets.push_back(OutcomeBucket::default());
604 if state.buckets.len() > self.config.buckets {
605 state.buckets.pop_front();
606 }
607 }
608 state.current_started += self.bucket_width * elapsed_buckets as u32;
609 }
610
611 fn random_unit(&self) -> f64 {
612 let mut current = self.random.load(Ordering::Relaxed);
613 loop {
614 let mut next = current;
615 next ^= next << 13;
616 next ^= next >> 7;
617 next ^= next << 17;
618 next = next.max(1);
619 match self.random.compare_exchange_weak(
620 current,
621 next,
622 Ordering::Relaxed,
623 Ordering::Relaxed,
624 ) {
625 Ok(_) => return ((next >> 11) as f64) * (1.0 / 9_007_199_254_740_992.0),
626 Err(observed) => current = observed,
627 }
628 }
629 }
630}
631
632struct RollingState {
633 current_started: Instant,
634 buckets: VecDeque<OutcomeBucket>,
635}
636
637#[derive(Default)]
638struct OutcomeBucket {
639 accepted: u64,
640 total: u64,
641}
642
643pub struct CircuitBreakerPermit {
645 breaker: Arc<CircuitBreaker>,
646 finished: bool,
647}
648
649impl CircuitBreakerPermit {
650 pub fn finish(self, acceptable: bool) {
651 self.finish_with_outcome(if acceptable {
652 CircuitOutcome::Success
653 } else {
654 CircuitOutcome::Failure
655 });
656 }
657
658 pub fn finish_with_outcome(mut self, outcome: CircuitOutcome) {
659 self.breaker.record_outcome(outcome);
660 self.finished = true;
661 }
662}
663
664impl Drop for CircuitBreakerPermit {
665 fn drop(&mut self) {
666 if !self.finished {
667 self.breaker.record_outcome(CircuitOutcome::Cancellation);
668 }
669 }
670}
671
672#[derive(Debug, PartialEq, Eq)]
674pub enum CircuitBreakerError<E> {
675 Open,
676 Operation(E),
677}
678
679impl<E: fmt::Display> fmt::Display for CircuitBreakerError<E> {
680 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
681 match self {
682 Self::Open => formatter.write_str("circuit breaker is open"),
683 Self::Operation(error) => write!(formatter, "protected operation failed: {error}"),
684 }
685 }
686}
687
688impl<E: std::error::Error + 'static> std::error::Error for CircuitBreakerError<E> {
689 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
690 match self {
691 Self::Open => None,
692 Self::Operation(error) => Some(error),
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use std::thread;
701
702 #[test]
703 fn opens_after_the_configured_failure_threshold() {
704 let breaker = CircuitBreaker::new(
705 CircuitBreakerConfig::new(2, Duration::from_secs(1))
706 .with_policy(CircuitBreakerPolicy::Consecutive),
707 );
708 assert_eq!(
709 breaker.execute(|| Err::<(), _>("first")),
710 Err(CircuitBreakerError::Operation("first"))
711 );
712 assert_eq!(
713 breaker.execute(|| Err::<(), _>("second")),
714 Err(CircuitBreakerError::Operation("second"))
715 );
716 assert_eq!(breaker.state(), BreakerState::Open);
717 assert_eq!(
718 breaker.execute(|| Ok::<_, ()>(())),
719 Err(CircuitBreakerError::Open)
720 );
721 }
722
723 #[test]
724 fn closes_after_a_successful_half_open_probe() {
725 let breaker = CircuitBreaker::new(CircuitBreakerConfig::new(1, Duration::from_millis(5)));
726 let _ = breaker.execute(|| Err::<(), _>("unavailable"));
727 thread::sleep(Duration::from_millis(10));
728 assert_eq!(breaker.execute(|| Ok::<_, ()>(42)), Ok(42));
729 assert_eq!(breaker.state(), BreakerState::Closed);
730 }
731
732 #[tokio::test]
733 async fn async_acceptance_can_reject_a_successful_result() {
734 let breaker = CircuitBreaker::new(CircuitBreakerConfig::new(1, Duration::from_secs(1)));
735 let response = breaker
736 .execute_async_with_accept(
737 || async { Ok::<_, ()>(503) },
738 |result| result.as_ref().is_ok_and(|status| *status < 500),
739 )
740 .await;
741 assert_eq!(response, Ok(503));
742 assert_eq!(breaker.state(), BreakerState::Open);
743 }
744
745 #[tokio::test]
746 async fn cancelled_async_call_is_neutral_to_rolling_health() {
747 let breaker = Arc::new(CircuitBreaker::new(CircuitBreakerConfig::rolling(
748 RollingCircuitBreakerConfig::new(),
749 )));
750 let task_breaker = Arc::clone(&breaker);
751 let task = tokio::spawn(async move {
752 task_breaker
753 .execute_async(std::future::pending::<Result<(), ()>>)
754 .await
755 });
756 tokio::task::yield_now().await;
757 task.abort();
758 assert!(task.await.unwrap_err().is_cancelled());
759
760 let snapshot = breaker.snapshot();
761 assert_eq!(snapshot.cancellations, 1);
762 assert_eq!((snapshot.accepted, snapshot.total), (0, 0));
763 assert_eq!(breaker.state(), BreakerState::Closed);
764 }
765
766 #[test]
767 fn dropped_transport_permit_records_cancellation_without_healing() {
768 let breaker = Arc::new(CircuitBreaker::new(
769 CircuitBreakerConfig::new(1, Duration::from_millis(5)).with_half_open_max_calls(2),
770 ));
771 breaker.acquire().unwrap().finish(false);
772 thread::sleep(Duration::from_millis(10));
773 drop(breaker.acquire().unwrap());
774 assert_eq!(breaker.state(), BreakerState::HalfOpen);
775 assert_eq!(breaker.snapshot().cancellations, 1);
776 breaker.acquire().unwrap().finish(true);
777 assert_eq!(breaker.state(), BreakerState::HalfOpen);
778 }
779
780 #[test]
781 fn rolling_fault_pattern_uses_recent_accepted_and_total_counts() {
782 let breaker = Arc::new(CircuitBreaker::new(CircuitBreakerConfig::rolling(
783 RollingCircuitBreakerConfig::new()
784 .with_window(Duration::from_millis(40), 4)
785 .with_minimum_requests(2)
786 .with_random_seed(7),
787 )));
788 for _ in 0..3 {
789 breaker.acquire().unwrap().finish(false);
790 }
791 let snapshot = breaker.snapshot();
792 assert_eq!((snapshot.accepted, snapshot.total), (0, 3));
793 assert!(snapshot.drop_ratio > 0.0);
794 assert_eq!(breaker.state(), BreakerState::Open);
795
796 thread::sleep(Duration::from_millis(50));
797 assert_eq!(breaker.state(), BreakerState::Closed);
798 assert_eq!(breaker.snapshot().total, 0);
799 }
800
801 #[test]
802 fn rolling_breaker_guarantees_bounded_probe_traffic() {
803 let breaker = Arc::new(CircuitBreaker::new(CircuitBreakerConfig::rolling(
804 RollingCircuitBreakerConfig::new()
805 .with_sensitivity(0.000_001)
806 .with_minimum_requests(0)
807 .with_probe_interval(4)
808 .with_random_seed(1),
809 )));
810 for _ in 0..20 {
811 if let Some(permit) = breaker.acquire() {
812 permit.finish(false);
813 }
814 }
815 assert!(breaker.snapshot().rejections > 0);
816 let admitted = (0..4).filter(|_| breaker.acquire().is_some()).count();
817 assert!(admitted >= 1);
818 }
819
820 #[test]
821 fn rolling_accounting_is_exact_under_concurrent_completion() {
822 let breaker = Arc::new(CircuitBreaker::new(CircuitBreakerConfig::rolling(
823 RollingCircuitBreakerConfig::new().with_minimum_requests(1_000),
824 )));
825 let workers: Vec<_> = (0..32)
826 .map(|_| {
827 let breaker = Arc::clone(&breaker);
828 thread::spawn(move || breaker.acquire().unwrap().finish(true))
829 })
830 .collect();
831 for worker in workers {
832 worker.join().unwrap();
833 }
834 let snapshot = breaker.snapshot();
835 assert_eq!((snapshot.accepted, snapshot.total), (32, 32));
836 assert_eq!(snapshot.successes, 32);
837 }
838}