1use std::{fmt::Display, future::Future};
7
8use crate::time::{Duration, sleep};
9use rand::RngExt;
10use tracing::{debug, warn};
11
12#[derive(Debug, Clone)]
14pub struct RetryConfig {
15 pub max_retries: usize,
17 pub initial_delay: Duration,
19 pub max_delay: Duration,
21 pub backoff_base: f64,
23 pub use_jitter: bool,
25}
26
27impl Default for RetryConfig {
28 fn default() -> Self {
29 Self {
30 max_retries: 3,
31 initial_delay: Duration::from_secs(1),
32 max_delay: Duration::from_secs(60),
33 backoff_base: 2.0,
34 use_jitter: true,
35 }
36 }
37}
38
39impl RetryConfig {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
47 self.max_retries = max_retries;
48 self
49 }
50
51 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
53 self.initial_delay = delay;
54 self
55 }
56
57 pub fn with_max_delay(mut self, delay: Duration) -> Self {
59 self.max_delay = delay;
60 self
61 }
62
63 pub fn without_jitter(mut self) -> Self {
65 self.use_jitter = false;
66 self
67 }
68
69 fn calculate_delay(&self, attempt: usize) -> Duration {
71 let base_delay = self.initial_delay.as_millis() as f64;
72 let exponential_delay = base_delay * self.backoff_base.powi(attempt as i32);
73 let capped_delay = exponential_delay.min(self.max_delay.as_millis() as f64);
74
75 let final_delay = if self.use_jitter {
76 let mut rng = rand::rng();
78 let jitter_factor = rng.random_range(0.5..1.5);
79 capped_delay * jitter_factor
80 } else {
81 capped_delay
82 };
83
84 Duration::from_millis(final_delay as u64)
85 }
86}
87
88pub trait RetryableError {
90 fn is_retryable(&self) -> bool;
92
93 fn retry_reason(&self) -> &str {
95 if self.is_retryable() {
96 "Transient error, will retry"
97 } else {
98 "Non-transient error, will not retry"
99 }
100 }
101}
102
103pub async fn with_retry<F, Fut, T, E>(
115 mut operation: F,
116 config: &RetryConfig,
117 operation_name: &str,
118) -> Result<T, E>
119where
120 F: FnMut() -> Fut,
121 Fut: Future<Output = Result<T, E>>,
122 E: RetryableError + Display,
123{
124 let mut attempt = 0;
125
126 loop {
127 debug!(
128 operation = operation_name,
129 attempt = attempt,
130 max_retries = config.max_retries,
131 "Attempting operation"
132 );
133
134 match operation().await {
135 Ok(result) => {
136 if attempt > 0 {
137 debug!(
138 operation = operation_name,
139 attempt = attempt,
140 "Operation succeeded after retry"
141 );
142 }
143 return Ok(result);
144 }
145 Err(error) => {
146 debug!(
147 operation = operation_name,
148 error = %error,
149 is_retryable = error.is_retryable(),
150 reason = error.retry_reason(),
151 "Error encountered"
152 );
153
154 if !error.is_retryable() {
155 debug!(
156 operation = operation_name,
157 error = %error,
158 reason = error.retry_reason(),
159 "Non-retryable error encountered"
160 );
161 return Err(error);
162 }
163
164 if attempt >= config.max_retries {
165 warn!(
166 operation = operation_name,
167 attempts = attempt + 1,
168 error = %error,
169 "Max retries exceeded, operation failed"
170 );
171 return Err(error);
172 }
173
174 let delay = config.calculate_delay(attempt);
175 debug!(
176 operation = operation_name,
177 attempt = attempt + 1,
178 max_retries = config.max_retries,
179 delay_ms = delay.as_millis(),
180 error = %error,
181 "Retryable error encountered, will retry after delay"
182 );
183 sleep(delay).await;
184 }
185 }
186
187 attempt += 1;
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use std::sync::Arc;
195 use std::sync::atomic::{AtomicUsize, Ordering};
196
197 #[derive(Debug, thiserror::Error)]
198 enum TestError {
199 #[error("Retryable error")]
200 Retryable,
201 #[error("Non-retryable error")]
202 NonRetryable,
203 }
204
205 impl RetryableError for TestError {
206 fn is_retryable(&self) -> bool {
207 matches!(self, TestError::Retryable)
208 }
209 }
210
211 #[tokio::test]
212 async fn test_successful_operation() {
213 let config = RetryConfig::new().without_jitter();
214 let counter = Arc::new(AtomicUsize::new(0));
215 let counter_clone = counter.clone();
216
217 let result = with_retry(
218 || {
219 counter_clone.fetch_add(1, Ordering::SeqCst);
220 async { Ok::<_, TestError>(42) }
221 },
222 &config,
223 "test_operation",
224 )
225 .await;
226
227 assert_eq!(result.unwrap(), 42);
228 assert_eq!(counter.load(Ordering::SeqCst), 1); }
230
231 #[tokio::test]
232 async fn test_retry_then_success() {
233 let config = RetryConfig::new()
234 .with_max_retries(3)
235 .with_initial_delay(Duration::from_millis(10))
236 .without_jitter();
237
238 let counter = Arc::new(AtomicUsize::new(0));
239 let counter_clone = counter.clone();
240
241 let result = with_retry(
242 || {
243 let count = counter_clone.fetch_add(1, Ordering::SeqCst);
244 async move {
245 if count < 2 {
246 Err(TestError::Retryable)
247 } else {
248 Ok(42)
249 }
250 }
251 },
252 &config,
253 "test_operation",
254 )
255 .await;
256
257 assert_eq!(result.unwrap(), 42);
258 assert_eq!(counter.load(Ordering::SeqCst), 3); }
260
261 #[tokio::test]
262 async fn test_non_retryable_error() {
263 let config = RetryConfig::new().with_max_retries(3);
264 let counter = Arc::new(AtomicUsize::new(0));
265 let counter_clone = counter.clone();
266
267 let result = with_retry(
268 || {
269 counter_clone.fetch_add(1, Ordering::SeqCst);
270 async { Err::<i32, _>(TestError::NonRetryable) }
271 },
272 &config,
273 "test_operation",
274 )
275 .await;
276
277 assert!(matches!(result, Err(TestError::NonRetryable)));
278 assert_eq!(counter.load(Ordering::SeqCst), 1); }
280
281 #[tokio::test]
282 async fn test_max_retries_exceeded() {
283 let config = RetryConfig::new()
284 .with_max_retries(2)
285 .with_initial_delay(Duration::from_millis(10))
286 .without_jitter();
287
288 let counter = Arc::new(AtomicUsize::new(0));
289 let counter_clone = counter.clone();
290
291 let result = with_retry(
292 || {
293 counter_clone.fetch_add(1, Ordering::SeqCst);
294 async { Err::<i32, _>(TestError::Retryable) }
295 },
296 &config,
297 "test_operation",
298 )
299 .await;
300
301 assert!(matches!(result, Err(TestError::Retryable)));
302 assert_eq!(counter.load(Ordering::SeqCst), 3); }
304
305 #[test]
306 fn test_exponential_backoff_calculation() {
307 let config = RetryConfig::new()
308 .with_initial_delay(Duration::from_secs(1))
309 .with_max_delay(Duration::from_secs(30))
310 .without_jitter();
311
312 assert_eq!(config.calculate_delay(0), Duration::from_secs(1));
314 assert_eq!(config.calculate_delay(1), Duration::from_secs(2));
315 assert_eq!(config.calculate_delay(2), Duration::from_secs(4));
316 assert_eq!(config.calculate_delay(3), Duration::from_secs(8));
317 assert_eq!(config.calculate_delay(4), Duration::from_secs(16));
318
319 assert_eq!(config.calculate_delay(5), Duration::from_secs(30)); assert_eq!(config.calculate_delay(10), Duration::from_secs(30)); }
323
324 #[test]
325 fn test_jitter() {
326 let config = RetryConfig::new().with_initial_delay(Duration::from_secs(1));
327
328 let delay1 = config.calculate_delay(1);
330 let delay2 = config.calculate_delay(1);
331
332 assert!(delay1.as_millis() >= 1000);
334 assert!(delay1.as_millis() <= 3000);
335 assert!(delay2.as_millis() >= 1000);
336 assert!(delay2.as_millis() <= 3000);
337
338 }
341}