Skip to main content

do_over/
retry.rs

1//! Retry policy for handling transient failures.
2//!
3//! The retry policy automatically retries failed operations with configurable
4//! backoff strategies.
5//!
6//! # Backoff Strategies
7//!
8//! - **Fixed**: Wait a constant duration between retries
9//! - **Exponential**: Increase delay exponentially with each retry
10//!
11//! # Examples
12//!
13//! ```rust
14//! use do_over::{policy::Policy, retry::RetryPolicy};
15//! use std::time::Duration;
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! // Fixed backoff: 3 retries with 100ms delay
19//! let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
20//!
21//! // Exponential backoff: 3 retries, starting at 100ms, doubling each time
22//! let policy = RetryPolicy::exponential(3, Duration::from_millis(100), 2.0);
23//!
24//! let result = policy.execute(|| async {
25//!     Ok::<_, std::io::Error>("success")
26//! }).await?;
27//! # Ok(())
28//! # }
29//! ```
30
31use std::{sync::Arc, time::Duration};
32use tokio::time::sleep;
33use crate::policy::Policy;
34use crate::metrics::Metrics;
35
36/// Backoff strategy for retry delays.
37#[derive(Clone)]
38pub enum Backoff {
39    /// Fixed delay between retries.
40    Fixed(Duration),
41    /// Exponential backoff with configurable base and factor.
42    Exponential {
43        /// Initial delay duration.
44        base: Duration,
45        /// Multiplier applied for each subsequent retry.
46        factor: f64,
47    },
48}
49
50/// A policy that retries failed operations with configurable backoff.
51///
52/// # Examples
53///
54/// ```rust
55/// use do_over::{policy::Policy, retry::RetryPolicy};
56/// use std::time::Duration;
57///
58/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
59/// let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
60///
61/// let result = policy.execute(|| async {
62///     // This operation will be retried up to 3 times on failure
63///     Ok::<_, std::io::Error>("success")
64/// }).await?;
65/// # Ok(())
66/// # }
67/// ```
68pub struct RetryPolicy {
69    max_retries: usize,
70    backoff: Backoff,
71    metrics: Option<Arc<dyn Metrics>>,
72}
73
74impl Clone for RetryPolicy {
75    fn clone(&self) -> Self {
76        Self {
77            max_retries: self.max_retries,
78            backoff: self.backoff.clone(),
79            metrics: self.metrics.clone(),
80        }
81    }
82}
83
84impl RetryPolicy {
85    /// Create a retry policy with fixed backoff.
86    ///
87    /// # Arguments
88    ///
89    /// * `max_retries` - Maximum number of retry attempts
90    /// * `delay` - Fixed delay between retries
91    ///
92    /// # Examples
93    ///
94    /// ```rust
95    /// use do_over::retry::RetryPolicy;
96    /// use std::time::Duration;
97    ///
98    /// // Retry up to 3 times with 100ms between attempts
99    /// let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
100    /// ```
101    pub fn fixed(max_retries: usize, delay: Duration) -> Self {
102        Self {
103            max_retries,
104            backoff: Backoff::Fixed(delay),
105            metrics: None,
106        }
107    }
108
109    /// Create a retry policy with exponential backoff.
110    ///
111    /// The delay for attempt `n` is calculated as: `base * factor^n`
112    ///
113    /// # Arguments
114    ///
115    /// * `max_retries` - Maximum number of retry attempts
116    /// * `base` - Initial delay duration
117    /// * `factor` - Multiplier for exponential growth (typically 2.0)
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use do_over::retry::RetryPolicy;
123    /// use std::time::Duration;
124    ///
125    /// // Delays: 100ms, 200ms, 400ms, 800ms
126    /// let policy = RetryPolicy::exponential(4, Duration::from_millis(100), 2.0);
127    /// ```
128    pub fn exponential(max_retries: usize, base: Duration, factor: f64) -> Self {
129        Self {
130            max_retries,
131            backoff: Backoff::Exponential { base, factor },
132            metrics: None,
133        }
134    }
135
136    /// Attach a metrics collector to this policy.
137    ///
138    /// # Arguments
139    ///
140    /// * `metrics` - Implementation of the `Metrics` trait
141    pub fn with_metrics(mut self, metrics: Arc<dyn Metrics>) -> Self {
142        self.metrics = Some(metrics);
143        self
144    }
145
146    fn delay(&self, attempt: usize) -> Duration {
147        match self.backoff {
148            Backoff::Fixed(d) => d,
149            Backoff::Exponential { base, factor } => {
150                base.mul_f64(factor.powi(attempt as i32))
151            }
152        }
153    }
154}
155
156#[async_trait::async_trait]
157impl<E> Policy<E> for RetryPolicy
158where
159    E: Send + Sync,
160{
161    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, E>
162    where
163        F: Fn() -> Fut + Send + Sync,
164        Fut: std::future::Future<Output = Result<T, E>> + Send,
165        T: Send,
166    {
167        let mut attempt = 0;
168        loop {
169            match f().await {
170                Ok(v) => {
171                    if let Some(m) = &self.metrics { m.on_success(); }
172                    return Ok(v);
173                }
174                Err(_e) if attempt < self.max_retries => {
175                    if let Some(m) = &self.metrics { m.on_retry(); }
176                    attempt += 1;
177                    sleep(self.delay(attempt)).await;
178                }
179                Err(e) => {
180                    if let Some(m) = &self.metrics { m.on_failure(); }
181                    return Err(e);
182                }
183            }
184        }
185    }
186}