use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay: Duration,
pub multiplier: f64,
pub jitter: f64,
pub max_delay: Duration,
pub max_retry_after: Duration,
}
impl RetryPolicy {
pub fn new(max_attempts: u32) -> Self {
Self {
max_attempts: max_attempts.max(1),
base_delay: Duration::from_millis(500),
multiplier: 2.0,
jitter: 0.2,
max_delay: Duration::from_secs(30),
max_retry_after: Duration::from_secs(300),
}
}
pub fn base_delay(mut self, delay: Duration) -> Self {
self.base_delay = delay;
self
}
pub fn multiplier(mut self, multiplier: f64) -> Self {
self.multiplier = multiplier;
self
}
pub fn jitter(mut self, jitter: f64) -> Self {
self.jitter = jitter.clamp(0.0, 1.0);
self
}
pub fn max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = max_delay;
self
}
pub fn max_retry_after(mut self, max_retry_after: Duration) -> Self {
self.max_retry_after = max_retry_after;
self
}
pub(crate) fn delay_for(
&self,
attempt: u32,
retry_after: Option<Duration>,
seed: &mut u64,
) -> Duration {
match retry_after {
Some(explicit) => explicit.min(self.max_retry_after),
None => crate::backoff::BackoffParams {
base: self.base_delay,
max: self.max_delay,
multiplier: self.multiplier,
jitter: self.jitter,
}
.delay_for(attempt, seed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_clamps_zero_attempts_to_one() {
assert_eq!(RetryPolicy::new(0).max_attempts, 1);
}
#[test]
fn defaults_are_sane() {
let policy = RetryPolicy::new(3);
assert_eq!(policy.max_attempts, 3);
assert_eq!(policy.base_delay, Duration::from_millis(500));
assert_eq!(policy.multiplier, 2.0);
assert_eq!(policy.jitter, 0.2);
assert_eq!(policy.max_delay, Duration::from_secs(30));
assert_eq!(policy.max_retry_after, Duration::from_secs(300));
}
#[test]
fn jitter_is_clamped() {
assert_eq!(RetryPolicy::new(1).jitter(5.0).jitter, 1.0);
assert_eq!(RetryPolicy::new(1).jitter(-5.0).jitter, 0.0);
}
#[test]
fn explicit_retry_after_is_honored_independently_of_max_delay() {
let policy = RetryPolicy::new(3).max_delay(Duration::from_secs(1));
let mut seed = 42;
let delay = policy.delay_for(5, Some(Duration::from_secs(120)), &mut seed);
assert_eq!(delay, Duration::from_secs(120));
}
#[test]
fn an_absurd_retry_after_hint_is_capped() {
let policy = RetryPolicy::new(3);
let mut seed = 42;
let delay = policy.delay_for(0, Some(Duration::from_secs(86_400)), &mut seed);
assert_eq!(delay, Duration::from_secs(300));
}
#[test]
fn falls_back_to_exponential_backoff_when_no_retry_after() {
let policy = RetryPolicy::new(5)
.base_delay(Duration::from_secs(1))
.multiplier(2.0)
.jitter(0.0)
.max_delay(Duration::from_secs(10));
let mut seed = 1;
assert_eq!(policy.delay_for(0, None, &mut seed), Duration::from_secs(1));
assert_eq!(policy.delay_for(1, None, &mut seed), Duration::from_secs(2));
assert_eq!(policy.delay_for(2, None, &mut seed), Duration::from_secs(4));
assert_eq!(
policy.delay_for(10, None, &mut seed),
Duration::from_secs(10)
);
}
}