use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
pub struct CircuitBreaker {
state: Arc<RwLock<CircuitBreakerState>>,
failure_threshold: usize,
timeout: Duration,
consecutive_failures: Arc<AtomicUsize>,
last_failure_time: Arc<RwLock<Option<Instant>>>,
opened_at: Arc<RwLock<Option<Instant>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CircuitBreakerState {
Closed,
Open,
HalfOpen,
}
impl CircuitBreaker {
pub fn new(failure_threshold: usize, timeout: Duration) -> Self {
Self {
state: Arc::new(RwLock::new(CircuitBreakerState::Closed)),
failure_threshold,
timeout,
consecutive_failures: Arc::new(AtomicUsize::new(0)),
last_failure_time: Arc::new(RwLock::new(None)),
opened_at: Arc::new(RwLock::new(None)),
}
}
pub async fn is_open(&self) -> bool {
let state = *self.state.read().await;
match state {
CircuitBreakerState::Open => {
if let Some(opened_time) = *self.opened_at.read().await {
if opened_time.elapsed() > self.timeout {
info!("Circuit breaker transitioning to half-open");
*self.state.write().await = CircuitBreakerState::HalfOpen;
return false;
}
}
true
}
_ => false,
}
}
pub async fn record_success(&self) {
let prev_failures = self.consecutive_failures.swap(0, Ordering::SeqCst);
let prev_state = *self.state.read().await;
match prev_state {
CircuitBreakerState::HalfOpen => {
info!("Circuit breaker closing after successful recovery");
*self.state.write().await = CircuitBreakerState::Closed;
*self.opened_at.write().await = None;
}
CircuitBreakerState::Open => {
warn!("Success recorded while circuit was open");
*self.state.write().await = CircuitBreakerState::Closed;
*self.opened_at.write().await = None;
}
CircuitBreakerState::Closed => {
if prev_failures > 0 {
debug!("Failure count reset after success");
}
}
}
}
pub async fn record_failure(&self) {
let failures = self.consecutive_failures.fetch_add(1, Ordering::SeqCst) + 1;
*self.last_failure_time.write().await = Some(Instant::now());
let current_state = *self.state.read().await;
match current_state {
CircuitBreakerState::HalfOpen => {
warn!(
"Circuit breaker reopening after failed recovery attempt (failure #{})",
failures
);
*self.state.write().await = CircuitBreakerState::Open;
*self.opened_at.write().await = Some(Instant::now());
}
CircuitBreakerState::Closed => {
if failures >= self.failure_threshold {
warn!(
"Circuit breaker opening after {} consecutive failures",
failures
);
*self.state.write().await = CircuitBreakerState::Open;
*self.opened_at.write().await = Some(Instant::now());
} else {
debug!("Failure recorded: {} of {}", failures, self.failure_threshold);
}
}
CircuitBreakerState::Open => {
debug!("Failure recorded while circuit is open");
}
}
}
pub async fn state(&self) -> CircuitBreakerState {
*self.state.read().await
}
pub fn failure_count(&self) -> usize {
self.consecutive_failures.load(Ordering::SeqCst)
}
pub async fn time_since_last_failure(&self) -> Option<Duration> {
self.last_failure_time
.read()
.await
.as_ref()
.map(|t| t.elapsed())
}
pub async fn time_since_opened(&self) -> Option<Duration> {
self.opened_at.read().await.as_ref().map(|t| t.elapsed())
}
pub async fn reset(&self) {
info!("Circuit breaker manually reset");
*self.state.write().await = CircuitBreakerState::Closed;
self.consecutive_failures.store(0, Ordering::SeqCst);
*self.last_failure_time.write().await = None;
*self.opened_at.write().await = None;
}
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new(5, Duration::from_secs(30))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::sleep;
#[tokio::test]
async fn test_circuit_breaker_opens_after_threshold() {
let cb = CircuitBreaker::new(3, Duration::from_secs(10));
assert!(!cb.is_open().await);
assert_eq!(cb.state().await, CircuitBreakerState::Closed);
cb.record_failure().await;
assert!(!cb.is_open().await);
assert_eq!(cb.failure_count(), 1);
cb.record_failure().await;
assert!(!cb.is_open().await);
assert_eq!(cb.failure_count(), 2);
cb.record_failure().await;
assert!(cb.is_open().await);
assert_eq!(cb.state().await, CircuitBreakerState::Open);
assert_eq!(cb.failure_count(), 3);
}
#[tokio::test]
async fn test_circuit_breaker_closes_after_success() {
let cb = CircuitBreaker::new(2, Duration::from_secs(10));
cb.record_failure().await;
cb.record_failure().await;
assert!(cb.is_open().await);
cb.record_success().await;
assert!(!cb.is_open().await);
assert_eq!(cb.state().await, CircuitBreakerState::Closed);
assert_eq!(cb.failure_count(), 0);
}
#[tokio::test]
async fn test_circuit_breaker_half_open_transition() {
let cb = CircuitBreaker::new(2, Duration::from_millis(100));
cb.record_failure().await;
cb.record_failure().await;
assert!(cb.is_open().await);
sleep(Duration::from_millis(150)).await;
assert!(!cb.is_open().await);
assert_eq!(cb.state().await, CircuitBreakerState::HalfOpen);
cb.record_success().await;
assert_eq!(cb.state().await, CircuitBreakerState::Closed);
}
#[tokio::test]
async fn test_circuit_breaker_half_open_failure() {
let cb = CircuitBreaker::new(2, Duration::from_millis(100));
cb.record_failure().await;
cb.record_failure().await;
assert!(cb.is_open().await);
sleep(Duration::from_millis(150)).await;
let _ = cb.is_open().await;
cb.record_failure().await;
assert_eq!(cb.state().await, CircuitBreakerState::Open);
}
#[tokio::test]
async fn test_circuit_breaker_reset() {
let cb = CircuitBreaker::new(2, Duration::from_secs(10));
cb.record_failure().await;
cb.record_failure().await;
assert!(cb.is_open().await);
cb.reset().await;
assert!(!cb.is_open().await);
assert_eq!(cb.state().await, CircuitBreakerState::Closed);
assert_eq!(cb.failure_count(), 0);
}
#[tokio::test]
async fn test_circuit_breaker_time_tracking() {
let cb = CircuitBreaker::new(2, Duration::from_secs(10));
assert!(cb.time_since_last_failure().await.is_none());
cb.record_failure().await;
sleep(Duration::from_millis(50)).await;
let time = cb.time_since_last_failure().await.unwrap();
assert!(time >= Duration::from_millis(50));
}
}