use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
pub trait CircuitBreaker: Send + Sync {
fn state(&self) -> CircuitState;
fn record_success(&mut self);
fn record_failure(&mut self);
fn can_execute(&mut self) -> bool;
fn reset(&mut self) -> bool;
}
#[derive(Debug)]
pub struct DefaultCircuitBreaker {
failure_threshold: usize,
reset_timeout: Duration,
state: CircuitState,
consecutive_failures: usize,
last_failure_at: Option<Instant>,
}
impl DefaultCircuitBreaker {
pub fn new(failure_threshold: usize, reset_timeout: Duration) -> Self {
Self {
failure_threshold,
reset_timeout,
state: CircuitState::Closed,
consecutive_failures: 0,
last_failure_at: None,
}
}
pub fn consecutive_failures(&self) -> usize {
self.consecutive_failures
}
pub fn failure_threshold(&self) -> usize {
self.failure_threshold
}
pub fn reset_timeout(&self) -> Duration {
self.reset_timeout
}
}
impl Default for DefaultCircuitBreaker {
fn default() -> Self {
Self::new(5, Duration::from_secs(30))
}
}
impl CircuitBreaker for DefaultCircuitBreaker {
fn state(&self) -> CircuitState {
self.state
}
fn record_success(&mut self) {
self.consecutive_failures = 0;
self.state = CircuitState::Closed;
self.last_failure_at = None;
}
fn record_failure(&mut self) {
self.consecutive_failures += 1;
self.last_failure_at = Some(Instant::now());
if self.consecutive_failures >= self.failure_threshold {
self.state = CircuitState::Open;
}
}
fn can_execute(&mut self) -> bool {
match self.state {
CircuitState::Closed => true,
CircuitState::HalfOpen => true,
CircuitState::Open => {
let elapsed = self
.last_failure_at
.map(|t| t.elapsed())
.unwrap_or_else(|| Duration::ZERO);
if elapsed >= self.reset_timeout {
self.state = CircuitState::HalfOpen;
true
} else {
false
}
}
}
}
fn reset(&mut self) -> bool {
let changed = self.state != CircuitState::Closed || self.consecutive_failures != 0;
self.state = CircuitState::Closed;
self.consecutive_failures = 0;
self.last_failure_at = None;
changed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circuit_breaker_starts_closed() {
let mut cb = DefaultCircuitBreaker::new(3, Duration::from_millis(100));
assert_eq!(cb.state(), CircuitState::Closed);
assert!(cb.can_execute());
}
#[test]
fn test_circuit_breaker_trips_after_threshold() {
let mut cb = DefaultCircuitBreaker::new(3, Duration::from_secs(60));
assert!(cb.can_execute());
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.can_execute());
}
#[test]
fn test_circuit_breaker_success_resets() {
let mut cb = DefaultCircuitBreaker::new(3, Duration::from_secs(60));
cb.record_failure();
cb.record_failure();
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_half_open_after_timeout() {
let mut cb = DefaultCircuitBreaker::new(1, Duration::from_millis(10));
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.can_execute());
std::thread::sleep(Duration::from_millis(30));
assert!(cb.can_execute());
assert_eq!(cb.state(), CircuitState::HalfOpen);
}
#[test]
fn test_circuit_breaker_half_open_success_closes() {
let mut cb = DefaultCircuitBreaker::new(1, Duration::from_millis(10));
cb.record_failure();
std::thread::sleep(Duration::from_millis(20));
assert!(cb.can_execute());
assert_eq!(cb.state(), CircuitState::HalfOpen);
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_half_open_failure_reopens() {
let mut cb = DefaultCircuitBreaker::new(1, Duration::from_millis(10));
cb.record_failure();
std::thread::sleep(Duration::from_millis(20));
assert!(cb.can_execute());
assert_eq!(cb.state(), CircuitState::HalfOpen);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}
#[test]
fn test_circuit_breaker_boundary_exactly_threshold() {
let mut cb = DefaultCircuitBreaker::new(3, Duration::from_secs(60));
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}
#[test]
fn test_circuit_breaker_reset_from_open() {
let mut cb = DefaultCircuitBreaker::new(2, Duration::from_secs(60));
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(cb.reset());
assert_eq!(cb.state(), CircuitState::Closed);
assert_eq!(cb.consecutive_failures(), 0);
assert!(cb.can_execute());
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_reset_from_half_open() {
let mut cb = DefaultCircuitBreaker::new(1, Duration::from_millis(10));
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
std::thread::sleep(Duration::from_millis(20));
assert!(cb.can_execute());
assert_eq!(cb.state(), CircuitState::HalfOpen);
assert!(cb.reset());
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_reset_idempotent_when_closed() {
let mut cb = DefaultCircuitBreaker::new(3, Duration::from_secs(60));
assert!(!cb.reset());
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
cb.record_failure();
assert!(cb.reset());
assert_eq!(cb.consecutive_failures(), 0);
assert!(!cb.reset());
}
#[test]
fn test_circuit_state_variants_distinct() {
assert_ne!(CircuitState::Closed, CircuitState::Open);
assert_ne!(CircuitState::Open, CircuitState::HalfOpen);
assert_ne!(CircuitState::Closed, CircuitState::HalfOpen);
}
#[test]
fn test_default_circuit_breaker_default_config() {
let cb = DefaultCircuitBreaker::default();
assert_eq!(cb.failure_threshold(), 5);
assert_eq!(cb.reset_timeout(), Duration::from_secs(30));
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<DefaultCircuitBreaker>();
assert_send_sync::<CircuitState>();
}
#[test]
fn test_circuit_breaker_via_trait_object() {
let cb: Box<dyn CircuitBreaker> =
Box::new(DefaultCircuitBreaker::new(2, Duration::from_secs(60)));
assert_eq!(cb.state(), CircuitState::Closed);
}
}