use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug)]
pub struct CircuitBreaker {
failure_threshold: usize,
open_duration_ms: u64,
state: Mutex<CircuitState>,
consecutive_failures: AtomicUsize,
opened_at: Mutex<Option<Instant>>,
}
impl CircuitBreaker {
pub fn new(failure_threshold: usize, open_duration_ms: u64) -> Self {
Self {
failure_threshold,
open_duration_ms,
state: Mutex::new(CircuitState::Closed),
consecutive_failures: AtomicUsize::new(0),
opened_at: Mutex::new(None),
}
}
pub fn record_success(&self) {
self.consecutive_failures.store(0, Ordering::SeqCst);
let mut state = self.state.lock().unwrap();
let prev = *state;
*state = CircuitState::Closed;
*self.opened_at.lock().unwrap() = None;
if prev != CircuitState::Closed {
tracing::info!(from = ?prev, to = ?CircuitState::Closed, "circuit breaker state changed");
}
}
pub fn record_failure(&self) {
let count = self.consecutive_failures.fetch_add(1, Ordering::SeqCst) + 1;
if count >= self.failure_threshold {
let mut state = self.state.lock().unwrap();
let prev = *state;
if prev != CircuitState::Open {
*state = CircuitState::Open;
*self.opened_at.lock().unwrap() = Some(Instant::now());
tracing::warn!(
from = ?prev,
to = ?CircuitState::Open,
consecutive_failures = count,
threshold = self.failure_threshold,
"circuit breaker opened"
);
}
}
}
pub fn is_available(&self) -> bool {
let mut state = self.state.lock().unwrap();
match *state {
CircuitState::Closed => true,
CircuitState::HalfOpen => true,
CircuitState::Open => {
let opened_at = self.opened_at.lock().unwrap();
if let Some(at) = *opened_at
&& at.elapsed().as_millis() >= self.open_duration_ms as u128
{
*state = CircuitState::HalfOpen;
tracing::info!(
from = ?CircuitState::Open,
to = ?CircuitState::HalfOpen,
"circuit breaker half-open, allowing trial request"
);
return true;
}
false
}
}
}
pub fn state(&self) -> CircuitState {
*self.state.lock().unwrap()
}
pub fn consecutive_failures(&self) -> usize {
self.consecutive_failures.load(Ordering::SeqCst)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
#[test]
fn new_breaker_is_closed() {
let cb = CircuitBreaker::new(3, 1000);
assert_eq!(cb.state(), CircuitState::Closed);
assert!(cb.is_available());
assert_eq!(cb.consecutive_failures(), 0);
}
#[test]
fn stays_closed_below_threshold() {
let cb = CircuitBreaker::new(3, 1000);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
assert!(cb.is_available());
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
assert!(cb.is_available());
}
#[test]
fn opens_at_threshold() {
let cb = CircuitBreaker::new(3, 1000);
cb.record_failure();
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.is_available());
}
#[test]
fn success_resets_counter() {
let cb = CircuitBreaker::new(3, 1000);
cb.record_failure();
cb.record_failure();
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
assert_eq!(cb.consecutive_failures(), 0);
}
#[test]
fn transitions_to_half_open_after_duration() {
let cb = CircuitBreaker::new(2, 50); cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.is_available());
thread::sleep(Duration::from_millis(60));
assert!(cb.is_available());
assert_eq!(cb.state(), CircuitState::HalfOpen);
}
#[test]
fn half_open_success_closes() {
let cb = CircuitBreaker::new(2, 50);
cb.record_failure();
cb.record_failure();
thread::sleep(Duration::from_millis(60));
assert!(cb.is_available()); cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn half_open_failure_reopens() {
let cb = CircuitBreaker::new(2, 50);
cb.record_failure();
cb.record_failure();
thread::sleep(Duration::from_millis(60));
assert!(cb.is_available()); cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(!cb.is_available());
}
#[test]
fn weak_reference_allows_cleanup() {
let strong = Arc::new(CircuitBreaker::new(3, 1000));
let weak = Arc::downgrade(&strong);
assert!(weak.upgrade().is_some());
drop(strong);
assert!(weak.upgrade().is_none());
}
}