lsp_max/primitives/
circuit_breaker.rs1use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum CircuitState {
16 Closed = 0,
18 HalfOpen = 1,
20 Open = 2,
22}
23
24#[derive(Debug)]
28pub struct CircuitBreaker {
29 state: CircuitState,
30 failure_count: u32,
31 last_failure: Option<Instant>,
32 cooldown: Duration,
33 failure_threshold: u32,
34}
35
36impl Default for CircuitBreaker {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl CircuitBreaker {
43 pub fn new() -> Self {
45 Self::with_config(3, Duration::from_secs(30))
46 }
47
48 pub fn with_config(failure_threshold: u32, cooldown: Duration) -> Self {
50 Self {
51 state: CircuitState::Closed,
52 failure_count: 0,
53 last_failure: None,
54 cooldown,
55 failure_threshold,
56 }
57 }
58
59 pub fn state(&self) -> CircuitState {
61 self.state
62 }
63
64 pub fn is_allowed(&mut self) -> bool {
67 match self.state {
68 CircuitState::Closed | CircuitState::HalfOpen => true,
69 CircuitState::Open => {
70 let elapsed = self
71 .last_failure
72 .map(|t| t.elapsed())
73 .unwrap_or(Duration::MAX);
74 if elapsed >= self.cooldown {
75 tracing::info!(
76 cooldown_elapsed_ms = elapsed.as_millis(),
77 "CircuitBreaker: Open → HalfOpen"
78 );
79 self.state = CircuitState::HalfOpen;
80 true
81 } else {
82 false
83 }
84 }
85 }
86 }
87
88 pub fn record_success(&mut self) {
90 match self.state {
91 CircuitState::HalfOpen => {
92 tracing::info!("CircuitBreaker: HalfOpen → Closed");
93 self.state = CircuitState::Closed;
94 self.failure_count = 0;
95 self.last_failure = None;
96 }
97 CircuitState::Closed => {
98 self.failure_count = 0;
99 }
100 CircuitState::Open => {}
101 }
102 }
103
104 pub fn record_failure(&mut self) -> CircuitState {
106 self.failure_count += 1;
107 match self.state {
108 CircuitState::Closed => {
109 if self.failure_count >= self.failure_threshold {
110 tracing::warn!(
111 failure_count = self.failure_count,
112 "CircuitBreaker: Closed → Open"
113 );
114 self.state = CircuitState::Open;
115 self.last_failure = Some(Instant::now());
116 }
117 }
118 CircuitState::HalfOpen => {
119 tracing::warn!("CircuitBreaker: HalfOpen → Open (probe failed)");
120 self.state = CircuitState::Open;
121 self.last_failure = Some(Instant::now());
122 }
123 CircuitState::Open => {
124 self.last_failure = Some(Instant::now());
125 }
126 }
127 self.state
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn trips_open_after_threshold_failures() {
137 let mut cb = CircuitBreaker::with_config(3, Duration::from_secs(3600));
138 assert!(cb.is_allowed());
139 cb.record_failure();
140 cb.record_failure();
141 assert_eq!(cb.state(), CircuitState::Closed);
142 cb.record_failure(); assert_eq!(cb.state(), CircuitState::Open);
144 assert!(!cb.is_allowed());
145 }
146
147 #[test]
148 fn half_open_to_closed_on_success() {
149 let mut cb = CircuitBreaker::with_config(1, Duration::from_millis(1));
150 cb.record_failure(); std::thread::sleep(Duration::from_millis(5));
152 assert!(cb.is_allowed()); assert_eq!(cb.state(), CircuitState::HalfOpen);
154 cb.record_success();
155 assert_eq!(cb.state(), CircuitState::Closed);
156 }
157
158 #[test]
159 fn half_open_to_open_on_failure() {
160 let mut cb = CircuitBreaker::with_config(1, Duration::from_millis(1));
161 cb.record_failure();
162 std::thread::sleep(Duration::from_millis(5));
163 cb.is_allowed(); cb.record_failure();
165 assert_eq!(cb.state(), CircuitState::Open);
166 }
167}