use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::failure::FailureClass;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub multiplier: f64,
pub jitter: bool,
}
impl RetryPolicy {
pub const NONE: Self = Self {
max_attempts: 1,
initial_delay: Duration::ZERO,
max_delay: Duration::ZERO,
multiplier: 1.0,
jitter: false,
};
pub const fn allows_another(&self, attempts_made: u32) -> bool {
attempts_made < self.max_attempts
}
pub fn backoff(&self, retry: u32) -> Duration {
let multiplier = self.multiplier.max(1.0);
let exponent = i32::try_from(retry).unwrap_or(i32::MAX);
let secs = self.initial_delay.as_secs_f64() * multiplier.powi(exponent);
if secs.is_nan() {
return Duration::ZERO;
}
if secs >= self.max_delay.as_secs_f64() {
return self.max_delay;
}
Duration::from_secs_f64(secs).min(self.max_delay)
}
pub fn delay(&self, retry: u32, failure: FailureClass, sample: f64) -> Duration {
let backoff = self.backoff(retry);
let jittered = if self.jitter {
let sample = if sample.is_finite() {
sample.clamp(0.0, 1.0)
} else {
0.0
};
backoff.mul_f64(0.5 + 0.5 * sample).min(backoff)
} else {
backoff
};
match failure.retry_after() {
Some(asked) => jittered.max(asked),
None => jittered,
}
}
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 5,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(30),
multiplier: 2.0,
jitter: true,
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn default_backoff_doubles_up_to_the_cap() {
let policy = RetryPolicy::default();
let secs: Vec<u64> = (0..7).map(|r| policy.backoff(r).as_secs()).collect();
assert_eq!(secs, [1, 2, 4, 8, 16, 30, 30]);
}
#[test]
fn rate_limits_may_exceed_the_cap() {
let policy = RetryPolicy::default();
let failure = FailureClass::RateLimited {
retry_after: Some(Duration::from_secs(120)),
};
assert_eq!(policy.delay(0, failure, 0.0), Duration::from_secs(120));
}
#[test]
fn attempt_counting_includes_the_first_attempt() {
assert!(!RetryPolicy::NONE.allows_another(1));
let policy = RetryPolicy::default();
assert!(policy.allows_another(4));
assert!(!policy.allows_another(5));
}
proptest! {
#[test]
fn delays_are_bounded_and_monotone(
initial_ms in 0u64..10_000,
max_ms in 0u64..600_000,
multiplier in prop_oneof![Just(f64::NAN), Just(f64::INFINITY), -10.0f64..10.0],
retry in 0u32..u32::MAX,
sample in prop_oneof![Just(f64::NAN), -1.0f64..2.0],
) {
let policy = RetryPolicy {
max_attempts: 5,
initial_delay: Duration::from_millis(initial_ms),
max_delay: Duration::from_millis(max_ms),
multiplier,
jitter: true,
};
let backoff = policy.backoff(retry);
prop_assert!(backoff <= policy.max_delay);
prop_assert!(policy.backoff(retry.saturating_add(1)) >= backoff);
let delay = policy.delay(retry, FailureClass::Transient, sample);
prop_assert!(delay <= backoff);
prop_assert!(delay + Duration::from_nanos(1) >= backoff / 2);
}
}
}