use std::time::{Duration, Instant};
use rand::Rng;
pub trait RetryPolicy: Send + Sync {
fn should_retry(&mut self) -> bool;
fn attempt_count(&self) -> u32;
fn next_sleep(&self) -> Duration;
}
pub struct ExponentialTimeBoundedRetry {
max_duration: Duration,
max_sleep: Duration,
start: Instant,
attempts: u32,
current_sleep: Duration,
}
impl ExponentialTimeBoundedRetry {
pub fn new(max_duration: Duration, initial_sleep: Duration, max_sleep: Duration) -> Self {
Self {
max_duration,
max_sleep,
start: Instant::now(),
attempts: 0,
current_sleep: initial_sleep,
}
}
pub fn with_defaults() -> Self {
Self::new(
Duration::from_secs(120),
Duration::from_millis(50),
Duration::from_secs(3),
)
}
}
impl RetryPolicy for ExponentialTimeBoundedRetry {
fn should_retry(&mut self) -> bool {
if self.attempts == 0 {
self.attempts = 1;
return true;
}
let elapsed = self.start.elapsed();
if elapsed >= self.max_duration {
return false;
}
self.attempts += 1;
if self.attempts >= 2 {
self.current_sleep =
std::cmp::min(self.current_sleep.saturating_mul(2), self.max_sleep);
}
true
}
fn attempt_count(&self) -> u32 {
self.attempts
}
fn next_sleep(&self) -> Duration {
add_jitter(self.current_sleep)
}
}
pub struct ExponentialBackoffRetry {
max_sleep: Duration,
max_retries: u32,
attempts: u32,
current_sleep: Duration,
}
impl ExponentialBackoffRetry {
pub fn new(base_sleep: Duration, max_sleep: Duration, max_retries: u32) -> Self {
Self {
max_sleep,
max_retries,
attempts: 0,
current_sleep: base_sleep,
}
}
}
impl RetryPolicy for ExponentialBackoffRetry {
fn should_retry(&mut self) -> bool {
if self.attempts == 0 {
self.attempts = 1;
return true;
}
if self.attempts > self.max_retries {
return false;
}
self.attempts += 1;
if self.attempts >= 2 {
self.current_sleep =
std::cmp::min(self.current_sleep.saturating_mul(2), self.max_sleep);
}
true
}
fn attempt_count(&self) -> u32 {
self.attempts
}
fn next_sleep(&self) -> Duration {
add_jitter(self.current_sleep)
}
}
fn add_jitter(base: Duration) -> Duration {
let mut rng = rand::rng();
let jitter_fraction: f64 = rng.random_range(0.0..0.1);
let jitter = base.mul_f64(jitter_fraction);
base + jitter
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_bounded_first_attempt_always_allowed() {
let mut policy = ExponentialTimeBoundedRetry::new(
Duration::from_millis(0), Duration::from_millis(10),
Duration::from_millis(100),
);
assert!(policy.should_retry());
assert_eq!(policy.attempt_count(), 1);
assert!(!policy.should_retry());
}
#[test]
fn test_time_bounded_multiple_retries() {
let mut policy = ExponentialTimeBoundedRetry::new(
Duration::from_secs(10), Duration::from_millis(10),
Duration::from_millis(200),
);
for _ in 0..5 {
assert!(policy.should_retry());
}
assert!(policy.attempt_count() == 5);
}
#[test]
fn test_time_bounded_sleep_grows() {
let initial = Duration::from_millis(50);
let max_sleep = Duration::from_secs(3);
let mut policy =
ExponentialTimeBoundedRetry::new(Duration::from_secs(60), initial, max_sleep);
assert!(policy.should_retry()); let s1 = policy.next_sleep();
assert!(policy.should_retry()); let _s2 = policy.next_sleep();
assert!(policy.should_retry()); let s3 = policy.next_sleep();
assert!(s1 <= initial + initial.mul_f64(0.11)); assert!(s3 >= initial); }
#[test]
fn test_backoff_second_retry_doubles_base_sleep() {
let base = Duration::from_millis(50);
let max_sleep = Duration::from_secs(60); let mut policy = ExponentialBackoffRetry::new(base, max_sleep, 10);
assert!(policy.should_retry());
let s1 = policy.next_sleep();
assert!(
s1 >= base && s1 <= base + base.mul_f64(0.11),
"1st retry sleep must be base ± 10% jitter, got {:?}",
s1
);
assert!(policy.should_retry());
let s2 = policy.next_sleep();
let expected_s2 = base * 2;
assert!(
s2 >= expected_s2 && s2 <= expected_s2 + expected_s2.mul_f64(0.11),
"2nd retry MUST use base*2 ± jitter ({:?} ± 10%), got {:?} — off-by-one regression",
expected_s2,
s2
);
assert!(policy.should_retry());
let s3 = policy.next_sleep();
let expected_s3 = base * 4;
assert!(
s3 >= expected_s3 && s3 <= expected_s3 + expected_s3.mul_f64(0.11),
"3rd retry must use base*4 ± jitter ({:?} ± 10%), got {:?}",
expected_s3,
s3
);
}
#[test]
fn test_backoff_saturating_mul_no_overflow() {
let base = Duration::from_secs(u64::MAX / 2 + 1);
let mut policy = ExponentialBackoffRetry::new(base, Duration::MAX, 5);
for _ in 0..5 {
policy.should_retry();
}
}
#[test]
fn test_backoff_retry_max_retries() {
let mut policy = ExponentialBackoffRetry::new(
Duration::from_millis(10),
Duration::from_millis(100),
3, );
assert!(policy.should_retry()); assert!(policy.should_retry()); assert!(policy.should_retry()); assert!(policy.should_retry()); assert!(!policy.should_retry()); assert_eq!(policy.attempt_count(), 4);
}
#[test]
fn test_backoff_retry_zero_retries() {
let mut policy =
ExponentialBackoffRetry::new(Duration::from_millis(10), Duration::from_millis(100), 0);
assert!(policy.should_retry()); assert!(!policy.should_retry()); assert_eq!(policy.attempt_count(), 1);
}
#[test]
fn test_backoff_sleep_capped() {
let base = Duration::from_millis(50);
let max_sleep = Duration::from_millis(100);
let mut policy = ExponentialBackoffRetry::new(base, max_sleep, 10);
for _ in 0..6 {
assert!(policy.should_retry());
}
let sleep = policy.next_sleep();
assert!(sleep <= max_sleep + max_sleep.mul_f64(0.11));
}
#[test]
fn test_jitter_within_bounds() {
let base = Duration::from_millis(100);
for _ in 0..100 {
let result = add_jitter(base);
assert!(result >= base);
assert!(result <= base + base.mul_f64(0.11)); }
}
}