1use crate::error::Error;
2use reqwest::header::HeaderMap;
3use std::time::{Duration, Instant, SystemTime};
4use tokio::time::sleep;
5
6#[derive(Debug, Clone)]
8pub struct RetryConfig {
9 pub max_attempts: usize,
10 pub initial_delay_ms: u64,
11 pub max_delay_ms: u64,
12 pub backoff_multiplier: f64,
13 pub jitter: bool,
14}
15
16impl Default for RetryConfig {
17 fn default() -> Self {
18 Self {
19 max_attempts: 3,
20 initial_delay_ms: 100,
21 max_delay_ms: 5000,
22 backoff_multiplier: 2.0,
23 jitter: true,
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
30pub struct TimeoutConfig {
31 pub connect_timeout_ms: u64,
32 pub request_timeout_ms: u64,
33}
34
35impl Default for TimeoutConfig {
36 fn default() -> Self {
37 Self {
38 connect_timeout_ms: 10_000, request_timeout_ms: 30_000, }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct RetryInfo {
47 pub attempt: u32,
49 pub status_code: Option<u16>,
51 pub delay_ms: u64,
53 pub reason: String,
55}
56
57impl RetryInfo {
58 #[must_use]
60 pub fn new(
61 attempt: u32,
62 status_code: Option<u16>,
63 delay_ms: u64,
64 reason: impl Into<String>,
65 ) -> Self {
66 Self {
67 attempt,
68 status_code,
69 delay_ms,
70 reason: reason.into(),
71 }
72 }
73}
74
75#[derive(Debug)]
77pub struct RetryResult<T> {
78 pub result: Result<T, Error>,
80 pub retry_history: Vec<RetryInfo>,
82 pub total_attempts: u32,
84}
85
86#[must_use]
94pub fn parse_retry_after_header(headers: &HeaderMap) -> Option<Duration> {
95 let retry_after = headers.get("retry-after")?;
96 let value = retry_after.to_str().ok()?;
97 parse_retry_after_value(value)
98}
99
100#[must_use]
109pub fn parse_retry_after_value(value: &str) -> Option<Duration> {
110 if let Ok(seconds) = value.parse::<u64>() {
112 return Some(Duration::from_secs(seconds));
113 }
114
115 httpdate::parse_http_date(value)
119 .ok()
120 .and_then(|date| date.duration_since(SystemTime::now()).ok())
121}
122
123#[must_use]
128#[allow(
129 clippy::cast_precision_loss,
130 clippy::cast_possible_truncation,
131 clippy::cast_sign_loss,
132 clippy::cast_possible_wrap
133)]
134pub fn calculate_retry_delay_with_header(
135 config: &RetryConfig,
136 attempt: usize,
137 retry_after: Option<Duration>,
138) -> Duration {
139 let calculated_delay = calculate_retry_delay(config, attempt);
140
141 retry_after.map_or(calculated_delay, |server_delay| {
142 let delay = calculated_delay.max(server_delay);
144 let max_delay = Duration::from_millis(config.max_delay_ms);
146 delay.min(max_delay)
147 })
148}
149
150#[must_use]
152pub fn is_retryable_error(error: &reqwest::Error) -> bool {
153 if error.is_connect() {
155 return true;
156 }
157
158 if error.is_timeout() {
160 return true;
161 }
162
163 error
165 .status()
166 .is_none_or(|status| is_retryable_status(status.as_u16()))
167}
168
169#[must_use]
176pub const fn is_retryable_status(status: u16) -> bool {
177 match status {
178 408 | 429 => true, 500..=599 => !matches!(status, 501 | 505), _ => false, }
186}
187
188#[must_use]
190#[allow(
191 clippy::cast_precision_loss,
192 clippy::cast_possible_truncation,
193 clippy::cast_sign_loss,
194 clippy::cast_possible_wrap
195)]
196pub fn calculate_retry_delay(config: &RetryConfig, attempt: usize) -> Duration {
197 let base_delay = config.initial_delay_ms as f64;
198 let attempt_i32 = attempt.min(30) as i32; let delay_ms =
200 (base_delay * config.backoff_multiplier.powi(attempt_i32)).min(config.max_delay_ms as f64);
201
202 let final_delay_ms = if config.jitter {
203 let jitter_factor = fastrand::f64().mul_add(0.25, 1.0);
205 delay_ms * jitter_factor
206 } else {
207 delay_ms
208 } as u64;
209
210 Duration::from_millis(final_delay_ms)
211}
212
213pub async fn execute_with_retry<F, Fut, T>(
218 config: &RetryConfig,
219 _operation_name: &str,
220 mut operation: F,
221) -> Result<T, Error>
222where
223 F: FnMut() -> Fut,
224 Fut: std::future::Future<Output = Result<T, reqwest::Error>>,
225{
226 let _start_time = Instant::now();
227 let mut last_error = None;
228
229 for attempt in 0..config.max_attempts {
230 match operation().await {
231 Ok(result) => {
232 return Ok(result);
234 }
235 Err(error) => {
236 let is_last_attempt = attempt + 1 >= config.max_attempts;
237 let is_retryable = is_retryable_error(&error);
238
239 if !is_retryable {
241 let error_message = error.to_string();
242 return Err(Error::transient_network_error(error_message, false));
243 }
244
245 if is_last_attempt {
247 let error_message = error.to_string();
248 last_error = Some(error_message);
249 break;
250 }
251
252 let delay = calculate_retry_delay(config, attempt);
254
255 sleep(delay).await;
256 last_error = Some(error.to_string());
257 }
258 }
259 }
260
261 Err(Error::retry_limit_exceeded(
262 config.max_attempts.try_into().unwrap_or(u32::MAX),
263 last_error.unwrap_or_else(|| "Unknown error".to_string()),
264 ))
265}
266
267#[allow(clippy::cast_possible_truncation)]
272pub async fn execute_with_retry_tracking<F, Fut, T>(
273 config: &RetryConfig,
274 operation_name: &str,
275 mut operation: F,
276) -> RetryResult<T>
277where
278 F: FnMut() -> Fut,
279 Fut: std::future::Future<Output = Result<T, reqwest::Error>>,
280{
281 let mut retry_history = Vec::new();
282 let mut last_error = None;
283
284 for attempt in 0..config.max_attempts {
285 match operation().await {
286 Ok(result) => {
287 return RetryResult {
288 result: Ok(result),
289 retry_history,
290 total_attempts: (attempt + 1) as u32,
291 };
292 }
293 Err(error) => {
294 let is_last_attempt = attempt + 1 >= config.max_attempts;
295 let is_retryable = is_retryable_error(&error);
296 let status_code = error.status().map(|s| s.as_u16());
297 let error_message = error.to_string();
298
299 if !is_retryable {
301 return RetryResult {
302 result: Err(Error::transient_network_error(error_message, false)),
303 retry_history,
304 total_attempts: (attempt + 1) as u32,
305 };
306 }
307
308 if is_last_attempt {
310 last_error = Some(error_message);
311 break;
312 }
313
314 let delay = calculate_retry_delay(config, attempt);
316 let delay_ms = delay.as_millis() as u64;
317
318 retry_history.push(RetryInfo::new(
320 (attempt + 1) as u32,
321 status_code,
322 delay_ms,
323 format!("{operation_name}: {error_message}"),
324 ));
325
326 sleep(delay).await;
328 last_error = Some(error_message);
329 }
330 }
331 }
332
333 RetryResult {
334 result: Err(Error::retry_limit_exceeded(
335 config.max_attempts.try_into().unwrap_or(u32::MAX),
336 last_error.unwrap_or_else(|| "Unknown error".to_string()),
337 )),
338 retry_history,
339 total_attempts: config.max_attempts as u32,
340 }
341}
342
343pub fn create_resilient_client(timeout_config: &TimeoutConfig) -> Result<reqwest::Client, Error> {
348 reqwest::Client::builder()
349 .connect_timeout(Duration::from_millis(timeout_config.connect_timeout_ms))
350 .timeout(Duration::from_millis(timeout_config.request_timeout_ms))
351 .build()
352 .map_err(|e| {
353 Error::network_request_failed(format!("Failed to create resilient HTTP client: {e}"))
354 })
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_calculate_retry_delay() {
363 let config = RetryConfig {
364 max_attempts: 5,
365 initial_delay_ms: 100,
366 max_delay_ms: 1000,
367 backoff_multiplier: 2.0,
368 jitter: false,
369 };
370
371 let delay1 = calculate_retry_delay(&config, 0);
372 let delay2 = calculate_retry_delay(&config, 1);
373 let delay3 = calculate_retry_delay(&config, 2);
374
375 assert_eq!(delay1.as_millis(), 100);
376 assert_eq!(delay2.as_millis(), 200);
377 assert_eq!(delay3.as_millis(), 400);
378
379 let delay_max = calculate_retry_delay(&config, 10);
381 assert_eq!(delay_max.as_millis(), 1000);
382 }
383
384 #[test]
385 fn test_calculate_retry_delay_with_jitter() {
386 let config = RetryConfig {
387 max_attempts: 3,
388 initial_delay_ms: 100,
389 max_delay_ms: 1000,
390 backoff_multiplier: 2.0,
391 jitter: true,
392 };
393
394 let delay1 = calculate_retry_delay(&config, 0);
395 let delay2 = calculate_retry_delay(&config, 0);
396
397 assert!(delay1.as_millis() >= 100 && delay1.as_millis() <= 125);
400 assert!(delay2.as_millis() >= 100 && delay2.as_millis() <= 125);
401 }
402
403 #[test]
404 fn test_default_configs() {
405 let retry_config = RetryConfig::default();
406 assert_eq!(retry_config.max_attempts, 3);
407 assert_eq!(retry_config.initial_delay_ms, 100);
408
409 let timeout_config = TimeoutConfig::default();
410 assert_eq!(timeout_config.connect_timeout_ms, 10_000);
411 assert_eq!(timeout_config.request_timeout_ms, 30_000);
412 }
413
414 #[test]
415 fn test_parse_retry_after_header_seconds() {
416 let mut headers = HeaderMap::new();
417 headers.insert("retry-after", "120".parse().unwrap());
418
419 let duration = parse_retry_after_header(&headers);
420 assert_eq!(duration, Some(Duration::from_secs(120)));
421 }
422
423 #[test]
424 fn test_parse_retry_after_header_zero() {
425 let mut headers = HeaderMap::new();
426 headers.insert("retry-after", "0".parse().unwrap());
427
428 let duration = parse_retry_after_header(&headers);
429 assert_eq!(duration, Some(Duration::from_secs(0)));
430 }
431
432 #[test]
433 fn test_parse_retry_after_header_missing() {
434 let headers = HeaderMap::new();
435
436 let duration = parse_retry_after_header(&headers);
437 assert_eq!(duration, None);
438 }
439
440 #[test]
441 fn test_parse_retry_after_header_invalid() {
442 let mut headers = HeaderMap::new();
443 headers.insert("retry-after", "not-a-number".parse().unwrap());
444
445 let duration = parse_retry_after_header(&headers);
446 assert_eq!(duration, None);
448 }
449
450 #[test]
451 fn test_calculate_retry_delay_with_header_none() {
452 let config = RetryConfig {
453 max_attempts: 3,
454 initial_delay_ms: 100,
455 max_delay_ms: 5000,
456 backoff_multiplier: 2.0,
457 jitter: false,
458 };
459
460 let delay = calculate_retry_delay_with_header(&config, 0, None);
461 assert_eq!(delay.as_millis(), 100);
462 }
463
464 #[test]
465 fn test_calculate_retry_delay_with_header_uses_server_delay_when_larger() {
466 let config = RetryConfig {
467 max_attempts: 3,
468 initial_delay_ms: 100,
469 max_delay_ms: 5000,
470 backoff_multiplier: 2.0,
471 jitter: false,
472 };
473
474 let retry_after = Some(Duration::from_secs(3));
476 let delay = calculate_retry_delay_with_header(&config, 0, retry_after);
477 assert_eq!(delay.as_secs(), 3);
478 }
479
480 #[test]
481 fn test_calculate_retry_delay_with_header_uses_calculated_when_larger() {
482 let config = RetryConfig {
483 max_attempts: 3,
484 initial_delay_ms: 5000,
485 max_delay_ms: 30_000,
486 backoff_multiplier: 2.0,
487 jitter: false,
488 };
489
490 let retry_after = Some(Duration::from_secs(1));
492 let delay = calculate_retry_delay_with_header(&config, 0, retry_after);
493 assert_eq!(delay.as_millis(), 5000);
494 }
495
496 #[test]
497 fn test_calculate_retry_delay_with_header_caps_at_max() {
498 let config = RetryConfig {
499 max_attempts: 3,
500 initial_delay_ms: 100,
501 max_delay_ms: 5000,
502 backoff_multiplier: 2.0,
503 jitter: false,
504 };
505
506 let retry_after = Some(Duration::from_secs(60));
508 let delay = calculate_retry_delay_with_header(&config, 0, retry_after);
509 assert_eq!(delay.as_millis(), 5000);
510 }
511
512 #[test]
513 fn test_retry_info_new() {
514 let info = RetryInfo::new(1, Some(429), 500, "Rate limited");
515 assert_eq!(info.attempt, 1);
516 assert_eq!(info.status_code, Some(429));
517 assert_eq!(info.delay_ms, 500);
518 assert_eq!(info.reason, "Rate limited");
519 }
520
521 #[test]
522 fn test_retry_info_without_status_code() {
523 let info = RetryInfo::new(2, None, 1000, "Connection refused");
524 assert_eq!(info.attempt, 2);
525 assert_eq!(info.status_code, None);
526 assert_eq!(info.delay_ms, 1000);
527 assert_eq!(info.reason, "Connection refused");
528 }
529
530 #[test]
531 fn test_retry_result_success_no_retries() {
532 let result: RetryResult<i32> = RetryResult {
533 result: Ok(42),
534 retry_history: vec![],
535 total_attempts: 1,
536 };
537 assert!(result.result.is_ok());
538 assert!(result.retry_history.is_empty());
539 assert_eq!(result.total_attempts, 1);
540 }
541
542 #[test]
543 fn test_retry_result_success_after_retries() {
544 let result: RetryResult<i32> = RetryResult {
545 result: Ok(42),
546 retry_history: vec![RetryInfo::new(1, Some(503), 100, "Service unavailable")],
547 total_attempts: 2,
548 };
549 assert!(result.result.is_ok());
550 assert_eq!(result.retry_history.len(), 1);
551 assert_eq!(result.total_attempts, 2);
552 }
553
554 #[test]
555 fn test_is_retryable_status_408_request_timeout() {
556 assert!(is_retryable_status(408));
557 }
558
559 #[test]
560 fn test_is_retryable_status_429_too_many_requests() {
561 assert!(is_retryable_status(429));
562 }
563
564 #[test]
565 fn test_is_retryable_status_500_internal_server_error() {
566 assert!(is_retryable_status(500));
567 }
568
569 #[test]
570 fn test_is_retryable_status_502_bad_gateway() {
571 assert!(is_retryable_status(502));
572 }
573
574 #[test]
575 fn test_is_retryable_status_503_service_unavailable() {
576 assert!(is_retryable_status(503));
577 }
578
579 #[test]
580 fn test_is_retryable_status_504_gateway_timeout() {
581 assert!(is_retryable_status(504));
582 }
583
584 #[test]
585 fn test_is_retryable_status_501_not_implemented_not_retryable() {
586 assert!(!is_retryable_status(501));
588 }
589
590 #[test]
591 fn test_is_retryable_status_505_http_version_not_supported_not_retryable() {
592 assert!(!is_retryable_status(505));
594 }
595
596 #[test]
597 fn test_is_retryable_status_4xx_not_retryable() {
598 assert!(!is_retryable_status(400)); assert!(!is_retryable_status(401)); assert!(!is_retryable_status(403)); assert!(!is_retryable_status(404)); assert!(!is_retryable_status(405)); assert!(!is_retryable_status(422)); }
606
607 #[test]
608 fn test_is_retryable_status_2xx_not_retryable() {
609 assert!(!is_retryable_status(200));
611 assert!(!is_retryable_status(201));
612 assert!(!is_retryable_status(204));
613 }
614
615 #[test]
616 fn test_is_retryable_status_3xx_not_retryable() {
617 assert!(!is_retryable_status(301));
619 assert!(!is_retryable_status(302));
620 assert!(!is_retryable_status(304));
621 }
622}