1use std::sync::Mutex;
5use std::sync::atomic::{AtomicU8, Ordering};
6use std::time::{Duration, Instant};
7
8use crate::classify::ErrorPredicate;
9use crate::error::BreakerState;
10
11const CLOSED: u8 = 0;
12const OPEN: u8 = 1;
13const HALF_OPEN: u8 = 2;
14
15#[derive(Debug, Clone, Copy)]
17enum Trip {
18 Consecutive {
19 threshold: u32,
20 },
21 FailureRatio {
22 ratio: f64,
23 min_throughput: u32,
24 window: Duration,
25 },
26}
27
28pub struct CircuitBreaker<E> {
34 trip: Trip,
35 open_for: Duration,
36 half_open_max_calls: u32,
37 record_when: Option<ErrorPredicate<E>>,
38}
39
40impl<E> std::fmt::Debug for CircuitBreaker<E> {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("CircuitBreaker")
43 .field("trip", &self.trip)
44 .field("open_for", &self.open_for)
45 .field("half_open_max_calls", &self.half_open_max_calls)
46 .field("record_when", &self.record_when.as_ref().map(|_| "<fn>"))
47 .finish()
48 }
49}
50
51impl<E> CircuitBreaker<E> {
52 pub fn consecutive_failures(n: u32) -> Self {
54 Self {
55 trip: Trip::Consecutive {
56 threshold: n.max(1),
57 },
58 open_for: Duration::from_secs(30),
59 half_open_max_calls: 1,
60 record_when: None,
61 }
62 }
63
64 pub fn failure_ratio() -> Self {
66 Self {
67 trip: Trip::FailureRatio {
68 ratio: 0.5,
69 min_throughput: 10,
70 window: Duration::from_secs(30),
71 },
72 open_for: Duration::from_secs(30),
73 half_open_max_calls: 1,
74 record_when: None,
75 }
76 }
77
78 pub fn failure_ratio_value(mut self, ratio: f64) -> Self {
79 if let Trip::FailureRatio { ratio: r, .. } = &mut self.trip {
80 *r = ratio.clamp(0.0, 1.0);
81 }
82 self
83 }
84 pub fn ratio(self, ratio: f64) -> Self {
86 self.failure_ratio_value(ratio)
87 }
88 pub fn minimum_throughput(mut self, n: u32) -> Self {
89 if let Trip::FailureRatio { min_throughput, .. } = &mut self.trip {
90 *min_throughput = n;
91 }
92 self
93 }
94 pub fn sampling_window(mut self, d: Duration) -> Self {
95 if let Trip::FailureRatio { window, .. } = &mut self.trip {
96 *window = d;
97 }
98 self
99 }
100 pub fn open_for(mut self, d: Duration) -> Self {
101 self.open_for = d;
102 self
103 }
104 pub fn half_open_max_calls(mut self, n: u32) -> Self {
105 self.half_open_max_calls = n.max(1);
106 self
107 }
108 pub fn record_when(mut self, pred: impl Fn(&E) -> bool + Send + Sync + 'static) -> Self {
109 self.record_when = Some(Box::new(pred));
110 self
111 }
112
113 pub(crate) fn compile(self) -> (BreakerRuntime, Option<ErrorPredicate<E>>) {
115 let rt = BreakerRuntime::new(self.trip, self.open_for, self.half_open_max_calls);
116 (rt, self.record_when)
117 }
118}
119
120#[derive(Debug)]
122pub(crate) struct BreakerRuntime {
123 state: AtomicU8,
124 trip: Trip,
125 open_for: Duration,
126 half_open_max_calls: u32,
127 inner: Mutex<Inner>,
128}
129
130#[derive(Debug)]
131struct Inner {
132 open_until: Option<Instant>,
133 consecutive_failures: u32,
134 half_open_in_flight: u32,
135 half_open_successes: u32,
136 window: Window,
137}
138
139impl BreakerRuntime {
140 fn new(trip: Trip, open_for: Duration, half_open_max_calls: u32) -> Self {
141 let buckets = match trip {
142 Trip::FailureRatio { window, .. } => Window::new(window),
143 Trip::Consecutive { .. } => Window::new(Duration::from_secs(1)),
144 };
145 Self {
146 state: AtomicU8::new(CLOSED),
147 trip,
148 open_for,
149 half_open_max_calls,
150 inner: Mutex::new(Inner {
151 open_until: None,
152 consecutive_failures: 0,
153 half_open_in_flight: 0,
154 half_open_successes: 0,
155 window: buckets,
156 }),
157 }
158 }
159
160 pub(crate) fn state(&self) -> BreakerState {
162 match self.state.load(Ordering::Acquire) {
163 OPEN => BreakerState::Open,
164 HALF_OPEN => BreakerState::HalfOpen,
165 _ => BreakerState::Closed,
166 }
167 }
168
169 pub(crate) fn gate(&self, now: Instant) -> Result<BreakerState, ()> {
172 if self.state.load(Ordering::Acquire) == CLOSED {
173 return Ok(BreakerState::Closed);
174 }
175 let mut inner = self.inner.lock().unwrap();
176 match self.state.load(Ordering::Acquire) {
177 CLOSED => Ok(BreakerState::Closed),
178 OPEN => {
179 let ready = inner.open_until.map(|t| now >= t).unwrap_or(true);
180 if ready {
181 self.state.store(HALF_OPEN, Ordering::Release);
182 inner.half_open_in_flight = 1;
183 inner.half_open_successes = 0;
184 Ok(BreakerState::HalfOpen)
185 } else {
186 Err(())
187 }
188 }
189 _ => {
190 if inner.half_open_in_flight < self.half_open_max_calls {
192 inner.half_open_in_flight += 1;
193 Ok(BreakerState::HalfOpen)
194 } else {
195 Err(())
196 }
197 }
198 }
199 }
200
201 pub(crate) fn record_success(&self, now: Instant) -> Option<BreakerState> {
203 let mut inner = self.inner.lock().unwrap();
204 match self.state.load(Ordering::Acquire) {
205 HALF_OPEN => {
206 inner.half_open_in_flight = inner.half_open_in_flight.saturating_sub(1);
207 inner.half_open_successes += 1;
208 if inner.half_open_successes >= self.half_open_max_calls {
209 self.close(&mut inner);
210 return Some(BreakerState::Closed);
211 }
212 None
213 }
214 _ => {
215 inner.consecutive_failures = 0;
216 inner.window.record(now, false);
217 None
218 }
219 }
220 }
221
222 pub(crate) fn record_failure(&self, now: Instant) -> Option<BreakerState> {
224 let mut inner = self.inner.lock().unwrap();
225 match self.state.load(Ordering::Acquire) {
226 HALF_OPEN => {
227 inner.half_open_in_flight = inner.half_open_in_flight.saturating_sub(1);
228 self.open(&mut inner, now);
229 Some(BreakerState::Open)
230 }
231 _ => {
232 inner.consecutive_failures += 1;
233 inner.window.record(now, true);
234 if self.should_trip(&mut inner, now) {
235 self.open(&mut inner, now);
236 Some(BreakerState::Open)
237 } else {
238 None
239 }
240 }
241 }
242 }
243
244 fn should_trip(&self, inner: &mut Inner, now: Instant) -> bool {
245 match self.trip {
246 Trip::Consecutive { threshold } => inner.consecutive_failures >= threshold,
247 Trip::FailureRatio {
248 ratio,
249 min_throughput,
250 ..
251 } => {
252 let (failures, total) = inner.window.totals(now);
253 total >= min_throughput as u64 && (failures as f64 / total as f64) >= ratio
254 }
255 }
256 }
257
258 fn open(&self, inner: &mut Inner, now: Instant) {
259 self.state.store(OPEN, Ordering::Release);
260 inner.open_until = Some(now + self.open_for);
261 inner.half_open_successes = 0;
262 inner.half_open_in_flight = 0;
263 }
264
265 fn close(&self, inner: &mut Inner) {
266 self.state.store(CLOSED, Ordering::Release);
267 inner.consecutive_failures = 0;
268 inner.half_open_successes = 0;
269 inner.half_open_in_flight = 0;
270 inner.window.clear();
271 }
272}
273
274#[derive(Debug)]
276struct Window {
277 span: Duration,
278 bucket: Duration,
279 buckets: Vec<(Instant, u64, u64)>, }
281
282impl Window {
283 fn new(span: Duration) -> Self {
284 let n = 10u32;
285 let bucket = (span / n).max(Duration::from_millis(1));
286 Self {
287 span,
288 bucket,
289 buckets: Vec::with_capacity(n as usize + 1),
290 }
291 }
292
293 fn clear(&mut self) {
294 self.buckets.clear();
295 }
296
297 fn record(&mut self, now: Instant, failure: bool) {
298 self.evict(now);
299 let start = now;
300 match self.buckets.last_mut() {
301 Some((b_start, fails, total)) if now.duration_since(*b_start) < self.bucket => {
302 *total += 1;
303 if failure {
304 *fails += 1;
305 }
306 }
307 _ => {
308 self.buckets.push((start, u64::from(failure), 1));
309 }
310 }
311 }
312
313 fn totals(&mut self, now: Instant) -> (u64, u64) {
314 self.evict(now);
315 self.buckets
316 .iter()
317 .fold((0, 0), |(f, t), (_, fails, total)| (f + fails, t + total))
318 }
319
320 fn evict(&mut self, now: Instant) {
321 let cutoff = now.checked_sub(self.span);
322 if let Some(cutoff) = cutoff {
323 self.buckets.retain(|(start, _, _)| *start >= cutoff);
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn t0() -> Instant {
333 Instant::now()
334 }
335
336 #[test]
337 fn consecutive_trips_after_threshold() {
338 let (rt, _) = CircuitBreaker::<()>::consecutive_failures(3)
339 .open_for(Duration::from_secs(10))
340 .compile();
341 let now = t0();
342 assert_eq!(rt.gate(now), Ok(BreakerState::Closed));
343 rt.record_failure(now);
344 rt.record_failure(now);
345 assert_eq!(rt.state(), BreakerState::Closed);
346 rt.record_failure(now); assert_eq!(rt.state(), BreakerState::Open);
348 assert_eq!(rt.gate(now), Err(())); }
350
351 #[test]
352 fn success_resets_consecutive() {
353 let (rt, _) = CircuitBreaker::<()>::consecutive_failures(3).compile();
354 let now = t0();
355 rt.record_failure(now);
356 rt.record_failure(now);
357 rt.record_success(now);
358 rt.record_failure(now);
359 rt.record_failure(now);
360 assert_eq!(rt.state(), BreakerState::Closed); }
362
363 #[test]
364 fn half_open_probe_then_close_on_success() {
365 let (rt, _) = CircuitBreaker::<()>::consecutive_failures(2)
366 .open_for(Duration::from_secs(5))
367 .half_open_max_calls(1)
368 .compile();
369 let now = t0();
370 rt.record_failure(now);
371 rt.record_failure(now);
372 assert_eq!(rt.state(), BreakerState::Open);
373 assert_eq!(rt.gate(now + Duration::from_secs(1)), Err(()));
375 let later = now + Duration::from_secs(6);
377 assert_eq!(rt.gate(later), Ok(BreakerState::HalfOpen));
378 rt.record_success(later);
379 assert_eq!(rt.state(), BreakerState::Closed);
380 }
381
382 #[test]
383 fn half_open_failure_reopens() {
384 let (rt, _) = CircuitBreaker::<()>::consecutive_failures(1)
385 .open_for(Duration::from_secs(5))
386 .compile();
387 let now = t0();
388 rt.record_failure(now);
389 assert_eq!(rt.state(), BreakerState::Open);
390 let later = now + Duration::from_secs(6);
391 assert_eq!(rt.gate(later), Ok(BreakerState::HalfOpen));
392 rt.record_failure(later);
393 assert_eq!(rt.state(), BreakerState::Open);
394 }
395
396 #[test]
397 fn failure_ratio_trips() {
398 let (rt, _) = CircuitBreaker::<()>::failure_ratio()
399 .ratio(0.5)
400 .minimum_throughput(4)
401 .sampling_window(Duration::from_secs(10))
402 .compile();
403 let now = t0();
404 rt.record_success(now);
406 rt.record_success(now);
407 rt.record_failure(now);
408 rt.record_failure(now);
409 assert_eq!(rt.state(), BreakerState::Open);
410 }
411
412 #[test]
413 fn failure_ratio_respects_min_throughput() {
414 let (rt, _) = CircuitBreaker::<()>::failure_ratio()
415 .ratio(0.5)
416 .minimum_throughput(10)
417 .compile();
418 let now = t0();
419 rt.record_failure(now);
420 rt.record_failure(now); assert_eq!(rt.state(), BreakerState::Closed);
422 }
423
424 #[test]
425 fn record_when_compiles_predicate() {
426 let cb = CircuitBreaker::<i32>::consecutive_failures(1).record_when(|e: &i32| *e >= 500);
427 let (_rt, record_when) = cb.compile();
428 let p = record_when.expect("predicate present");
429 assert!(p(&503));
430 assert!(!p(&404));
431 }
432}