do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Retry policy for handling transient failures.
//!
//! The retry policy automatically retries failed operations with configurable
//! backoff strategies.
//!
//! # Backoff Strategies
//!
//! - **Fixed**: Wait a constant duration between retries
//! - **Exponential**: Increase delay exponentially with each retry
//!
//! # Examples
//!
//! ```rust
//! use do_over::{policy::Policy, retry::RetryPolicy};
//! use std::time::Duration;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Fixed backoff: 3 retries with 100ms delay
//! let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
//!
//! // Exponential backoff: 3 retries, starting at 100ms, doubling each time
//! let policy = RetryPolicy::exponential(3, Duration::from_millis(100), 2.0);
//!
//! let result = policy.execute(|| async {
//!     Ok::<_, std::io::Error>("success")
//! }).await?;
//! # Ok(())
//! # }
//! ```

use std::{sync::Arc, time::Duration};
use tokio::time::sleep;
use crate::policy::Policy;
use crate::metrics::Metrics;

/// Backoff strategy for retry delays.
#[derive(Clone)]
pub enum Backoff {
    /// Fixed delay between retries.
    Fixed(Duration),
    /// Exponential backoff with configurable base and factor.
    Exponential {
        /// Initial delay duration.
        base: Duration,
        /// Multiplier applied for each subsequent retry.
        factor: f64,
    },
}

/// A policy that retries failed operations with configurable backoff.
///
/// # Examples
///
/// ```rust
/// use do_over::{policy::Policy, retry::RetryPolicy};
/// use std::time::Duration;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
///
/// let result = policy.execute(|| async {
///     // This operation will be retried up to 3 times on failure
///     Ok::<_, std::io::Error>("success")
/// }).await?;
/// # Ok(())
/// # }
/// ```
pub struct RetryPolicy {
    max_retries: usize,
    backoff: Backoff,
    metrics: Option<Arc<dyn Metrics>>,
}

impl Clone for RetryPolicy {
    fn clone(&self) -> Self {
        Self {
            max_retries: self.max_retries,
            backoff: self.backoff.clone(),
            metrics: self.metrics.clone(),
        }
    }
}

impl RetryPolicy {
    /// Create a retry policy with fixed backoff.
    ///
    /// # Arguments
    ///
    /// * `max_retries` - Maximum number of retry attempts
    /// * `delay` - Fixed delay between retries
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::retry::RetryPolicy;
    /// use std::time::Duration;
    ///
    /// // Retry up to 3 times with 100ms between attempts
    /// let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
    /// ```
    pub fn fixed(max_retries: usize, delay: Duration) -> Self {
        Self {
            max_retries,
            backoff: Backoff::Fixed(delay),
            metrics: None,
        }
    }

    /// Create a retry policy with exponential backoff.
    ///
    /// The delay for attempt `n` is calculated as: `base * factor^n`
    ///
    /// # Arguments
    ///
    /// * `max_retries` - Maximum number of retry attempts
    /// * `base` - Initial delay duration
    /// * `factor` - Multiplier for exponential growth (typically 2.0)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::retry::RetryPolicy;
    /// use std::time::Duration;
    ///
    /// // Delays: 100ms, 200ms, 400ms, 800ms
    /// let policy = RetryPolicy::exponential(4, Duration::from_millis(100), 2.0);
    /// ```
    pub fn exponential(max_retries: usize, base: Duration, factor: f64) -> Self {
        Self {
            max_retries,
            backoff: Backoff::Exponential { base, factor },
            metrics: None,
        }
    }

    /// Attach a metrics collector to this policy.
    ///
    /// # Arguments
    ///
    /// * `metrics` - Implementation of the `Metrics` trait
    pub fn with_metrics(mut self, metrics: Arc<dyn Metrics>) -> Self {
        self.metrics = Some(metrics);
        self
    }

    fn delay(&self, attempt: usize) -> Duration {
        match self.backoff {
            Backoff::Fixed(d) => d,
            Backoff::Exponential { base, factor } => {
                base.mul_f64(factor.powi(attempt as i32))
            }
        }
    }
}

#[async_trait::async_trait]
impl<E> Policy<E> for RetryPolicy
where
    E: Send + Sync,
{
    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, E>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: std::future::Future<Output = Result<T, E>> + Send,
        T: Send,
    {
        let mut attempt = 0;
        loop {
            match f().await {
                Ok(v) => {
                    if let Some(m) = &self.metrics { m.on_success(); }
                    return Ok(v);
                }
                Err(_e) if attempt < self.max_retries => {
                    if let Some(m) = &self.metrics { m.on_retry(); }
                    attempt += 1;
                    sleep(self.delay(attempt)).await;
                }
                Err(e) => {
                    if let Some(m) = &self.metrics { m.on_failure(); }
                    return Err(e);
                }
            }
        }
    }
}