mecha10-core 0.8.1

Mecha10 core runtime and foundational types
Documentation
//! Circuit Breaker Pattern for Fault Tolerance
//!
//! Prevents cascading failures by temporarily blocking requests to failing services.
//! When a service experiences repeated failures, the circuit breaker "opens" and
//! fails fast without attempting the request, giving the failing service time to recover.
//!
//! # States
//!
//! 1. **Closed** - Normal operation, requests pass through
//! 2. **Open** - Service is failing, requests fail fast without trying
//! 3. **Half-Open** - Testing if service has recovered
//!
//! # State Transitions
//!
//! ```text
//!           Success
//!             ↓
//!    ┌─────────────┐
//!    │   CLOSED    │ ←─────┐
//!    │  (Normal)   │       │
//!    └─────────────┘       │
//!          │               │ Success
//!  Failure threshold       │
//!  reached                 │
//!          ↓               │
//!    ┌─────────────┐   ┌──────────┐
//!    │    OPEN     │   │ HALF-OPEN│
//!    │(Fail Fast)  │──→│ (Testing)│
//!    └─────────────┘   └──────────┘
//!      After timeout       │
//!                     Failure
//!                          ↓
//!                     (back to OPEN)
//! ```
//!
//! # Example
//!
//! ```rust
//! use mecha10_core::prelude::*;
//! use mecha10_core::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
//! use mecha10_core::actuator::{MotorStatus, Twist};
//! use mecha10_core::rpc::RpcExt;
//!
//! # async fn example(ctx: &Context) -> Result<()> {
//! // Create circuit breaker for motor control service
//! let breaker = CircuitBreaker::new(
//!     CircuitBreakerConfig {
//!         failure_threshold: 5,           // Open after 5 failures
//!         success_threshold: 2,           // Close after 2 successes in half-open
//!         timeout: Duration::from_secs(30), // Wait 30s before trying again
//!         ..Default::default()
//!     }
//! );
//!
//! let command = Twist { linear: 1.0, angular: 0.0 };
//!
//! // Make request through circuit breaker
//! let result: MotorStatus = breaker.call(|| async {
//!     ctx.request("/motor/control", &command)
//!         .timeout(Duration::from_secs(5))
//!         .execute()
//!         .await
//! }).await?;
//! # Ok(())
//! # }
//! ```

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 {
    /// Create a new circuit breaker with the given configuration
    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())),
        }
    }

    /// Execute a potentially failing operation through the circuit breaker
    ///
    /// # Arguments
    ///
    /// * `operation` - Async function to execute
    ///
    /// # Returns
    ///
    /// - `Ok(T)` if operation succeeded
    /// - `Err(CircuitBreakerOpen)` if circuit is open (failing fast)
    /// - `Err(...)` if operation failed
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::circuit_breaker::CircuitBreaker;
    /// # async fn example(breaker: &CircuitBreaker) -> mecha10_core::Result<String> {
    /// let result = breaker.call(|| async {
    ///     // Your operation here
    ///     Ok("success".to_string())
    /// }).await?;
    /// # Ok(result)
    /// # }
    /// ```
    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
    }

    /// Handle successful operation
    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)");
            }
        }
    }

    /// Handle failed operation
    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 => {}
        }
    }

    /// Shared body behind `is_open`/`is_closed`/`is_half_open`, which only differ in which
    /// `CircuitState` variant they compare against.
    async fn is_state(&self, want: CircuitState) -> bool {
        let state = self.state.read().await;
        state.state == want
    }

    /// Check if the circuit breaker is currently open
    pub async fn is_open(&self) -> bool {
        self.is_state(CircuitState::Open).await
    }

    /// Check if the circuit breaker is currently closed
    pub async fn is_closed(&self) -> bool {
        self.is_state(CircuitState::Closed).await
    }

    /// Check if the circuit breaker is currently half-open
    pub async fn is_half_open(&self) -> bool {
        self.is_state(CircuitState::HalfOpen).await
    }

    /// Get the current state
    pub async fn state(&self) -> CircuitState {
        let state = self.state.read().await;
        state.state
    }

    /// Get statistics about the circuit breaker
    ///
    /// Returns (total_requests, successes, failures, rejected)
    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,
        )
    }

    /// Get success rate (0.0 to 1.0)
    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
        }
    }

    /// Manually reset the circuit breaker to closed state
    ///
    /// Use with caution - only when you know the service has recovered.
    pub async fn reset(&self) {
        self.force_state("reset to closed", CircuitState::Closed, None).await;
    }

    /// Manually open the circuit breaker
    ///
    /// Use for maintenance or manual intervention.
    pub async fn open(&self) {
        self.force_state("manually opened", CircuitState::Open, Some(Instant::now()))
            .await;
    }

    /// Shared implementation behind `reset`/`open`, which differ only in the target state, the
    /// `opened_at` timestamp to record, and the log message.
    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()
    }
}