use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const FAILURE_THRESHOLD: u32 = 5;
pub const FAILURE_WINDOW: Duration = Duration::from_secs(60);
pub const COOLDOWN: Duration = Duration::from_secs(120);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Closed,
Open,
HalfOpen,
}
impl State {
pub fn as_str(self) -> &'static str {
match self {
State::Closed => "closed",
State::Open => "open",
State::HalfOpen => "half_open",
}
}
}
#[derive(Debug)]
struct Inner {
state: State,
failure_count: u32,
last_failure: Option<Instant>,
opened_at: Option<Instant>,
}
#[derive(Debug)]
pub struct CircuitBreaker {
failure_threshold: u32,
failure_window: Duration,
cooldown: Duration,
inner: Mutex<Inner>,
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new(FAILURE_THRESHOLD, FAILURE_WINDOW, COOLDOWN)
}
}
impl CircuitBreaker {
pub fn new(failure_threshold: u32, failure_window: Duration, cooldown: Duration) -> Self {
Self {
failure_threshold,
failure_window,
cooldown,
inner: Mutex::new(Inner {
state: State::Closed,
failure_count: 0,
last_failure: None,
opened_at: None,
}),
}
}
pub fn state(&self) -> State {
let mut g = self.inner.lock().unwrap();
if g.state == State::Open {
if let Some(opened_at) = g.opened_at {
if opened_at.elapsed() >= self.cooldown {
g.state = State::HalfOpen;
tracing::info!(target: "llm.circuit", "circuit_breaker_half_open");
}
}
}
g.state
}
pub fn is_open(&self) -> bool {
self.state() == State::Open
}
pub fn record_success(&self) {
let mut g = self.inner.lock().unwrap();
if g.state == State::HalfOpen {
tracing::info!(target: "llm.circuit", reason = "half_open_success", "circuit_breaker_closed");
}
g.state = State::Closed;
g.failure_count = 0;
}
pub fn record_failure(&self) {
let now = Instant::now();
let mut g = self.inner.lock().unwrap();
if let Some(last) = g.last_failure {
if now.duration_since(last) > self.failure_window {
g.failure_count = 0;
}
}
g.failure_count += 1;
g.last_failure = Some(now);
if g.state == State::HalfOpen {
g.state = State::Open;
g.opened_at = Some(now);
tracing::warn!(target: "llm.circuit", failures = g.failure_count, "circuit_breaker_reopened");
} else if g.failure_count >= self.failure_threshold {
g.state = State::Open;
g.opened_at = Some(now);
tracing::warn!(target: "llm.circuit", failures = g.failure_count, "circuit_breaker_opened");
}
}
pub fn status(&self) -> Status {
let state = self.state();
let g = self.inner.lock().unwrap();
Status {
state,
failure_count: g.failure_count,
threshold: self.failure_threshold,
cooldown: self.cooldown,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Status {
pub state: State,
pub failure_count: u32,
pub threshold: u32,
pub cooldown: Duration,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opens_after_threshold_failures() {
let cb = CircuitBreaker::default();
assert!(!cb.is_open());
for _ in 0..FAILURE_THRESHOLD {
cb.record_failure();
}
assert!(cb.is_open());
assert_eq!(cb.state(), State::Open);
}
#[test]
fn success_resets() {
let cb = CircuitBreaker::default();
cb.record_failure();
cb.record_failure();
cb.record_success();
assert_eq!(cb.status().failure_count, 0);
assert!(!cb.is_open());
}
#[test]
fn cooldown_moves_to_half_open_then_closes_on_success() {
let cb = CircuitBreaker::new(2, FAILURE_WINDOW, Duration::from_millis(10));
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), State::Open);
std::thread::sleep(Duration::from_millis(20));
assert_eq!(cb.state(), State::HalfOpen);
cb.record_success();
assert_eq!(cb.state(), State::Closed);
}
#[test]
fn half_open_failure_reopens() {
let cb = CircuitBreaker::new(2, FAILURE_WINDOW, Duration::from_millis(10));
cb.record_failure();
cb.record_failure();
std::thread::sleep(Duration::from_millis(20));
assert_eq!(cb.state(), State::HalfOpen);
cb.record_failure();
assert_eq!(cb.state(), State::Open);
}
}