use std::time::Duration;
const RETRY_AFTER_CAP: Duration = Duration::from_secs(300);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
pub max_retries: u32,
pub base_delay: Duration,
pub max_delay: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
base_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(30),
}
}
}
impl RetryPolicy {
pub const fn disabled() -> Self {
Self {
max_retries: 0,
base_delay: Duration::ZERO,
max_delay: Duration::ZERO,
}
}
pub const fn new(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
Self {
max_retries,
base_delay,
max_delay,
}
}
pub(crate) fn should_retry(&self, retry_index: u32) -> bool {
retry_index < self.max_retries
}
pub(crate) fn backoff(&self, retry_index: u32, retry_after: Option<Duration>) -> Duration {
if let Some(ra) = retry_after {
let jitter = Duration::from_millis((jitter_fraction() * 250.0) as u64);
return ra.saturating_add(jitter).min(RETRY_AFTER_CAP);
}
let factor = 2u32.saturating_pow(retry_index);
let capped = self.base_delay.saturating_mul(factor).min(self.max_delay);
let half = capped / 2;
half + half.mul_f64(jitter_fraction())
}
}
fn jitter_fraction() -> f64 {
use std::{
collections::hash_map::RandomState,
hash::{BuildHasher, Hasher},
};
let mut hasher = RandomState::new().build_hasher();
hasher.write_u64(0x9E37_79B9_7F4A_7C15);
((hasher.finish() >> 11) as f64) / ((1u64 << 53) as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retry_after_is_honored() {
let policy = RetryPolicy::default();
let delay = policy.backoff(0, Some(Duration::from_secs(7)));
assert!(delay >= Duration::from_secs(7), "got {delay:?}");
assert!(delay <= Duration::from_secs(8), "got {delay:?}");
}
#[test]
fn retry_after_is_capped() {
let policy = RetryPolicy::default();
let delay = policy.backoff(0, Some(Duration::from_secs(10_000)));
assert_eq!(delay, RETRY_AFTER_CAP);
}
#[test]
fn exponential_backoff_grows_and_caps() {
let policy = RetryPolicy::new(10, Duration::from_millis(100), Duration::from_millis(800));
for (idx, capped_ms) in [(0u32, 100u64), (1, 200), (2, 400), (3, 800), (4, 800)] {
let d = policy.backoff(idx, None);
assert!(
d >= Duration::from_millis(capped_ms / 2) && d <= Duration::from_millis(capped_ms),
"retry {idx}: {d:?} outside [{}/2, {capped_ms}]ms",
capped_ms
);
}
}
#[test]
fn disabled_policy_never_retries() {
let policy = RetryPolicy::disabled();
assert!(!policy.should_retry(0));
}
#[test]
fn should_retry_respects_max() {
let policy = RetryPolicy::new(2, Duration::from_millis(1), Duration::from_millis(1));
assert!(policy.should_retry(0));
assert!(policy.should_retry(1));
assert!(!policy.should_retry(2));
}
}