1use std::collections::{HashMap, VecDeque};
24
25#[derive(Clone, Debug, PartialEq)]
34pub enum CircuitBreakerState {
35 Closed,
37 Open {
39 opened_at: u64,
41 },
42 HalfOpen {
44 probe_start: u64,
46 },
47}
48
49pub type PeerCircuitState = CircuitBreakerState;
51
52#[derive(Clone, Debug)]
58pub struct CircuitConfig {
59 pub failure_threshold: u32,
61 pub success_threshold: u32,
63 pub timeout_ms: u64,
65 pub half_open_max_calls: u32,
67 pub slow_call_threshold_ms: u64,
70 pub window_size: u32,
73}
74
75impl Default for CircuitConfig {
76 fn default() -> Self {
77 Self {
78 failure_threshold: 5,
79 success_threshold: 2,
80 timeout_ms: 30_000,
81 half_open_max_calls: 1,
82 slow_call_threshold_ms: 5_000,
83 window_size: 10,
84 }
85 }
86}
87
88#[derive(Clone, Debug)]
95pub enum CallResult {
96 Success {
98 duration_ms: u64,
100 },
101 Failure {
103 duration_ms: u64,
105 reason: String,
107 },
108 Timeout {
110 duration_ms: u64,
112 },
113}
114
115impl CallResult {
116 pub fn is_failure(&self, slow_call_threshold_ms: u64) -> bool {
122 match self {
123 Self::Failure { .. } | Self::Timeout { .. } => true,
124 Self::Success { duration_ms } => *duration_ms >= slow_call_threshold_ms,
125 }
126 }
127}
128
129#[derive(Clone, Debug, Default)]
135pub struct CircuitStats {
136 pub state: String,
138 pub failure_count: u32,
140 pub success_count: u32,
142 pub consecutive_failures: u32,
144 pub consecutive_successes: u32,
146 pub total_calls: u64,
148 pub rejected_calls: u64,
150 pub last_opened_at: Option<u64>,
152 pub last_closed_at: Option<u64>,
154}
155
156#[derive(Clone, Debug)]
167pub struct PeerCircuitBreaker {
168 pub peer_id: String,
170 pub config: CircuitConfig,
172 pub state: CircuitBreakerState,
174 pub window: VecDeque<bool>,
177 pub consecutive_failures: u32,
179 pub consecutive_successes: u32,
181 pub total_calls: u64,
183 pub rejected_calls: u64,
185 pub last_opened_at: Option<u64>,
187 pub last_closed_at: Option<u64>,
189 pub half_open_calls: u32,
191}
192
193impl PeerCircuitBreaker {
194 pub fn new(peer_id: String, config: CircuitConfig) -> Self {
196 Self {
197 peer_id,
198 config,
199 state: CircuitBreakerState::Closed,
200 window: VecDeque::new(),
201 consecutive_failures: 0,
202 consecutive_successes: 0,
203 total_calls: 0,
204 rejected_calls: 0,
205 last_opened_at: None,
206 last_closed_at: None,
207 half_open_calls: 0,
208 }
209 }
210
211 pub fn can_call(&mut self, now: u64) -> bool {
218 match &self.state.clone() {
219 CircuitBreakerState::Closed => true,
220 CircuitBreakerState::Open { opened_at } => {
221 if now >= opened_at + self.config.timeout_ms {
222 self.state = CircuitBreakerState::HalfOpen { probe_start: now };
224 self.half_open_calls = 0;
225 self.consecutive_successes = 0;
226 true
227 } else {
228 false
229 }
230 }
231 CircuitBreakerState::HalfOpen { .. } => {
232 self.half_open_calls < self.config.half_open_max_calls
233 }
234 }
235 }
236
237 pub fn record_result(&mut self, result: CallResult, now: u64) {
243 let is_failure = result.is_failure(self.config.slow_call_threshold_ms);
244 self.push_window(!is_failure);
245
246 match self.state.clone() {
247 CircuitBreakerState::Closed => {
248 self.record_closed(is_failure, now);
249 }
250 CircuitBreakerState::HalfOpen { .. } => {
251 self.record_half_open(is_failure, now);
252 }
253 CircuitBreakerState::Open { .. } => {
254 }
257 }
258 }
259
260 fn record_closed(&mut self, is_failure: bool, now: u64) {
262 if is_failure {
263 self.consecutive_failures += 1;
264 self.consecutive_successes = 0;
265 if self.consecutive_failures >= self.config.failure_threshold {
266 self.trip_open(now);
267 }
268 } else {
269 self.consecutive_failures = 0;
270 self.consecutive_successes += 1;
271 }
272 }
273
274 fn record_half_open(&mut self, is_failure: bool, now: u64) {
276 if is_failure {
277 self.consecutive_successes = 0;
279 self.trip_open(now);
280 } else {
281 self.consecutive_successes += 1;
282 self.consecutive_failures = 0;
283 if self.consecutive_successes >= self.config.success_threshold {
284 self.close(now);
285 }
286 }
287 }
288
289 fn trip_open(&mut self, now: u64) {
291 self.state = CircuitBreakerState::Open { opened_at: now };
292 self.last_opened_at = Some(now);
293 self.half_open_calls = 0;
294 }
295
296 fn close(&mut self, now: u64) {
298 self.state = CircuitBreakerState::Closed;
299 self.last_closed_at = Some(now);
300 self.consecutive_failures = 0;
301 self.consecutive_successes = 0;
302 self.half_open_calls = 0;
303 }
304
305 fn push_window(&mut self, success: bool) {
310 if self.window.len() >= self.config.window_size as usize {
311 self.window.pop_front();
312 }
313 self.window.push_back(success);
314 }
315
316 pub fn failure_rate(&self) -> f64 {
322 if self.window.is_empty() {
323 return 0.0;
324 }
325 let failures = self.window.iter().filter(|&&s| !s).count();
326 failures as f64 / self.window.len() as f64
327 }
328
329 fn window_success_count(&self) -> u32 {
331 self.window.iter().filter(|&&s| s).count() as u32
332 }
333
334 fn window_failure_count(&self) -> u32 {
336 self.window.iter().filter(|&&s| !s).count() as u32
337 }
338
339 pub fn reset(&mut self, now: u64) {
341 self.state = CircuitBreakerState::Closed;
342 self.window.clear();
343 self.consecutive_failures = 0;
344 self.consecutive_successes = 0;
345 self.half_open_calls = 0;
346 self.last_closed_at = Some(now);
347 }
348
349 pub fn stats(&self) -> CircuitStats {
351 let state_str = match &self.state {
352 CircuitBreakerState::Closed => "Closed",
353 CircuitBreakerState::Open { .. } => "Open",
354 CircuitBreakerState::HalfOpen { .. } => "HalfOpen",
355 };
356 CircuitStats {
357 state: state_str.to_string(),
358 failure_count: self.window_failure_count(),
359 success_count: self.window_success_count(),
360 consecutive_failures: self.consecutive_failures,
361 consecutive_successes: self.consecutive_successes,
362 total_calls: self.total_calls,
363 rejected_calls: self.rejected_calls,
364 last_opened_at: self.last_opened_at,
365 last_closed_at: self.last_closed_at,
366 }
367 }
368
369 pub fn is_closed(&self) -> bool {
371 matches!(self.state, CircuitBreakerState::Closed)
372 }
373
374 pub fn is_open(&self) -> bool {
376 matches!(self.state, CircuitBreakerState::Open { .. })
377 }
378
379 pub fn is_half_open(&self) -> bool {
381 matches!(self.state, CircuitBreakerState::HalfOpen { .. })
382 }
383}
384
385#[derive(Clone, Debug, Default)]
391pub struct RegistryStats {
392 pub total_peers: usize,
394 pub closed_count: usize,
396 pub open_count: usize,
398 pub half_open_count: usize,
400 pub total_rejected_calls: u64,
402}
403
404#[derive(Clone, Debug)]
412pub struct PeerCircuit {
413 pub peer_id: String,
415 pub state: CircuitBreakerState,
417 pub consecutive_failures: u32,
419 pub probe_successes: u32,
421 pub total_calls: u64,
423 pub rejected_calls: u64,
425}
426
427impl From<&PeerCircuitBreaker> for PeerCircuit {
428 fn from(b: &PeerCircuitBreaker) -> Self {
429 Self {
430 peer_id: b.peer_id.clone(),
431 state: b.state.clone(),
432 consecutive_failures: b.consecutive_failures,
433 probe_successes: b.consecutive_successes,
434 total_calls: b.total_calls,
435 rejected_calls: b.rejected_calls,
436 }
437 }
438}
439
440pub struct CircuitBreakerRegistry {
449 breakers: HashMap<String, PeerCircuitBreaker>,
450 default_config: CircuitConfig,
451}
452
453impl CircuitBreakerRegistry {
454 pub fn new(default_config: CircuitConfig) -> Self {
456 Self {
457 breakers: HashMap::new(),
458 default_config,
459 }
460 }
461
462 pub fn get_or_create(&mut self, peer_id: &str) -> &mut PeerCircuitBreaker {
465 let config = self.default_config.clone();
466 self.breakers
467 .entry(peer_id.to_string())
468 .or_insert_with(|| PeerCircuitBreaker::new(peer_id.to_string(), config))
469 }
470
471 pub fn can_call(&mut self, peer_id: &str, now: u64) -> bool {
475 self.get_or_create(peer_id).can_call(now)
476 }
477
478 pub fn record_result(&mut self, peer_id: &str, result: CallResult, now: u64) {
480 let breaker = self.get_or_create(peer_id);
481 breaker.total_calls += 1;
482 breaker.record_result(result, now);
483 }
484
485 pub fn open_peers(&mut self, now: u64) -> Vec<String> {
487 self.breakers
488 .iter_mut()
489 .filter_map(|(id, b)| {
490 if let CircuitBreakerState::Open { opened_at } = &b.state {
491 if now < opened_at + b.config.timeout_ms {
493 return Some(id.clone());
494 }
495 }
496 None
497 })
498 .collect()
499 }
500
501 pub fn half_open_peers(&self) -> Vec<String> {
503 self.breakers
504 .iter()
505 .filter_map(|(id, b)| {
506 if matches!(b.state, CircuitBreakerState::HalfOpen { .. }) {
507 Some(id.clone())
508 } else {
509 None
510 }
511 })
512 .collect()
513 }
514
515 pub fn reset_peer(&mut self, peer_id: &str, now: u64) -> bool {
519 match self.breakers.get_mut(peer_id) {
520 Some(b) => {
521 b.reset(now);
522 true
523 }
524 None => false,
525 }
526 }
527
528 pub fn evict_closed_peers(&mut self, min_calls: u64) -> usize {
532 let before = self.breakers.len();
533 self.breakers.retain(|_, b| {
534 !matches!(b.state, CircuitBreakerState::Closed) || b.total_calls >= min_calls
536 });
537 before - self.breakers.len()
538 }
539
540 pub fn registry_stats(&mut self, now: u64) -> RegistryStats {
542 let mut stats = RegistryStats {
543 total_peers: self.breakers.len(),
544 ..Default::default()
545 };
546 for b in self.breakers.values_mut() {
547 if let CircuitBreakerState::Open { opened_at } = &b.state {
549 if now >= opened_at + b.config.timeout_ms {
550 let probe_start = now;
551 b.state = CircuitBreakerState::HalfOpen { probe_start };
552 b.half_open_calls = 0;
553 b.consecutive_successes = 0;
554 }
555 }
556 match &b.state {
557 CircuitBreakerState::Closed => stats.closed_count += 1,
558 CircuitBreakerState::Open { .. } => stats.open_count += 1,
559 CircuitBreakerState::HalfOpen { .. } => stats.half_open_count += 1,
560 }
561 stats.total_rejected_calls += b.rejected_calls;
562 }
563 stats
564 }
565
566 pub fn len(&self) -> usize {
568 self.breakers.len()
569 }
570
571 pub fn is_empty(&self) -> bool {
573 self.breakers.is_empty()
574 }
575}
576
577#[cfg(test)]
582mod tests {
583 use crate::circuit_breaker::{
584 CallResult, CircuitBreakerRegistry, CircuitConfig, PeerCircuitBreaker, RegistryStats,
585 };
586
587 fn default_config() -> CircuitConfig {
590 CircuitConfig::default()
591 }
592
593 fn make_breaker(peer_id: &str) -> PeerCircuitBreaker {
594 PeerCircuitBreaker::new(peer_id.to_string(), default_config())
595 }
596
597 fn success(ms: u64) -> CallResult {
598 CallResult::Success { duration_ms: ms }
599 }
600
601 fn failure(ms: u64) -> CallResult {
602 CallResult::Failure {
603 duration_ms: ms,
604 reason: "err".to_string(),
605 }
606 }
607
608 fn timeout_result(ms: u64) -> CallResult {
609 CallResult::Timeout { duration_ms: ms }
610 }
611
612 fn inject_failures(b: &mut PeerCircuitBreaker, n: u32, now: u64) {
614 for _ in 0..n {
615 b.record_result(failure(1), now);
616 }
617 }
618
619 fn trip_open(b: &mut PeerCircuitBreaker, now: u64) {
621 inject_failures(b, b.config.failure_threshold, now);
622 }
623
624 #[test]
627 fn new_breaker_is_closed() {
628 let b = make_breaker("p1");
629 assert!(b.is_closed());
630 }
631
632 #[test]
635 fn can_call_while_closed() {
636 let mut b = make_breaker("p2");
637 assert!(b.can_call(0));
638 }
639
640 #[test]
643 fn consecutive_failures_trip_open() {
644 let mut b = make_breaker("p3");
645 inject_failures(&mut b, 4, 0); assert!(b.is_closed(), "should still be Closed after 4 failures");
647 b.record_result(failure(1), 0); assert!(
649 b.is_open(),
650 "should be Open after hitting failure_threshold"
651 );
652 }
653
654 #[test]
657 fn open_circuit_rejects_can_call() {
658 let mut b = make_breaker("p4");
659 trip_open(&mut b, 0);
660 assert!(!b.can_call(1000), "should be rejected while Open");
661 }
662
663 #[test]
666 fn open_transitions_to_half_open_after_timeout() {
667 let mut b = make_breaker("p5");
668 trip_open(&mut b, 0);
669 let result = b.can_call(30_000);
671 assert!(result, "should allow call after timeout elapses");
672 assert!(b.is_half_open(), "should be HalfOpen");
673 }
674
675 #[test]
678 fn open_stays_open_before_timeout() {
679 let mut b = make_breaker("p6");
680 trip_open(&mut b, 0);
681 assert!(
682 !b.can_call(29_999),
683 "should still be blocked before timeout"
684 );
685 assert!(b.is_open());
686 }
687
688 #[test]
691 fn half_open_allows_limited_calls() {
692 let mut b = PeerCircuitBreaker::new(
693 "p7".to_string(),
694 CircuitConfig {
695 half_open_max_calls: 2,
696 ..default_config()
697 },
698 );
699 trip_open(&mut b, 0);
700 b.can_call(30_000); assert!(b.is_half_open());
702
703 b.half_open_calls = 1; assert!(b.can_call(30_001), "second call should be allowed");
705
706 b.half_open_calls = 2; assert!(!b.can_call(30_002), "third call should be rejected");
708 }
709
710 #[test]
713 fn half_open_successes_close_circuit() {
714 let mut b = make_breaker("p8");
715 trip_open(&mut b, 0);
716 b.can_call(30_000); b.record_result(success(100), 30_001);
718 assert!(b.is_half_open(), "still HalfOpen after 1 success (need 2)");
719 b.record_result(success(100), 30_002);
720 assert!(
721 b.is_closed(),
722 "should be Closed after success_threshold reached"
723 );
724 }
725
726 #[test]
729 fn half_open_failure_reopens() {
730 let mut b = make_breaker("p9");
731 trip_open(&mut b, 0);
732 b.can_call(30_000); b.record_result(failure(1), 30_001);
734 assert!(b.is_open(), "failure in HalfOpen should re-open circuit");
735 }
736
737 #[test]
740 fn slow_success_counts_as_failure() {
741 let mut b = make_breaker("p10");
742 for _ in 0..5 {
744 b.record_result(success(5_000), 0);
745 }
746 assert!(b.is_open(), "slow calls should trip the circuit");
747 }
748
749 #[test]
752 fn fast_success_is_not_a_failure() {
753 let mut b = make_breaker("p11");
754 b.record_result(success(4_999), 0);
755 assert!(b.is_closed());
756 assert_eq!(b.consecutive_failures, 0);
757 }
758
759 #[test]
762 fn failure_rate_empty_window() {
763 let b = make_breaker("p12");
764 assert_eq!(b.failure_rate(), 0.0);
765 }
766
767 #[test]
770 fn failure_rate_calculation() {
771 let mut b = make_breaker("p13");
772 b.record_result(success(1), 0);
773 b.record_result(failure(1), 0);
774 b.record_result(failure(1), 0);
775 b.record_result(success(1), 0);
776 let rate = b.failure_rate();
778 assert!((rate - 0.5).abs() < 1e-9, "rate={rate}");
779 }
780
781 #[test]
784 fn sliding_window_evicts_oldest() {
785 let mut b = PeerCircuitBreaker::new(
786 "p14".to_string(),
787 CircuitConfig {
788 window_size: 3,
789 failure_threshold: 100, ..default_config()
791 },
792 );
793 b.record_result(failure(1), 0);
794 b.record_result(failure(1), 0);
795 b.record_result(failure(1), 0);
796 assert_eq!(b.window.len(), 3);
797 b.record_result(success(1), 0);
799 assert_eq!(b.window.len(), 3);
800 let rate = b.failure_rate();
802 assert!((rate - 2.0 / 3.0).abs() < 1e-9, "rate={rate}");
803 }
804
805 #[test]
808 fn reset_clears_state() {
809 let mut b = make_breaker("p15");
810 trip_open(&mut b, 0);
811 b.reset(1000);
812 assert!(b.is_closed());
813 assert_eq!(b.consecutive_failures, 0);
814 assert_eq!(b.consecutive_successes, 0);
815 assert!(b.window.is_empty());
816 assert_eq!(b.last_closed_at, Some(1000));
817 }
818
819 #[test]
822 fn stats_reflects_current_state() {
823 let mut b = make_breaker("p16");
824 b.total_calls = 10;
825 b.rejected_calls = 2;
826 b.record_result(failure(1), 0);
827 let s = b.stats();
828 assert_eq!(s.state, "Closed");
829 assert_eq!(s.total_calls, 10);
830 assert_eq!(s.rejected_calls, 2);
831 assert_eq!(s.consecutive_failures, 1);
832 }
833
834 #[test]
837 fn stats_shows_open_state() {
838 let mut b = make_breaker("p17");
839 trip_open(&mut b, 42);
840 let s = b.stats();
841 assert_eq!(s.state, "Open");
842 assert_eq!(s.last_opened_at, Some(42));
843 }
844
845 #[test]
848 fn last_opened_at_set_on_trip() {
849 let mut b = make_breaker("p18");
850 trip_open(&mut b, 9999);
851 assert_eq!(b.last_opened_at, Some(9999));
852 }
853
854 #[test]
857 fn last_closed_at_set_on_recovery() {
858 let mut b = make_breaker("p19");
859 trip_open(&mut b, 0);
860 b.can_call(30_000); b.record_result(success(1), 30_001);
862 b.record_result(success(1), 30_002);
863 assert!(b.is_closed());
864 assert_eq!(b.last_closed_at, Some(30_002));
865 }
866
867 #[test]
870 fn timeout_result_is_failure() {
871 let r = timeout_result(100);
872 assert!(r.is_failure(5_000), "Timeout should always be a failure");
873 }
874
875 #[test]
878 fn failure_result_is_failure() {
879 let r = failure(1);
880 assert!(r.is_failure(5_000));
881 }
882
883 #[test]
886 fn success_below_threshold_not_failure() {
887 let r = success(4_999);
888 assert!(!r.is_failure(5_000));
889 }
890
891 #[test]
894 fn success_at_threshold_is_failure() {
895 let r = success(5_000);
896 assert!(r.is_failure(5_000));
897 }
898
899 #[test]
902 fn registry_creates_new_breaker() {
903 let mut reg = CircuitBreakerRegistry::new(default_config());
904 let b = reg.get_or_create("r1");
905 assert!(b.is_closed());
906 }
907
908 #[test]
911 fn registry_can_call_delegates() {
912 let mut reg = CircuitBreakerRegistry::new(default_config());
913 assert!(reg.can_call("r2", 0));
914 }
915
916 #[test]
919 fn registry_record_result_increments_total_calls() {
920 let mut reg = CircuitBreakerRegistry::new(default_config());
921 reg.record_result("r3", success(1), 0);
922 reg.record_result("r3", success(1), 0);
923 assert_eq!(reg.get_or_create("r3").total_calls, 2);
924 }
925
926 #[test]
929 fn registry_open_peers() {
930 let mut reg = CircuitBreakerRegistry::new(default_config());
931 for _ in 0..5 {
933 reg.record_result("r4", failure(1), 0);
934 }
935 reg.record_result("r5", success(1), 0);
936 let open = reg.open_peers(0);
937 assert!(open.contains(&"r4".to_string()));
938 assert!(!open.contains(&"r5".to_string()));
939 }
940
941 #[test]
944 fn registry_half_open_peers() {
945 let mut reg = CircuitBreakerRegistry::new(default_config());
946 for _ in 0..5 {
948 reg.record_result("r6", failure(1), 0);
949 }
950 reg.can_call("r6", 30_000); let ho = reg.half_open_peers();
952 assert!(ho.contains(&"r6".to_string()));
953 }
954
955 #[test]
958 fn registry_reset_peer_unknown() {
959 let mut reg = CircuitBreakerRegistry::new(default_config());
960 assert!(!reg.reset_peer("nobody", 0));
961 }
962
963 #[test]
966 fn registry_reset_peer_closes_open() {
967 let mut reg = CircuitBreakerRegistry::new(default_config());
968 for _ in 0..5 {
969 reg.record_result("r7", failure(1), 0);
970 }
971 assert!(reg.reset_peer("r7", 100));
972 assert!(reg.get_or_create("r7").is_closed());
973 }
974
975 #[test]
978 fn registry_evict_closed_peers() {
979 let mut reg = CircuitBreakerRegistry::new(default_config());
980 reg.record_result("low", success(1), 0); reg.record_result("high", success(1), 0);
982 reg.record_result("high", success(1), 0);
983 reg.record_result("high", success(1), 0); let evicted = reg.evict_closed_peers(2); assert_eq!(evicted, 1, "should evict 'low' (1 call < 2)");
987 assert!(!reg.breakers.contains_key("low"));
988 assert!(reg.breakers.contains_key("high"));
989 }
990
991 #[test]
994 fn registry_evict_skips_open_peers() {
995 let mut reg = CircuitBreakerRegistry::new(default_config());
996 for _ in 0..5 {
997 reg.record_result("open-peer", failure(1), 0);
998 }
999 let evicted = reg.evict_closed_peers(10);
1002 assert_eq!(evicted, 0, "should not evict Open peer");
1003 assert!(reg.breakers.contains_key("open-peer"));
1004 }
1005
1006 #[test]
1009 fn registry_stats_counts() {
1010 let mut reg = CircuitBreakerRegistry::new(default_config());
1011 reg.record_result("s1", success(1), 0); for _ in 0..5 {
1013 reg.record_result("s2", failure(1), 0); }
1015 let stats: RegistryStats = reg.registry_stats(0);
1016 assert_eq!(stats.total_peers, 2);
1017 assert_eq!(stats.closed_count, 1);
1018 assert_eq!(stats.open_count, 1);
1019 assert_eq!(stats.half_open_count, 0);
1020 }
1021
1022 #[test]
1025 fn registry_stats_total_rejected() {
1026 let mut reg = CircuitBreakerRegistry::new(default_config());
1027 for _ in 0..5 {
1029 reg.record_result("rj", failure(1), 0);
1030 }
1031 {
1032 let b = reg.get_or_create("rj");
1033 b.rejected_calls = 7;
1034 }
1035 let stats = reg.registry_stats(0);
1036 assert_eq!(stats.total_rejected_calls, 7);
1037 }
1038
1039 #[test]
1042 fn multiple_trips_update_last_opened_at() {
1043 let mut b = make_breaker("p35");
1044 trip_open(&mut b, 100);
1045 assert_eq!(b.last_opened_at, Some(100));
1046
1047 b.can_call(130_100); b.record_result(success(1), 130_101);
1050 b.record_result(success(1), 130_102); trip_open(&mut b, 200_000);
1054 assert_eq!(b.last_opened_at, Some(200_000));
1055 }
1056
1057 #[test]
1060 fn timeout_result_trips_circuit() {
1061 let mut b = make_breaker("p36");
1062 for _ in 0..5 {
1063 b.record_result(timeout_result(10_000), 0);
1064 }
1065 assert!(b.is_open(), "repeated timeouts should trip circuit Open");
1066 }
1067
1068 #[test]
1071 fn window_counts_after_mixed_results() {
1072 let mut b = PeerCircuitBreaker::new(
1073 "p37".to_string(),
1074 CircuitConfig {
1075 window_size: 6,
1076 failure_threshold: 100,
1077 ..default_config()
1078 },
1079 );
1080 for _ in 0..3 {
1081 b.record_result(success(1), 0);
1082 }
1083 for _ in 0..3 {
1084 b.record_result(failure(1), 0);
1085 }
1086 let s = b.stats();
1087 assert_eq!(s.success_count, 3);
1088 assert_eq!(s.failure_count, 3);
1089 }
1090
1091 #[test]
1094 fn stats_timestamps_propagated() {
1095 let mut b = make_breaker("p38");
1096 trip_open(&mut b, 5555);
1097 b.can_call(35_555); b.record_result(success(1), 35_556);
1099 b.record_result(success(1), 35_557); let s = b.stats();
1102 assert_eq!(s.last_opened_at, Some(5555));
1103 assert_eq!(s.last_closed_at, Some(35_557));
1104 }
1105
1106 #[test]
1109 fn state_helpers_correct() {
1110 let mut b = make_breaker("p39");
1111 assert!(b.is_closed());
1112 assert!(!b.is_open());
1113 assert!(!b.is_half_open());
1114
1115 trip_open(&mut b, 0);
1116 assert!(!b.is_closed());
1117 assert!(b.is_open());
1118 assert!(!b.is_half_open());
1119
1120 b.can_call(30_000);
1121 assert!(!b.is_closed());
1122 assert!(!b.is_open());
1123 assert!(b.is_half_open());
1124 }
1125
1126 #[test]
1129 fn registry_len_and_is_empty() {
1130 let mut reg = CircuitBreakerRegistry::new(default_config());
1131 assert!(reg.is_empty());
1132 assert_eq!(reg.len(), 0);
1133 reg.can_call("x", 0);
1134 assert!(!reg.is_empty());
1135 assert_eq!(reg.len(), 1);
1136 }
1137}