use crate::error::{Mecha10Error, Result};
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub success_threshold: u32,
pub timeout: Duration,
pub name: Option<String>,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 2,
timeout: Duration::from_secs(60),
name: None,
}
}
}
#[derive(Debug)]
struct CircuitBreakerState {
state: CircuitState,
failure_count: u32,
success_count: u32,
opened_at: Option<Instant>,
total_requests: u64,
total_successes: u64,
total_failures: u64,
total_rejected: u64,
}
impl CircuitBreakerState {
fn new() -> Self {
Self {
state: CircuitState::Closed,
failure_count: 0,
success_count: 0,
opened_at: None,
total_requests: 0,
total_successes: 0,
total_failures: 0,
total_rejected: 0,
}
}
}
#[derive(Clone)]
pub struct CircuitBreaker {
config: CircuitBreakerConfig,
state: Arc<RwLock<CircuitBreakerState>>,
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
debug!(
"Creating circuit breaker '{}' with failure_threshold={}, success_threshold={}, timeout={:?}",
config.name.as_deref().unwrap_or("unnamed"),
config.failure_threshold,
config.success_threshold,
config.timeout
);
Self {
config,
state: Arc::new(RwLock::new(CircuitBreakerState::new())),
}
}
pub async fn call<F, Fut, T>(&self, operation: F) -> Result<T>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T>>,
{
{
let mut state = self.state.write().await;
state.total_requests += 1;
match state.state {
CircuitState::Closed => {}
CircuitState::Open => {
if let Some(opened_at) = state.opened_at {
if opened_at.elapsed() >= self.config.timeout {
info!(
"Circuit breaker '{}' transitioning to half-open (testing recovery)",
self.config.name.as_deref().unwrap_or("unnamed")
);
state.state = CircuitState::HalfOpen;
state.success_count = 0;
state.failure_count = 0;
} else {
state.total_rejected += 1;
return Err(Mecha10Error::CircuitBreakerOpen {
service: self.config.name.clone().unwrap_or_else(|| "unknown".to_string()),
message: format!(
"Circuit breaker open due to repeated failures. Retry after {:?}",
self.config.timeout - opened_at.elapsed()
),
});
}
}
}
CircuitState::HalfOpen => {}
}
}
let result = operation().await;
{
let mut state = self.state.write().await;
match &result {
Ok(_) => {
state.total_successes += 1;
self.on_success(&mut state).await;
}
Err(_) => {
state.total_failures += 1;
self.on_failure(&mut state).await;
}
}
}
result
}
async fn on_success(&self, state: &mut CircuitBreakerState) {
match state.state {
CircuitState::Closed => {
state.failure_count = 0;
}
CircuitState::HalfOpen => {
state.success_count += 1;
if state.success_count >= self.config.success_threshold {
info!(
"Circuit breaker '{}' closing (service recovered)",
self.config.name.as_deref().unwrap_or("unnamed")
);
state.state = CircuitState::Closed;
state.success_count = 0;
state.failure_count = 0;
state.opened_at = None;
}
}
CircuitState::Open => {
warn!("Success recorded in open state (unexpected)");
}
}
}
async fn on_failure(&self, state: &mut CircuitBreakerState) {
match state.state {
CircuitState::Closed => {
state.failure_count += 1;
if state.failure_count >= self.config.failure_threshold {
warn!(
"Circuit breaker '{}' opening (failure threshold reached: {} failures)",
self.config.name.as_deref().unwrap_or("unnamed"),
state.failure_count
);
state.state = CircuitState::Open;
state.opened_at = Some(Instant::now());
state.failure_count = 0;
}
}
CircuitState::HalfOpen => {
warn!(
"Circuit breaker '{}' re-opening (service still failing)",
self.config.name.as_deref().unwrap_or("unnamed")
);
state.state = CircuitState::Open;
state.opened_at = Some(Instant::now());
state.success_count = 0;
state.failure_count = 0;
}
CircuitState::Open => {}
}
}
async fn is_state(&self, want: CircuitState) -> bool {
let state = self.state.read().await;
state.state == want
}
pub async fn is_open(&self) -> bool {
self.is_state(CircuitState::Open).await
}
pub async fn is_closed(&self) -> bool {
self.is_state(CircuitState::Closed).await
}
pub async fn is_half_open(&self) -> bool {
self.is_state(CircuitState::HalfOpen).await
}
pub async fn state(&self) -> CircuitState {
let state = self.state.read().await;
state.state
}
pub async fn stats(&self) -> (u64, u64, u64, u64) {
let state = self.state.read().await;
(
state.total_requests,
state.total_successes,
state.total_failures,
state.total_rejected,
)
}
pub async fn success_rate(&self) -> f64 {
let state = self.state.read().await;
let total_attempted = state.total_successes + state.total_failures;
if total_attempted == 0 {
1.0
} else {
state.total_successes as f64 / total_attempted as f64
}
}
pub async fn reset(&self) {
self.force_state("reset to closed", CircuitState::Closed, None).await;
}
pub async fn open(&self) {
self.force_state("manually opened", CircuitState::Open, Some(Instant::now()))
.await;
}
async fn force_state(&self, log_action: &str, new_state: CircuitState, opened_at: Option<Instant>) {
let mut state = self.state.write().await;
info!(
"Circuit breaker '{}' {}",
self.config.name.as_deref().unwrap_or("unnamed"),
log_action
);
state.state = new_state;
state.failure_count = 0;
state.success_count = 0;
state.opened_at = opened_at;
}
}
impl std::fmt::Debug for CircuitBreaker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CircuitBreaker").field("config", &self.config).finish()
}
}