1use crate::error::Result;
7use std::time::Duration;
8use tokio::time::sleep;
9use tracing::{debug, warn};
10
11#[derive(Debug, Clone)]
13pub struct RetryConfig {
14 pub max_attempts: u32,
16
17 pub initial_backoff: Duration,
19
20 pub max_backoff: Duration,
22
23 pub backoff_multiplier: f64,
25
26 pub respect_retry_after: bool,
28}
29
30impl Default for RetryConfig {
31 fn default() -> Self {
32 Self {
33 max_attempts: 3,
34 initial_backoff: Duration::from_millis(500),
35 max_backoff: Duration::from_secs(60),
36 backoff_multiplier: 2.0,
37 respect_retry_after: true,
38 }
39 }
40}
41
42impl RetryConfig {
43 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn with_max_attempts(mut self, attempts: u32) -> Self {
50 self.max_attempts = attempts;
51 self
52 }
53
54 pub fn with_initial_backoff(mut self, duration: Duration) -> Self {
56 self.initial_backoff = duration;
57 self
58 }
59
60 pub fn with_max_backoff(mut self, duration: Duration) -> Self {
62 self.max_backoff = duration;
63 self
64 }
65
66 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
68 self.backoff_multiplier = multiplier;
69 self
70 }
71
72 fn calculate_backoff(&self, attempt: u32, retry_after: Option<u64>) -> Duration {
74 if self.respect_retry_after {
76 if let Some(seconds) = retry_after {
77 return Duration::from_secs(seconds).min(self.max_backoff);
78 }
79 }
80
81 let backoff_secs =
83 self.initial_backoff.as_secs_f64() * self.backoff_multiplier.powi(attempt as i32);
84
85 Duration::from_secs_f64(backoff_secs.min(self.max_backoff.as_secs_f64()))
86 }
87}
88
89pub async fn retry_with_backoff<F, Fut, T>(config: RetryConfig, mut operation: F) -> Result<T>
107where
108 F: FnMut() -> Fut,
109 Fut: std::future::Future<Output = Result<T>>,
110{
111 let mut attempt = 0;
112
113 loop {
114 attempt += 1;
115
116 debug!("Attempt {}/{}", attempt, config.max_attempts);
117
118 match operation().await {
119 Ok(result) => {
120 if attempt > 1 {
121 debug!("Request succeeded after {} attempts", attempt);
122 }
123 return Ok(result);
124 }
125 Err(error) => {
126 if !error.is_retryable() {
128 debug!("Error is not retryable: {:?}", error);
129 return Err(error);
130 }
131
132 if attempt >= config.max_attempts {
134 warn!(
135 "Max retry attempts ({}) reached, failing",
136 config.max_attempts
137 );
138 return Err(error);
139 }
140
141 let retry_after = error.retry_after();
143 let backoff = config.calculate_backoff(attempt - 1, retry_after);
144
145 warn!(
146 "Request failed (attempt {}/{}): {:?}. Retrying in {:?}",
147 attempt, config.max_attempts, error, backoff
148 );
149
150 sleep(backoff).await;
152 }
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::error::Error;
161 use std::sync::atomic::{AtomicU32, Ordering};
162 use std::sync::Arc;
163
164 #[test]
165 fn test_default_config() {
166 let config = RetryConfig::default();
167 assert_eq!(config.max_attempts, 3);
168 assert_eq!(config.initial_backoff, Duration::from_millis(500));
169 assert_eq!(config.max_backoff, Duration::from_secs(60));
170 }
171
172 #[test]
173 fn test_config_builder() {
174 let config = RetryConfig::new()
175 .with_max_attempts(5)
176 .with_initial_backoff(Duration::from_secs(1))
177 .with_max_backoff(Duration::from_secs(30));
178
179 assert_eq!(config.max_attempts, 5);
180 assert_eq!(config.initial_backoff, Duration::from_secs(1));
181 assert_eq!(config.max_backoff, Duration::from_secs(30));
182 }
183
184 #[test]
185 fn test_calculate_backoff() {
186 let config = RetryConfig::new()
187 .with_initial_backoff(Duration::from_secs(1))
188 .with_backoff_multiplier(2.0);
189
190 assert_eq!(config.calculate_backoff(0, None), Duration::from_secs(1));
192
193 assert_eq!(config.calculate_backoff(1, None), Duration::from_secs(2));
195
196 assert_eq!(config.calculate_backoff(2, None), Duration::from_secs(4));
198 }
199
200 #[test]
201 fn test_respect_retry_after() {
202 let config = RetryConfig::new().with_initial_backoff(Duration::from_secs(1));
203
204 let backoff = config.calculate_backoff(0, Some(10));
206 assert_eq!(backoff, Duration::from_secs(10));
207 }
208
209 #[test]
210 fn test_max_backoff_cap() {
211 let config = RetryConfig::new()
212 .with_initial_backoff(Duration::from_secs(1))
213 .with_max_backoff(Duration::from_secs(5))
214 .with_backoff_multiplier(10.0);
215
216 let backoff = config.calculate_backoff(10, None);
218 assert!(backoff <= Duration::from_secs(5));
219 }
220
221 #[tokio::test]
222 async fn test_retry_succeeds_first_attempt() {
223 let config = RetryConfig::new();
224 let call_count = Arc::new(AtomicU32::new(0));
225 let count = call_count.clone();
226
227 let result = retry_with_backoff(config, || {
228 let count = count.clone();
229 async move {
230 count.fetch_add(1, Ordering::SeqCst);
231 Ok::<_, Error>("success")
232 }
233 })
234 .await;
235
236 assert!(result.is_ok());
237 assert_eq!(call_count.load(Ordering::SeqCst), 1);
238 }
239
240 #[tokio::test]
241 async fn test_retry_succeeds_after_failures() {
242 let config = RetryConfig::new().with_initial_backoff(Duration::from_millis(10));
243 let call_count = Arc::new(AtomicU32::new(0));
244 let count = call_count.clone();
245
246 let result = retry_with_backoff(config, || {
247 let count = count.clone();
248 async move {
249 let current = count.fetch_add(1, Ordering::SeqCst) + 1;
250 if current < 3 {
251 Err(Error::Server {
252 status: 503,
253 message: "Service unavailable".into(),
254 })
255 } else {
256 Ok::<_, Error>("success")
257 }
258 }
259 })
260 .await;
261
262 assert!(result.is_ok());
263 assert_eq!(call_count.load(Ordering::SeqCst), 3);
264 }
265
266 #[tokio::test]
267 async fn test_retry_fails_non_retryable() {
268 let config = RetryConfig::new();
269 let call_count = Arc::new(AtomicU32::new(0));
270 let count = call_count.clone();
271
272 let result = retry_with_backoff(config, || {
273 let count = count.clone();
274 async move {
275 count.fetch_add(1, Ordering::SeqCst);
276 Err::<String, _>(Error::Authentication("Bad key".into()))
277 }
278 })
279 .await;
280
281 assert!(result.is_err());
282 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
284
285 #[tokio::test]
286 async fn test_retry_exhausts_attempts() {
287 let config = RetryConfig::new()
288 .with_max_attempts(2)
289 .with_initial_backoff(Duration::from_millis(10));
290 let call_count = Arc::new(AtomicU32::new(0));
291 let count = call_count.clone();
292
293 let result = retry_with_backoff(config, || {
294 let count = count.clone();
295 async move {
296 count.fetch_add(1, Ordering::SeqCst);
297 Err::<String, _>(Error::Server {
298 status: 500,
299 message: "Error".into(),
300 })
301 }
302 })
303 .await;
304
305 assert!(result.is_err());
306 assert_eq!(call_count.load(Ordering::SeqCst), 2);
307 }
308}