use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
}
}
}
impl RetryConfig {
pub fn new() -> Self {
Self::default()
}
pub fn max_attempts(mut self, attempts: u32) -> Self {
self.max_attempts = attempts;
self
}
pub fn initial_delay(mut self, delay: Duration) -> Self {
self.initial_delay = delay;
self
}
pub fn max_delay(mut self, delay: Duration) -> Self {
self.max_delay = delay;
self
}
pub fn backoff_multiplier(mut self, multiplier: f64) -> Self {
self.backoff_multiplier = multiplier;
self
}
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::ZERO;
}
let delay_ms = self.initial_delay.as_millis() as f64 * self.backoff_multiplier.powi((attempt - 1) as i32);
let delay = Duration::from_millis(delay_ms as u64);
if delay > self.max_delay { self.max_delay } else { delay }
}
pub fn should_retry(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}
}
pub fn is_retryable_status(status: u16) -> bool {
matches!(
status,
429 | 500..=599 )
}
pub fn is_retryable_error(error: &reqwest::Error) -> bool {
error.is_timeout() || error.is_connect() || error.is_request()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_attempts, 3);
assert_eq!(config.initial_delay, Duration::from_millis(100));
assert_eq!(config.max_delay, Duration::from_secs(60));
assert_eq!(config.backoff_multiplier, 2.0);
}
#[test]
fn test_retry_config_builder() {
let config = RetryConfig::new()
.max_attempts(5)
.initial_delay(Duration::from_millis(200))
.max_delay(Duration::from_secs(30))
.backoff_multiplier(1.5);
assert_eq!(config.max_attempts, 5);
assert_eq!(config.initial_delay, Duration::from_millis(200));
assert_eq!(config.max_delay, Duration::from_secs(30));
assert_eq!(config.backoff_multiplier, 1.5);
}
#[test]
fn test_delay_calculation() {
let config = RetryConfig::new()
.initial_delay(Duration::from_millis(100))
.backoff_multiplier(2.0)
.max_delay(Duration::from_secs(5));
assert_eq!(config.delay_for_attempt(0), Duration::ZERO);
assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
assert_eq!(config.delay_for_attempt(2), Duration::from_millis(200));
assert_eq!(config.delay_for_attempt(3), Duration::from_millis(400));
let long_delay = config.delay_for_attempt(10);
assert!(long_delay <= config.max_delay);
}
#[test]
fn test_should_retry() {
let config = RetryConfig::new().max_attempts(3);
assert!(config.should_retry(0));
assert!(config.should_retry(1));
assert!(config.should_retry(2));
assert!(!config.should_retry(3));
assert!(!config.should_retry(4));
}
#[test]
fn test_is_retryable_status() {
assert!(!is_retryable_status(200)); assert!(!is_retryable_status(400)); assert!(!is_retryable_status(404)); assert!(is_retryable_status(429)); assert!(is_retryable_status(500)); assert!(is_retryable_status(502)); assert!(is_retryable_status(503)); }
}