use std::time::Duration;
use rand::{Rng, RngExt};
pub const DEFAULT_JITTER_RATIO: f64 = 0.3;
pub const DEFAULT_MAX_ATTEMPTS: u32 = 5;
#[derive(Clone, Copy, Debug)]
pub struct ExponentialBackoff {
base: Duration,
max: Duration,
jitter_ratio: f64,
max_attempts: u32,
}
impl ExponentialBackoff {
pub const fn new(base: Duration, max: Duration) -> Self {
Self {
base,
max,
jitter_ratio: DEFAULT_JITTER_RATIO,
max_attempts: DEFAULT_MAX_ATTEMPTS,
}
}
#[must_use]
pub const fn with_jitter(mut self, ratio: f64) -> Self {
self.jitter_ratio = ratio;
self
}
#[must_use]
pub const fn with_max_attempts(mut self, n: u32) -> Self {
self.max_attempts = n;
self
}
pub const fn base(&self) -> Duration {
self.base
}
pub const fn max(&self) -> Duration {
self.max
}
pub const fn jitter_ratio(&self) -> f64 {
self.jitter_ratio.clamp(0.0, 1.0)
}
pub const fn max_attempts(&self) -> u32 {
self.max_attempts
}
pub fn delay_for_attempt<R: Rng + ?Sized>(&self, attempt: u32, rng: &mut R) -> Duration {
if attempt >= self.max_attempts {
return Duration::ZERO;
}
let exp = attempt.min(30); let multiplier: u128 = 1u128 << exp;
let base_nanos = u128::from(u64::try_from(self.base.as_nanos()).unwrap_or(u64::MAX));
let max_nanos = u128::from(u64::try_from(self.max.as_nanos()).unwrap_or(u64::MAX));
let unjittered = base_nanos.saturating_mul(multiplier).min(max_nanos);
let unjittered_ns = u64::try_from(unjittered).unwrap_or(u64::MAX);
let jitter_ratio = self.jitter_ratio();
if jitter_ratio <= 0.0 {
return Duration::from_nanos(unjittered_ns);
}
let max_offset_ns: u128 =
(u128::from(unjittered_ns)).saturating_mul(jitter_to_ppm(jitter_ratio)) / 1_000_000;
let offset_ns: u128 = rng.random_range(0..=max_offset_ns);
let offset_u64 = u64::try_from(offset_ns).unwrap_or(u64::MAX);
Duration::from_nanos(unjittered_ns.saturating_add(offset_u64))
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn jitter_to_ppm(ratio: f64) -> u128 {
let clamped = ratio.clamp(0.0, 1.0);
(clamped * 1_000_000.0).round() as u128
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self::new(Duration::from_millis(100), Duration::from_secs(30))
}
}