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}