use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay: Duration,
pub max_delay: Duration,
pub deadline: Duration,
}
impl RetryPolicy {
pub fn new(max_attempts: u32, base_delay: Duration, deadline: Duration) -> Self {
Self {
max_attempts: max_attempts.max(1),
base_delay,
max_delay: Duration::from_secs(30),
deadline,
}
}
pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = max_delay;
self
}
pub fn delay(&self, attempt: u32, jitter_entropy: u64) -> Duration {
let shift = attempt.min(31);
let backoff = self
.base_delay
.checked_mul(1u32 << shift)
.unwrap_or(self.max_delay)
.min(self.max_delay);
if backoff.is_zero() {
return Duration::ZERO;
}
let nanos = backoff.as_nanos().min(u64::MAX as u128) as u64;
let jitter = jitter_entropy % nanos.saturating_add(1);
Duration::from_nanos(nanos.saturating_sub(jitter))
}
pub fn deadline_exceeded(&self, elapsed: Duration) -> bool {
elapsed >= self.deadline
}
pub fn should_retry(&self, failures: u32, elapsed: Duration) -> bool {
failures < self.max_attempts && !self.deadline_exceeded(elapsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_doubles_and_caps() {
let policy = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(60));
assert_eq!(policy.delay(0, 0), Duration::from_millis(100));
assert_eq!(policy.delay(1, 0), Duration::from_millis(200));
assert_eq!(policy.delay(2, 0), Duration::from_millis(400));
let capped = RetryPolicy::new(5, Duration::from_secs(20), Duration::from_secs(600))
.with_max_delay(Duration::from_secs(25));
assert_eq!(capped.delay(3, 0), Duration::from_secs(25));
}
#[test]
fn jitter_only_shrinks() {
let policy = RetryPolicy::new(4, Duration::from_millis(200), Duration::from_secs(60));
for entropy in [0u64, 1, 7, 1_000, u64::MAX] {
let d = policy.delay(2, entropy);
assert!(
d <= Duration::from_millis(800),
"jitter grew backoff: {d:?}"
);
}
assert_eq!(policy.delay(2, 0), Duration::from_millis(800));
}
#[test]
fn zero_attempts_normalises_to_one_try() {
let policy = RetryPolicy::new(0, Duration::from_millis(10), Duration::from_secs(1));
assert_eq!(policy.max_attempts, 1);
assert!(policy.should_retry(0, Duration::ZERO));
assert!(!policy.should_retry(1, Duration::ZERO));
}
#[test]
fn huge_attempt_counts_cannot_overflow_or_hang() {
let policy = RetryPolicy::new(u32::MAX, Duration::from_millis(1), Duration::MAX);
let d = policy.delay(u32::MAX, u64::MAX);
assert!(d <= Duration::from_secs(30));
}
#[test]
fn deadline_gates_retries() {
let policy = RetryPolicy::new(10, Duration::from_millis(50), Duration::from_secs(5));
assert!(policy.should_retry(9, Duration::from_secs(4)));
assert!(!policy.should_retry(9, Duration::from_secs(5)));
assert!(!policy.should_retry(10, Duration::ZERO));
}
#[test]
fn zero_base_delay_stays_zero() {
let policy = RetryPolicy::new(3, Duration::ZERO, Duration::from_secs(1));
assert_eq!(policy.delay(4, 12345), Duration::ZERO);
}
}