use std::{
sync::Mutex,
time::{Duration, Instant},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug)]
pub(crate) struct CircuitBreaker {
inner: Mutex<CircuitInner>,
recovery_window: Duration,
threshold: u32,
}
#[derive(Debug)]
struct CircuitInner {
consecutive_failures: u32,
opened_at: Option<Instant>,
state: CircuitState,
}
impl CircuitBreaker {
pub(crate) fn new(threshold: u32, recovery_window_ms: u64) -> Self {
Self {
inner: Mutex::new(CircuitInner {
consecutive_failures: 0,
opened_at: None,
state: CircuitState::Closed,
}),
recovery_window: Duration::from_millis(recovery_window_ms),
threshold,
}
}
#[expect(clippy::expect_used, reason = "poisoned mutex is unrecoverable")]
pub(crate) fn check(&self) -> CircuitState {
let mut inner = self.inner.lock().expect("circuit breaker lock poisoned");
match inner.state {
CircuitState::Closed => CircuitState::Closed,
CircuitState::Open => {
if let Some(opened_at) = inner.opened_at
&& opened_at.elapsed() >= self.recovery_window
{
inner.state = CircuitState::HalfOpen;
CircuitState::HalfOpen
} else {
CircuitState::Open
}
},
CircuitState::HalfOpen => CircuitState::Open,
}
}
#[expect(clippy::expect_used, reason = "poisoned mutex is unrecoverable")]
pub(crate) fn record_failure(&self) {
let mut inner = self.inner.lock().expect("circuit breaker lock poisoned");
match inner.state {
CircuitState::Closed => {
inner.consecutive_failures = inner.consecutive_failures.saturating_add(1);
if inner.consecutive_failures >= self.threshold {
inner.state = CircuitState::Open;
inner.opened_at = Some(Instant::now());
}
},
CircuitState::HalfOpen => {
inner.state = CircuitState::Open;
inner.opened_at = Some(Instant::now());
},
CircuitState::Open => {},
}
}
#[expect(clippy::expect_used, reason = "poisoned mutex is unrecoverable")]
pub(crate) fn record_success(&self) {
let mut inner = self.inner.lock().expect("circuit breaker lock poisoned");
match inner.state {
CircuitState::Closed => {
inner.consecutive_failures = 0;
},
CircuitState::HalfOpen => {
inner.state = CircuitState::Closed;
inner.consecutive_failures = 0;
inner.opened_at = None;
},
CircuitState::Open => {},
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "poisoned mutex is unrecoverable")]
pub(crate) fn state(&self) -> CircuitState {
self.inner.lock().expect("circuit breaker lock poisoned").state
}
}