use std::time::Duration;
#[derive(Debug, Clone)]
pub enum RetryStrategy {
ExponentialBackoff {
initial_delay: Duration,
max_delay: Duration,
multiplier: f64,
},
LinearBackoff {
initial_delay: Duration,
increment: Duration,
},
FixedDelay(Duration),
NoRetry,
}
impl Default for RetryStrategy {
fn default() -> Self {
Self::ExponentialBackoff {
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(30),
multiplier: 2.0,
}
}
}
impl RetryStrategy {
pub fn delay(&self, attempt: u32) -> Duration {
let d = match self {
RetryStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
} => {
let delay_ms = initial_delay.as_millis() as f64 * multiplier.powi(attempt as i32);
let delay = Duration::from_millis(delay_ms as u64);
delay.min(*max_delay)
}
RetryStrategy::LinearBackoff {
initial_delay,
increment,
} => initial_delay.saturating_add(increment.saturating_mul(attempt)),
RetryStrategy::FixedDelay(delay) => *delay,
RetryStrategy::NoRetry => Duration::ZERO,
};
tracing::debug!("Calculated retry delay for attempt {}: {:?}", attempt, d);
d
}
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: u32,
pub strategy: RetryStrategy,
pub retry_on_client_error: bool,
pub retry_on_server_error: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
strategy: RetryStrategy::default(),
retry_on_client_error: false,
retry_on_server_error: true,
}
}
}
impl RetryPolicy {
pub fn should_retry(&self, status_code: u16, attempt: u32) -> bool {
if attempt >= self.max_retries {
tracing::warn!("Maximum retry attempts ({}) reached", self.max_retries);
return false;
}
let retry = match status_code {
400..=499 => self.retry_on_client_error,
500..=599 => self.retry_on_server_error,
_ => false,
};
if retry {
tracing::info!(
"Request failed with status {}, scheduling retry attempt {}",
status_code,
attempt + 1
);
} else {
tracing::debug!(
"Request failed with status {}, no retry scheduled",
status_code
);
}
retry
}
pub fn retry_delay(&self, attempt: u32) -> Duration {
self.strategy.delay(attempt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exponential_backoff() {
let strategy = RetryStrategy::ExponentialBackoff {
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(30),
multiplier: 2.0,
};
let delay0 = strategy.delay(0);
let delay1 = strategy.delay(1);
let delay2 = strategy.delay(2);
assert!(delay0 < delay1);
assert!(delay1 < delay2);
}
#[test]
fn test_retry_policy_should_retry() {
let policy = RetryPolicy::default();
assert!(!policy.should_retry(200, 0)); assert!(policy.should_retry(500, 0)); assert!(!policy.should_retry(500, 3)); }
}