use std::time::Duration;
use reqwest::header::{HeaderMap, RETRY_AFTER};
const DEFAULT_BASE_DELAY: Duration = Duration::from_secs(1);
const DEFAULT_MAX_DELAY: Duration = Duration::from_mins(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
max_retries: u32,
base_delay: Duration,
max_delay: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 0,
base_delay: DEFAULT_BASE_DELAY,
max_delay: DEFAULT_MAX_DELAY,
}
}
}
impl RetryPolicy {
#[must_use]
pub fn new(max_retries: u32) -> Self {
Self {
max_retries,
..Self::default()
}
}
#[must_use]
pub fn with_base_delay(mut self, base_delay: Duration) -> Self {
self.base_delay = base_delay;
self
}
#[must_use]
pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = max_delay;
self
}
pub(crate) fn max_retries(&self) -> u32 {
self.max_retries
}
pub(crate) fn delay(&self, attempt: u32, headers: &HeaderMap) -> Duration {
retry_after(headers)
.unwrap_or_else(|| self.backoff(attempt))
.min(self.max_delay)
}
fn backoff(&self, attempt: u32) -> Duration {
let factor = 2u32.saturating_pow(attempt);
self.base_delay.saturating_mul(factor)
}
}
fn retry_after(headers: &HeaderMap) -> Option<Duration> {
let secs: u64 = headers
.get(RETRY_AFTER)?
.to_str()
.ok()?
.trim()
.parse()
.ok()?;
Some(Duration::from_secs(secs))
}
#[cfg(test)]
mod tests {
use super::*;
fn headers_with_retry_after(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(RETRY_AFTER, value.parse().unwrap());
headers
}
#[test]
fn default_policy_does_not_retry() {
assert_eq!(RetryPolicy::default().max_retries(), 0);
}
#[test]
fn prefers_retry_after_when_present() {
let policy = RetryPolicy::new(3);
let delay = policy.delay(0, &headers_with_retry_after("5"));
assert_eq!(delay, Duration::from_secs(5));
}
#[test]
fn falls_back_to_exponential_backoff() {
let policy = RetryPolicy::new(3).with_base_delay(Duration::from_secs(2));
let empty = HeaderMap::new();
assert_eq!(policy.delay(0, &empty), Duration::from_secs(2));
assert_eq!(policy.delay(1, &empty), Duration::from_secs(4));
assert_eq!(policy.delay(2, &empty), Duration::from_secs(8));
}
#[test]
fn clamps_to_max_delay() {
let policy = RetryPolicy::new(10)
.with_base_delay(Duration::from_secs(10))
.with_max_delay(Duration::from_secs(30));
assert_eq!(policy.delay(5, &HeaderMap::new()), Duration::from_secs(30));
assert_eq!(
policy.delay(0, &headers_with_retry_after("9999")),
Duration::from_secs(30)
);
}
#[test]
fn backoff_saturates_without_overflow() {
let policy = RetryPolicy::new(99).with_max_delay(Duration::from_mins(1));
assert_eq!(
policy.delay(u32::MAX, &HeaderMap::new()),
Duration::from_mins(1)
);
}
}