use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CircuitState {
Closed = 0,
HalfOpen = 1,
Open = 2,
}
#[derive(Debug)]
pub struct CircuitBreaker {
state: CircuitState,
failure_count: u32,
last_failure: Option<Instant>,
cooldown: Duration,
failure_threshold: u32,
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new()
}
}
impl CircuitBreaker {
pub fn new() -> Self {
Self::with_config(3, Duration::from_secs(30))
}
pub fn with_config(failure_threshold: u32, cooldown: Duration) -> Self {
Self {
state: CircuitState::Closed,
failure_count: 0,
last_failure: None,
cooldown,
failure_threshold,
}
}
pub fn state(&self) -> CircuitState {
self.state
}
pub fn is_allowed(&mut self) -> bool {
match self.state {
CircuitState::Closed | CircuitState::HalfOpen => true,
CircuitState::Open => {
let elapsed = self
.last_failure
.map(|t| t.elapsed())
.unwrap_or(Duration::MAX);
if elapsed >= self.cooldown {
tracing::info!(
cooldown_elapsed_ms = elapsed.as_millis(),
"CircuitBreaker: Open → HalfOpen"
);
self.state = CircuitState::HalfOpen;
true
} else {
false
}
}
}
}
pub fn record_success(&mut self) {
match self.state {
CircuitState::HalfOpen => {
tracing::info!("CircuitBreaker: HalfOpen → Closed");
self.state = CircuitState::Closed;
self.failure_count = 0;
self.last_failure = None;
}
CircuitState::Closed => {
self.failure_count = 0;
}
CircuitState::Open => {}
}
}
pub fn record_failure(&mut self) -> CircuitState {
self.failure_count += 1;
match self.state {
CircuitState::Closed => {
if self.failure_count >= self.failure_threshold {
tracing::warn!(
failure_count = self.failure_count,
"CircuitBreaker: Closed → Open"
);
self.state = CircuitState::Open;
self.last_failure = Some(Instant::now());
}
}
CircuitState::HalfOpen => {
tracing::warn!("CircuitBreaker: HalfOpen → Open (probe failed)");
self.state = CircuitState::Open;
self.last_failure = Some(Instant::now());
}
CircuitState::Open => {
self.last_failure = Some(Instant::now());
}
}
self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trips_open_after_threshold_failures() {
let mut cb = CircuitBreaker::with_config(3, Duration::from_secs(3600));
assert!(cb.is_allowed());
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure(); assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.is_allowed());
}
#[test]
fn half_open_to_closed_on_success() {
let mut cb = CircuitBreaker::with_config(1, Duration::from_millis(1));
cb.record_failure(); std::thread::sleep(Duration::from_millis(5));
assert!(cb.is_allowed()); assert_eq!(cb.state(), CircuitState::HalfOpen);
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn half_open_to_open_on_failure() {
let mut cb = CircuitBreaker::with_config(1, Duration::from_millis(1));
cb.record_failure();
std::thread::sleep(Duration::from_millis(5));
cb.is_allowed(); cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}
}