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 #[must_use]
39 pub const fn new(threshold: u32, half_open_after_ms: u64) -> Self {
40 Self {
41 state: AtomicU8::new(STATE_CLOSED),
42 failure_count: AtomicU32::new(0),
43 last_failure: AtomicU64::new(0),
44 threshold,
45 half_open_after_ms,
46 }
47 }
48
49 #[inline]
51 pub fn state(&self) -> u8 {
52 self.state.load(Ordering::Acquire)
53 }
54
55 pub fn is_available(&self) -> bool {
60 match self.state.load(Ordering::Acquire) {
61 STATE_CLOSED | 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()
138 .try_into()
139 .unwrap_or(u64::MAX)
140}
141
142#[cfg(test)]
147mod tests {
148 use std::sync::Arc;
149
150 use super::*;
151
152 fn breaker(threshold: u32, half_open_after_ms: u64) -> CircuitBreaker {
153 CircuitBreaker::new(threshold, half_open_after_ms)
154 }
155
156 #[test]
157 fn failures_open_circuit() {
158 let cb = breaker(3, 30_000);
159 assert_eq!(cb.state(), STATE_CLOSED);
160 cb.record_failure();
161 cb.record_failure();
162 assert_eq!(cb.state(), STATE_CLOSED, "not tripped yet");
163 cb.record_failure();
164 assert_eq!(cb.state(), STATE_OPEN, "should be open after threshold");
165 assert!(!cb.is_available());
166 }
167
168 #[test]
169 fn half_open_after_elapsed() {
170 let cb = breaker(1, 0); cb.record_failure();
172 assert_eq!(cb.state(), STATE_OPEN);
173 assert!(cb.is_available(), "should transition to half-open");
176 assert_eq!(cb.state(), STATE_HALF_OPEN);
177 }
178
179 #[test]
180 fn success_in_half_open_closes_circuit() {
181 let cb = breaker(1, 0);
182 cb.record_failure();
183 assert!(cb.is_available()); cb.record_success();
185 assert_eq!(cb.state(), STATE_CLOSED);
186 assert!(cb.is_available());
187 }
188
189 #[test]
190 fn failure_in_half_open_reopens() {
191 let cb = breaker(1, 0);
192 cb.record_failure();
193 assert!(cb.is_available()); cb.record_failure(); assert_eq!(cb.state(), STATE_OPEN);
196 }
197
198 #[test]
199 fn concurrent_failures_open_circuit() {
200 use std::thread;
201 let cb = Arc::new(breaker(5, 30_000));
202 let handles: Vec<_> = (0..100)
203 .map(|_| {
204 let cb = Arc::clone(&cb);
205 thread::spawn(move || cb.record_failure())
206 })
207 .collect();
208 for h in handles {
209 assert!(h.join().is_ok(), "worker thread should not panic");
210 }
211 assert_eq!(cb.state(), STATE_OPEN);
212 assert!(cb.failure_count.load(Ordering::Relaxed) >= 5);
213 }
214}