use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub(crate) trait Retry {
fn max_attempts(&self) -> u32;
fn delay(&self, attempt: u32, server_hint: Option<Duration>) -> Duration;
}
const MAX_RETRY_DELAY: Duration = Duration::from_millis(32_000);
pub(crate) struct ExponentialRetry {
pub base_delay: Duration,
pub max_attempts: u32,
}
impl Retry for ExponentialRetry {
fn max_attempts(&self) -> u32 {
self.max_attempts
}
fn delay(&self, attempt: u32, server_hint: Option<Duration>) -> Duration {
if let Some(hint) = server_hint {
return hint;
}
let base_ms = self.base_delay.as_millis() as u64;
let exponential_ms = base_ms
.saturating_mul(1u64 << attempt.min(31))
.min(MAX_RETRY_DELAY.as_millis() as u64);
let jitter_range = exponential_ms / 4;
if jitter_range == 0 {
return Duration::from_millis(exponential_ms);
}
let entropy = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos() as u64;
let jitter_offset = entropy % jitter_range;
Duration::from_millis(exponential_ms.saturating_add(jitter_offset))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(base_ms: u64) -> ExponentialRetry {
ExponentialRetry {
base_delay: Duration::from_millis(base_ms),
max_attempts: 10,
}
}
#[test]
fn exponential_backoff() {
let policy = policy(1000);
for attempt in 0..3 {
let delay = policy.delay(attempt, None);
let expected_base_ms = 1000u64 * (1u64 << attempt);
let jitter_range_ms = expected_base_ms / 4;
let delay_ms = delay.as_millis() as u64;
assert!(delay_ms >= expected_base_ms);
assert!(delay_ms <= expected_base_ms + jitter_range_ms);
}
}
#[test]
fn respects_retry_delay() {
let delay = policy(1000).delay(0, Some(Duration::from_millis(5000)));
assert_eq!(delay, Duration::from_millis(5000));
}
#[test]
fn caps_at_max_delay() {
let delay = policy(1000).delay(20, None);
let max_ms = MAX_RETRY_DELAY.as_millis() as u64;
let jitter_range_ms = max_ms / 4;
let delay_ms = delay.as_millis() as u64;
assert!(delay_ms >= max_ms);
assert!(delay_ms <= max_ms + jitter_range_ms);
}
#[test]
fn saturates_instead_of_overflow() {
let _delay = policy(u64::MAX).delay(10, None);
}
}