do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Circuit breaker policy for preventing cascading failures.
//!
//! The circuit breaker monitors failures and temporarily stops calling a failing
//! service, giving it time to recover.
//!
//! # States
//!
//! - **Closed**: Normal operation, requests flow through. Failures are counted.
//! - **Open**: Circuit is tripped. Requests fail immediately without calling the service.
//! - **Half-Open**: After reset timeout, one test request is allowed through.
//!
//! # Examples
//!
//! ```rust
//! use do_over::{policy::Policy, circuit_breaker::CircuitBreaker, error::DoOverError};
//! use std::time::Duration;
//!
//! # async fn example() -> Result<(), DoOverError<std::io::Error>> {
//! // Open circuit after 5 failures, reset after 30 seconds
//! let breaker = CircuitBreaker::new(5, Duration::from_secs(30));
//!
//! match breaker.execute(|| async {
//!     Ok::<_, DoOverError<std::io::Error>>("success")
//! }).await {
//!     Ok(result) => println!("Success: {}", result),
//!     Err(DoOverError::CircuitOpen) => println!("Circuit is open"),
//!     Err(e) => println!("Error: {:?}", e),
//! }
//! # Ok(())
//! # }
//! ```

use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::policy::Policy;
use crate::error::DoOverError;

/// Internal circuit breaker state.
#[derive(Clone, Copy)]
enum State {
    /// Normal operation - requests pass through.
    Closed,
    /// Circuit is open - requests fail immediately.
    Open,
    /// Testing if service recovered - one request allowed.
    HalfOpen,
}

/// A circuit breaker that prevents cascading failures.
///
/// The circuit breaker tracks consecutive failures and "trips" when the failure
/// threshold is reached, causing subsequent requests to fail immediately without
/// calling the underlying service.
///
/// # State Transitions
///
/// ```text
/// Closed --[failures >= threshold]--> Open
/// Open --[reset_timeout elapsed]--> HalfOpen
/// HalfOpen --[success]--> Closed
/// HalfOpen --[failure]--> Open
/// ```
///
/// # Examples
///
/// ```rust
/// use do_over::{policy::Policy, circuit_breaker::CircuitBreaker, error::DoOverError};
/// use std::time::Duration;
///
/// # async fn example() {
/// let breaker = CircuitBreaker::new(3, Duration::from_secs(60));
///
/// // Use the breaker to protect calls to an external service
/// let result: Result<String, DoOverError<String>> = breaker.execute(|| async {
///     Ok("response".to_string())
/// }).await;
/// # }
/// ```
pub struct CircuitBreaker {
    failure_threshold: usize,
    reset_timeout: Duration,
    failures: AtomicUsize,
    opened_at: RwLock<Option<Instant>>,
    state: RwLock<State>,
}

impl Clone for CircuitBreaker {
    fn clone(&self) -> Self {
        Self {
            failure_threshold: self.failure_threshold,
            reset_timeout: self.reset_timeout,
            failures: AtomicUsize::new(self.failures.load(Ordering::Relaxed)),
            opened_at: RwLock::new(*self.opened_at.blocking_read()),
            state: RwLock::new(*self.state.blocking_read()),
        }
    }
}

impl CircuitBreaker {
    /// Create a new circuit breaker.
    ///
    /// # Arguments
    ///
    /// * `failure_threshold` - Number of consecutive failures before the circuit opens
    /// * `reset_timeout` - How long to wait before transitioning from Open to Half-Open
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::circuit_breaker::CircuitBreaker;
    /// use std::time::Duration;
    ///
    /// // Open after 5 failures, wait 60 seconds before testing recovery
    /// let breaker = CircuitBreaker::new(5, Duration::from_secs(60));
    /// ```
    pub fn new(failure_threshold: usize, reset_timeout: Duration) -> Self {
        Self {
            failure_threshold,
            reset_timeout,
            failures: AtomicUsize::new(0),
            opened_at: RwLock::new(None),
            state: RwLock::new(State::Closed),
        }
    }
}

#[async_trait::async_trait]
impl<E> Policy<DoOverError<E>> for CircuitBreaker
where
    E: Send + Sync,
{
    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, DoOverError<E>>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: std::future::Future<Output = Result<T, DoOverError<E>>> + Send,
        T: Send,
    {
        {
            let state = self.state.read().await;
            if matches!(*state, State::Open) {
                let opened = self.opened_at.read().await;
                if let Some(t) = *opened {
                    if t.elapsed() >= self.reset_timeout {
                        drop(opened);
                        *self.state.write().await = State::HalfOpen;
                    } else {
                        return Err(DoOverError::CircuitOpen);
                    }
                }
            }
        }

        match f().await {
            Ok(v) => {
                self.failures.store(0, Ordering::Relaxed);
                *self.state.write().await = State::Closed;
                Ok(v)
            }
            Err(e) => {
                let count = self.failures.fetch_add(1, Ordering::Relaxed) + 1;
                if count >= self.failure_threshold {
                    *self.state.write().await = State::Open;
                    *self.opened_at.write().await = Some(Instant::now());
                }
                Err(e)
            }
        }
    }
}