use std::fmt;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::clock::{Clock, SystemClock};
use parking_lot::Mutex;
pub use crate::error::CircuitCallError;
#[derive(Debug)]
enum State {
Closed { failures: u32 },
Open { opened_at: Instant },
HalfOpen,
}
pub struct SyncCircuitBreaker {
threshold: u32,
cooldown: Duration,
clock: Arc<dyn Clock>,
state: Mutex<State>,
}
impl fmt::Debug for SyncCircuitBreaker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SyncCircuitBreaker")
.field("threshold", &self.threshold)
.field("cooldown", &self.cooldown)
.finish_non_exhaustive()
}
}
impl SyncCircuitBreaker {
pub fn new(threshold: u32, cooldown: Duration) -> Self {
Self::with_clock(Arc::new(SystemClock), threshold, cooldown)
}
pub fn with_clock(clock: Arc<dyn Clock>, threshold: u32, cooldown: Duration) -> Self {
Self {
threshold: threshold.max(1),
cooldown,
clock,
state: Mutex::new(State::Closed { failures: 0 }),
}
}
pub fn call<T, E>(&self, f: impl FnOnce() -> Result<T, E>) -> Result<T, CircuitCallError<E>> {
if !self.admit() {
return Err(CircuitCallError::Open);
}
match f() {
Ok(value) => {
self.record_success();
Ok(value)
}
Err(err) => {
self.record_failure();
Err(CircuitCallError::Inner(err))
}
}
}
pub fn admit(&self) -> bool {
self.admit_at(self.clock.now())
}
pub fn admit_at(&self, now: Instant) -> bool {
let mut state = self.state.lock();
match &*state {
State::Closed { .. } => true,
State::Open { opened_at } => {
if now.duration_since(*opened_at) >= self.cooldown {
*state = State::HalfOpen;
true
} else {
false
}
}
State::HalfOpen => false,
}
}
pub fn record_success(&self) {
let mut state = self.state.lock();
match &*state {
State::HalfOpen => {
*state = State::Closed { failures: 0 };
}
State::Open { .. } => {
log::warn!("sync circuit: late success while Open; state unchanged");
}
State::Closed { .. } => {
if let State::Closed { failures } = &mut *state {
*failures = 0;
}
}
}
}
pub fn record_failure(&self) {
self.record_failure_at(self.clock.now());
}
pub fn record_failure_at(&self, now: Instant) {
let mut state = self.state.lock();
match &*state {
State::Closed { failures } => {
let failures = *failures + 1;
*state = if failures >= self.threshold {
State::Open { opened_at: now }
} else {
State::Closed { failures }
};
}
State::HalfOpen | State::Open { .. } => *state = State::Open { opened_at: now },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
fn t0() -> Instant {
Instant::now()
}
#[test]
fn test_call_passthrough_success_and_error() {
let b = SyncCircuitBreaker::new(3, Duration::from_secs(30));
assert_eq!(b.call(|| Ok::<i32, &str>(42)), Ok(42));
assert_eq!(
b.call(|| Err::<i32, &str>("x")),
Err(CircuitCallError::Inner("x"))
);
}
#[test]
fn test_threshold_failures_open_circuit_and_fast_fail() {
let b = SyncCircuitBreaker::new(3, Duration::from_secs(30));
let calls = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let counter = calls.clone();
let call = || {
let c = counter.clone();
b.call(move || -> Result<(), &str> {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err("boom")
})
};
assert!(matches!(call(), Err(CircuitCallError::Inner("boom"))));
assert!(matches!(call(), Err(CircuitCallError::Inner("boom"))));
assert!(matches!(call(), Err(CircuitCallError::Inner("boom"))));
assert!(matches!(call(), Err(CircuitCallError::Open)));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 3);
}
#[test]
fn test_success_resets_failure_count() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock, 3, Duration::from_secs(30));
b.record_failure();
b.record_failure();
b.record_success();
b.record_failure();
b.record_failure();
assert!(b.admit());
}
#[test]
fn test_open_rejects_until_cooldown_then_allows_probe() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock.clone(), 1, Duration::from_secs(10));
b.record_failure(); clock.advance(Duration::from_secs(5));
assert!(!b.admit());
clock.advance(Duration::from_secs(5));
assert!(b.admit());
}
#[test]
fn test_rejections_do_not_extend_cooldown() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock.clone(), 1, Duration::from_secs(10));
b.record_failure();
clock.advance(Duration::from_secs(3));
assert!(!b.admit());
clock.advance(Duration::from_secs(3));
assert!(!b.admit());
clock.advance(Duration::from_secs(4));
assert!(b.admit());
}
#[test]
fn test_half_open_probe_success_closes() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock.clone(), 2, Duration::from_secs(10));
b.record_failure();
b.record_failure(); clock.advance(Duration::from_secs(10));
assert!(b.admit()); b.record_success(); clock.advance(Duration::from_secs(1));
assert!(b.admit());
b.record_failure();
clock.advance(Duration::from_secs(1));
assert!(b.admit());
}
#[test]
fn test_half_open_probe_failure_reopens() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock.clone(), 1, Duration::from_secs(10));
b.record_failure(); clock.advance(Duration::from_secs(10));
assert!(b.admit()); b.record_failure(); clock.advance(Duration::from_secs(5));
assert!(!b.admit());
clock.advance(Duration::from_secs(5)); assert!(b.admit());
}
#[test]
fn test_half_open_rejects_concurrent_probes() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock.clone(), 1, Duration::from_secs(10));
b.record_failure();
clock.advance(Duration::from_secs(10));
assert!(b.admit(), "the first due call becomes the probe");
assert!(
!b.admit(),
"other calls are rejected while the probe is in flight"
);
b.record_success();
assert!(
b.admit(),
"allow resumes after the probe closes successfully"
);
}
#[test]
fn test_threshold_below_one_is_raised_to_one() {
let clock = Arc::new(MockClock::with_instant(t0(), 0));
let b = SyncCircuitBreaker::with_clock(clock, 0, Duration::from_secs(30));
b.record_failure();
assert!(!b.admit());
}
#[test]
fn test_late_success_in_open_does_not_close() {
let circuit = SyncCircuitBreaker::new(1, Duration::from_secs(60));
circuit.record_failure();
assert!(matches!(&*circuit.state.lock(), State::Open { .. }));
circuit.record_success();
assert!(
matches!(&*circuit.state.lock(), State::Open { .. }),
"a late success in the Open state must not close the circuit"
);
}
}