stygian_proxy/
circuit_breaker.rs1use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19pub const STATE_CLOSED: u8 = 0;
20pub const STATE_OPEN: u8 = 1;
21pub const STATE_HALF_OPEN: u8 = 2;
22
23pub struct CircuitBreaker {
28 state: AtomicU8,
29 failure_count: AtomicU32,
30 last_failure: AtomicU64,
32 threshold: u32,
33 half_open_after_ms: u64,
34}
35
36impl CircuitBreaker {
37 pub fn new(threshold: u32, half_open_after_ms: u64) -> Self {
39 Self {
40 state: AtomicU8::new(STATE_CLOSED),
41 failure_count: AtomicU32::new(0),
42 last_failure: AtomicU64::new(0),
43 threshold,
44 half_open_after_ms,
45 }
46 }
47
48 #[inline]
50 pub fn state(&self) -> u8 {
51 self.state.load(Ordering::Acquire)
52 }
53
54 pub fn is_available(&self) -> bool {
59 match self.state.load(Ordering::Acquire) {
60 STATE_CLOSED => true,
61 STATE_HALF_OPEN => true,
62 STATE_OPEN => {
63 let elapsed_ms = now_ms().saturating_sub(self.last_failure.load(Ordering::Acquire));
64 if elapsed_ms >= self.half_open_after_ms {
65 let _ = self.state.compare_exchange(
69 STATE_OPEN,
70 STATE_HALF_OPEN,
71 Ordering::AcqRel,
72 Ordering::Acquire,
73 );
74 true
75 } else {
76 false
77 }
78 }
79 _ => false,
80 }
81 }
82
83 pub fn record_success(&self) {
88 if self
89 .state
90 .compare_exchange(
91 STATE_HALF_OPEN,
92 STATE_CLOSED,
93 Ordering::AcqRel,
94 Ordering::Acquire,
95 )
96 .is_ok()
97 {
98 self.failure_count.store(0, Ordering::Release);
99 }
100 }
101
102 pub fn record_failure(&self) {
108 let count = self.failure_count.fetch_add(1, Ordering::AcqRel) + 1;
109 self.last_failure.store(now_ms(), Ordering::Release);
110
111 let current_state = self.state.load(Ordering::Acquire);
112 if current_state == STATE_CLOSED && count >= self.threshold {
113 let _ = self.state.compare_exchange(
115 STATE_CLOSED,
116 STATE_OPEN,
117 Ordering::AcqRel,
118 Ordering::Acquire,
119 );
120 } else if current_state == STATE_HALF_OPEN {
121 let _ = self.state.compare_exchange(
123 STATE_HALF_OPEN,
124 STATE_OPEN,
125 Ordering::AcqRel,
126 Ordering::Acquire,
127 );
128 }
129 }
130}
131
132#[inline]
133fn now_ms() -> u64 {
134 SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .unwrap_or_default()
137 .as_millis() as u64
138}
139
140#[cfg(test)]
145mod tests {
146 use std::sync::Arc;
147
148 use super::*;
149
150 fn breaker(threshold: u32, half_open_after_ms: u64) -> CircuitBreaker {
151 CircuitBreaker::new(threshold, half_open_after_ms)
152 }
153
154 #[test]
155 fn failures_open_circuit() {
156 let cb = breaker(3, 30_000);
157 assert_eq!(cb.state(), STATE_CLOSED);
158 cb.record_failure();
159 cb.record_failure();
160 assert_eq!(cb.state(), STATE_CLOSED, "not tripped yet");
161 cb.record_failure();
162 assert_eq!(cb.state(), STATE_OPEN, "should be open after threshold");
163 assert!(!cb.is_available());
164 }
165
166 #[test]
167 fn half_open_after_elapsed() {
168 let cb = breaker(1, 0); cb.record_failure();
170 assert_eq!(cb.state(), STATE_OPEN);
171 assert!(cb.is_available(), "should transition to half-open");
174 assert_eq!(cb.state(), STATE_HALF_OPEN);
175 }
176
177 #[test]
178 fn success_in_half_open_closes_circuit() {
179 let cb = breaker(1, 0);
180 cb.record_failure();
181 assert!(cb.is_available()); cb.record_success();
183 assert_eq!(cb.state(), STATE_CLOSED);
184 assert!(cb.is_available());
185 }
186
187 #[test]
188 fn failure_in_half_open_reopens() {
189 let cb = breaker(1, 0);
190 cb.record_failure();
191 assert!(cb.is_available()); cb.record_failure(); assert_eq!(cb.state(), STATE_OPEN);
194 }
195
196 #[test]
197 fn concurrent_failures_open_circuit() {
198 use std::thread;
199 let cb = Arc::new(breaker(5, 30_000));
200 let handles: Vec<_> = (0..100)
201 .map(|_| {
202 let cb = Arc::clone(&cb);
203 thread::spawn(move || cb.record_failure())
204 })
205 .collect();
206 for h in handles {
207 h.join().unwrap();
208 }
209 assert_eq!(cb.state(), STATE_OPEN);
210 assert!(cb.failure_count.load(Ordering::Relaxed) >= 5);
211 }
212}