use std::sync::LazyLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
pub const MAX_RETRIES: u32 = 16;
pub(crate) async fn wait_for_retry(
delay: Duration,
deadline: Option<tokio::time::Instant>,
) -> bool {
if deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
return false;
}
if delay.is_zero() {
tokio::task::yield_now().await;
return deadline.is_none_or(|deadline| tokio::time::Instant::now() < deadline);
}
if let Some(deadline) = deadline {
let wake = tokio::time::Instant::now()
.checked_add(delay)
.unwrap_or(deadline);
if deadline <= wake {
tokio::time::sleep_until(deadline).await;
return false;
}
tokio::time::sleep_until(wake).await;
true
} else {
tokio::time::sleep(delay).await;
true
}
}
#[derive(Clone, Debug)]
pub struct Retry {
retries: u32,
backoff: Backoff,
}
#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
pub enum RetryConfigError {
#[error("retries must not exceed {maximum} (got {requested})")]
TooManyRetries { requested: u32, maximum: u32 },
#[error("jitter must be finite and between 0.0 and 1.0 (got {0})")]
InvalidJitter(f64),
}
#[derive(Clone, Copy, Debug, Default)]
enum Backoff {
#[default]
None,
Fixed {
delay: Duration,
},
Exponential {
initial: Duration,
max: Duration,
jitter: f64,
},
}
impl Default for Retry {
fn default() -> Self {
Self {
retries: 3,
backoff: Backoff::Fixed {
delay: Duration::from_secs(1),
},
}
}
}
impl Retry {
#[must_use]
pub fn none() -> Self {
Self {
retries: 0,
backoff: Backoff::None,
}
}
pub fn fixed(retries: u32, delay: Duration) -> Result<Self, RetryConfigError> {
Self::validate_retries(retries)?;
Ok(Self {
retries,
backoff: Backoff::Fixed { delay },
})
}
#[must_use]
pub fn exponential(retries: u32) -> RetryBuilder {
RetryBuilder {
retries,
..Default::default()
}
}
#[must_use]
pub fn retries(&self) -> u32 {
self.retries
}
#[must_use]
pub fn compute_delay(&self, attempt: u32) -> Duration {
match &self.backoff {
Backoff::None => Duration::ZERO,
Backoff::Fixed { delay } => *delay,
Backoff::Exponential {
initial,
max,
jitter,
} => {
let shift = attempt.min(31);
let multiplier = 1u32.checked_shl(shift).unwrap_or(u32::MAX);
let base = initial.saturating_mul(multiplier);
let capped = base.min(*max);
let factor = jitter_factor(*jitter);
Duration::try_from_secs_f64(capped.as_secs_f64() * factor)
.unwrap_or(Duration::MAX)
.min(*max)
}
}
}
pub(crate) fn maximum_delay(&self, attempt: u32) -> Duration {
match &self.backoff {
Backoff::None => Duration::ZERO,
Backoff::Fixed { delay } => *delay,
Backoff::Exponential {
initial,
max,
jitter,
} => {
let multiplier = 1u32.checked_shl(attempt.min(31)).unwrap_or(u32::MAX);
let capped = initial.saturating_mul(multiplier).min(*max);
Duration::try_from_secs_f64(capped.as_secs_f64() * (1.0 + jitter))
.unwrap_or(Duration::MAX)
.min(*max)
}
}
}
pub(crate) fn maximum_total_delay(&self, retries: u32) -> Option<Duration> {
match &self.backoff {
Backoff::None => Some(Duration::ZERO),
Backoff::Fixed { delay } => delay.checked_mul(retries),
Backoff::Exponential { .. } => {
let prefix = retries.min(32);
let mut total = Duration::ZERO;
for attempt in 0..prefix {
total = total.checked_add(self.maximum_delay(attempt))?;
}
let remaining = retries - prefix;
total.checked_add(self.maximum_delay(31).checked_mul(remaining)?)
}
}
}
}
#[derive(Debug, Clone)]
pub struct RetryBuilder {
retries: u32,
initial: Duration,
max: Duration,
jitter: f64,
}
impl Default for RetryBuilder {
fn default() -> Self {
Self {
retries: 3,
initial: Duration::from_secs(1),
max: Duration::from_secs(5),
jitter: 0.25,
}
}
}
impl RetryBuilder {
#[must_use]
pub fn initial_delay(mut self, delay: Duration) -> Self {
self.initial = delay;
self
}
#[must_use]
pub fn max_delay(mut self, delay: Duration) -> Self {
self.max = delay;
self
}
#[must_use]
pub fn jitter(mut self, jitter: f64) -> Self {
self.jitter = jitter;
self
}
pub fn build(self) -> Result<Retry, RetryConfigError> {
Retry::validate_retries(self.retries)?;
Retry::validate_jitter(self.jitter)?;
Ok(Retry {
retries: self.retries,
backoff: Backoff::Exponential {
initial: self.initial,
max: self.max,
jitter: self.jitter,
},
})
}
}
impl Retry {
const fn validate_retries(retries: u32) -> Result<(), RetryConfigError> {
if retries <= MAX_RETRIES {
Ok(())
} else {
Err(RetryConfigError::TooManyRetries {
requested: retries,
maximum: MAX_RETRIES,
})
}
}
pub(crate) fn validate_jitter(jitter: f64) -> Result<(), RetryConfigError> {
if jitter.is_finite() && (0.0..=1.0).contains(&jitter) {
Ok(())
} else {
Err(RetryConfigError::InvalidJitter(jitter))
}
}
}
static JITTER_COUNTER: LazyLock<AtomicU64> =
LazyLock::new(|| AtomicU64::new(jitter_seed_with(getrandom::fill)));
fn jitter_seed_with(
mut fill: impl FnMut(&mut [u8]) -> std::result::Result<(), getrandom::Error>,
) -> u64 {
let mut seed = [0_u8; 8];
if let Err(error) = fill(&mut seed) {
tracing::warn!(target: "async_snmp::retry", %error, "OS random source unavailable; using deterministic retry jitter seed");
}
u64::from_ne_bytes(seed)
}
fn jitter_factor(jitter: f64) -> f64 {
if jitter <= 0.0 {
return 1.0;
}
let counter = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
jitter_factor_from_seed(jitter, counter)
}
#[allow(
clippy::cast_precision_loss,
reason = "u64->f64 cast is intentional part of hash-like algorithm"
)]
fn jitter_factor_from_seed(jitter: f64, seed: u64) -> f64 {
let hash = seed.wrapping_mul(0x5851_f42d_4c95_7f2d);
let random = (hash >> 11) as f64 / ((1u64 << 53) as f64);
1.0 + (random - 0.5) * 2.0 * jitter
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_none() {
let retry = Retry::none();
assert_eq!(retry.retries(), 0);
assert_eq!(retry.compute_delay(0), Duration::ZERO);
}
#[test]
fn maximum_delay_bounds_jittered_delays() {
let retry = Retry::exponential(4)
.initial_delay(Duration::from_millis(100))
.max_delay(Duration::from_secs(2))
.jitter(1.0)
.build()
.unwrap();
for attempt in 0..4 {
let maximum = retry.maximum_delay(attempt);
for _ in 0..100 {
assert!(retry.compute_delay(attempt) <= maximum);
}
}
}
#[test]
fn maximum_total_delay_handles_maximum_retry_count_in_bounded_work() {
let fixed = Retry::fixed(MAX_RETRIES, Duration::from_nanos(1)).unwrap();
assert_eq!(
fixed.maximum_total_delay(MAX_RETRIES),
Some(Duration::from_nanos(u64::from(MAX_RETRIES)))
);
let exponential = Retry::exponential(MAX_RETRIES)
.initial_delay(Duration::from_nanos(1))
.max_delay(Duration::from_nanos(32))
.jitter(1.0)
.build()
.unwrap();
assert!(exponential.maximum_total_delay(MAX_RETRIES).is_some());
}
#[test]
fn maximum_and_over_maximum_retry_counts_are_deterministic() {
assert_eq!(
Retry::fixed(MAX_RETRIES, Duration::ZERO).unwrap().retries(),
MAX_RETRIES
);
assert_eq!(
Retry::fixed(MAX_RETRIES + 1, Duration::ZERO).unwrap_err(),
RetryConfigError::TooManyRetries {
requested: MAX_RETRIES + 1,
maximum: MAX_RETRIES,
}
);
assert_eq!(
Retry::exponential(MAX_RETRIES + 1).build().unwrap_err(),
RetryConfigError::TooManyRetries {
requested: MAX_RETRIES + 1,
maximum: MAX_RETRIES,
}
);
}
#[test]
fn test_retry_default() {
let retry = Retry::default();
assert_eq!(retry.retries(), 3);
assert_eq!(retry.compute_delay(0), Duration::from_secs(1));
}
#[test]
fn test_retry_fixed() {
let retry = Retry::fixed(5, Duration::from_millis(200)).unwrap();
assert_eq!(retry.retries(), 5);
assert_eq!(retry.compute_delay(0), Duration::from_millis(200));
}
#[test]
fn test_retry_exponential_builder() {
let retry = Retry::exponential(4)
.initial_delay(Duration::from_millis(50))
.max_delay(Duration::from_millis(75))
.jitter(0.0)
.build()
.unwrap();
assert_eq!(retry.retries(), 4);
assert_eq!(retry.compute_delay(0), Duration::from_millis(50));
assert_eq!(retry.compute_delay(1), Duration::from_millis(75));
}
#[test]
fn test_builder_rejects_invalid_jitter() {
for jitter in [-0.1, 1.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(
Retry::exponential(1).jitter(jitter).build(),
Err(RetryConfigError::InvalidJitter(value)) if value.to_bits() == jitter.to_bits()
));
}
}
#[test]
fn test_builder_accepts_jitter_endpoints() {
assert!(Retry::exponential(1).jitter(0.0).build().is_ok());
assert!(Retry::exponential(1).jitter(1.0).build().is_ok());
}
#[test]
fn test_compute_delay_none() {
let retry = Retry::none();
assert_eq!(retry.compute_delay(0), Duration::ZERO);
assert_eq!(retry.compute_delay(5), Duration::ZERO);
}
#[test]
fn test_compute_delay_default() {
let retry = Retry::default();
assert_eq!(retry.compute_delay(0), Duration::from_secs(1));
assert_eq!(retry.compute_delay(5), Duration::from_secs(1));
}
#[test]
fn test_compute_delay_fixed() {
let retry = Retry::fixed(3, Duration::from_millis(100)).unwrap();
assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
assert_eq!(retry.compute_delay(1), Duration::from_millis(100));
assert_eq!(retry.compute_delay(10), Duration::from_millis(100));
}
#[test]
fn test_compute_delay_exponential_no_jitter() {
let retry = Retry::exponential(5)
.initial_delay(Duration::from_millis(100))
.max_delay(Duration::from_secs(10))
.jitter(0.0)
.build()
.unwrap();
assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
assert_eq!(retry.compute_delay(3), Duration::from_millis(800));
}
#[test]
fn test_compute_delay_exponential_capped() {
let retry = Retry::exponential(10)
.initial_delay(Duration::from_millis(100))
.max_delay(Duration::from_millis(500))
.jitter(0.0)
.build()
.unwrap();
assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
assert_eq!(retry.compute_delay(3), Duration::from_millis(500));
assert_eq!(retry.compute_delay(10), Duration::from_millis(500));
}
#[test]
fn test_compute_delay_exponential_with_jitter() {
let retry = Retry::exponential(3)
.initial_delay(Duration::from_millis(100))
.max_delay(Duration::from_secs(1))
.jitter(0.25)
.build()
.unwrap();
for _ in 0..10 {
let delay = retry.compute_delay(0);
let millis = delay.as_millis();
assert!((75..=125).contains(&millis), "delay was {millis}ms");
}
}
#[test]
fn test_compute_delay_jitter_respects_maximum_cap() {
let max = Duration::from_millis(500);
let retry = Retry::exponential(10)
.initial_delay(max)
.max_delay(max)
.jitter(1.0)
.build()
.unwrap();
for _ in 0..128 {
assert!(retry.compute_delay(u32::MAX) <= max);
}
}
#[test]
fn test_jitter_sequence_initializes() {
let counter = LazyLock::force(&JITTER_COUNTER);
let _ = counter.load(Ordering::Relaxed);
}
#[test]
fn test_injected_jitter_seed_is_deterministic() {
let seed = 0x0123_4567_89ab_cdef;
assert_eq!(
jitter_factor_from_seed(0.5, seed).to_bits(),
jitter_factor_from_seed(0.5, seed).to_bits()
);
}
#[test]
fn test_jitter_seed_falls_back_when_random_source_fails() {
let seed = jitter_seed_with(|_| Err(getrandom::Error::UNEXPECTED));
assert_eq!(seed, 0);
}
#[test]
fn test_jitter_factor_range() {
for _ in 0..100 {
let factor = jitter_factor(0.5);
assert!((0.5..=1.5).contains(&factor), "factor was {factor}");
}
}
#[test]
fn test_jitter_factor_zero() {
assert_eq!(jitter_factor(0.0), 1.0);
assert_eq!(jitter_factor(-0.1), 1.0);
}
#[test]
fn test_public_retry_configurations_compute_delays_without_panicking() {
let configurations = [
Retry::none(),
Retry::default(),
Retry::fixed(2, Duration::MAX).unwrap(),
Retry::exponential(2)
.initial_delay(Duration::MAX)
.max_delay(Duration::MAX)
.jitter(0.0)
.build()
.unwrap(),
Retry::exponential(2)
.initial_delay(Duration::MAX)
.max_delay(Duration::MAX)
.jitter(1.0)
.build()
.unwrap(),
];
for retry in configurations {
for attempt in [0, 1, u32::MAX] {
let _ = retry.compute_delay(attempt);
}
}
}
}