use crate::error::Error;
use async_trait::async_trait;
use std::sync::Arc;
use std::time::Duration;
#[async_trait]
pub trait Delay: Send + Sync {
async fn sleep(&self, how_long: Duration);
}
#[cfg(feature = "retry")]
#[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct TokioDelay;
#[cfg(feature = "retry")]
#[async_trait]
impl Delay for TokioDelay {
async fn sleep(&self, how_long: Duration) {
tokio::time::sleep(how_long).await;
}
}
#[derive(Clone)]
pub struct Retry {
attempts: u32,
base: Duration,
ceiling: Duration,
repeat_timeouts: bool,
delay: Arc<dyn Delay>,
}
impl std::fmt::Debug for Retry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Retry")
.field("attempts", &self.attempts)
.field("base", &self.base)
.field("ceiling", &self.ceiling)
.field("repeat_timeouts", &self.repeat_timeouts)
.finish_non_exhaustive()
}
}
impl Retry {
#[cfg(feature = "retry")]
#[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
pub fn new(attempts: u32) -> Self {
Self::with_delay(attempts, Arc::new(TokioDelay))
}
pub fn with_delay(attempts: u32, delay: Arc<dyn Delay>) -> Self {
Self {
attempts: attempts.max(1),
base: Duration::from_millis(250),
ceiling: Duration::from_secs(30),
repeat_timeouts: false,
delay,
}
}
#[must_use]
pub fn with_base(mut self, base: Duration) -> Self {
self.base = base;
self
}
#[must_use]
pub fn with_ceiling(mut self, ceiling: Duration) -> Self {
self.ceiling = ceiling;
self
}
#[must_use]
pub fn repeating_timeouts(mut self) -> Self {
self.repeat_timeouts = true;
self
}
pub fn attempts(&self) -> u32 {
self.attempts
}
pub fn wait_before(&self, next: u32, failure: &Error) -> Option<Duration> {
if next > self.attempts || !failure.is_retryable() {
return None;
}
if matches!(failure, Error::Timeout { .. }) && !self.repeat_timeouts {
return None;
}
if let Some(asked) = failure.retry_after() {
return Some(asked);
}
Some(self.backoff(next))
}
fn backoff(&self, next: u32) -> Duration {
let doubled = self
.base
.saturating_mul(1u32 << (next.saturating_sub(2)).min(16));
let capped = doubled.min(self.ceiling);
let half = capped / 2;
half + Duration::from_nanos(scatter(half.as_nanos() as u64))
}
pub(crate) async fn sleep(&self, how_long: Duration) {
self.delay.sleep(how_long).await;
}
}
fn scatter(span: u64) -> u64 {
if span == 0 {
return 0;
}
let mut x = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64 ^ (d.as_secs().wrapping_mul(0x9E37_79B9_7F4A_7C15)))
.unwrap_or(0x2545_F491_4F6C_DD1D);
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x % (span + 1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct Recording(Mutex<Vec<Duration>>);
#[async_trait]
impl Delay for Recording {
async fn sleep(&self, how_long: Duration) {
if let Ok(mut seen) = self.0.lock() {
seen.push(how_long);
}
}
}
fn policy(attempts: u32) -> Retry {
Retry::with_delay(attempts, Arc::new(Recording::default()))
}
#[test]
fn the_wait_a_provider_named_is_used_exactly() {
let named = Error::RateLimited {
retry_after: Some(Duration::from_secs(90)),
};
let policy = policy(5).with_ceiling(Duration::from_secs(2));
assert_eq!(policy.wait_before(2, &named), Some(Duration::from_secs(90)));
}
#[test]
fn the_four_settled_failures_are_never_repeated() {
let policy = policy(5);
for settled in [
Error::Auth("rejected".into()),
Error::InvalidRequest("malformed".into()),
Error::Refused { category: None },
Error::Unreadable("no content".into()),
] {
assert_eq!(
policy.wait_before(2, &settled),
None,
"{settled} must never be retried"
);
}
}
#[test]
fn a_timeout_is_left_alone_unless_you_ask_for_it() {
let timed_out = Error::Timeout {
elapsed: Duration::from_secs(60),
};
assert_eq!(policy(5).wait_before(2, &timed_out), None);
assert!(policy(5)
.repeating_timeouts()
.wait_before(2, &timed_out)
.is_some());
}
#[test]
fn attempts_run_out() {
let policy = policy(3);
let transient = Error::Transient("503".into());
assert!(policy.wait_before(2, &transient).is_some());
assert!(policy.wait_before(3, &transient).is_some());
assert_eq!(policy.wait_before(4, &transient), None, "three means three");
}
#[test]
fn one_attempt_is_a_policy_that_never_retries() {
assert_eq!(
policy(1).wait_before(2, &Error::Transient("503".into())),
None
);
assert_eq!(policy(0).attempts(), 1);
}
#[test]
fn the_computed_wait_doubles_and_then_stops_at_the_ceiling() {
let policy = policy(9)
.with_base(Duration::from_millis(100))
.with_ceiling(Duration::from_millis(800));
let transient = Error::Transient("503".into());
let wait = |n| {
policy
.wait_before(n, &transient)
.unwrap_or_default()
.as_millis()
};
assert!((50..=100).contains(&wait(2)), "{}", wait(2));
assert!((100..=200).contains(&wait(3)), "{}", wait(3));
assert!((200..=400).contains(&wait(4)), "{}", wait(4));
for n in 6..9 {
assert!((400..=800).contains(&wait(n)), "attempt {n}: {}", wait(n));
}
}
#[test]
fn jitter_does_not_return_the_same_number_every_time() {
let policy = policy(9).with_base(Duration::from_secs(4));
let transient = Error::Transient("503".into());
let seen: std::collections::HashSet<_> =
(0..64).map(|_| policy.wait_before(2, &transient)).collect();
assert!(seen.len() > 1, "every wait was identical: {seen:?}");
}
}