use std::result::Result as StdResult;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::time::sleep;
use crate::error::Result;
const MAX_RETRIES: u32 = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Retry {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
}
impl Retry {
pub const NONE: Self = Self {
max_retries: 0,
initial_backoff: Duration::ZERO,
max_backoff: Duration::ZERO,
};
pub(crate) fn check(&self) -> Result<()> {
if self.max_retries > MAX_RETRIES {
return Err(format!("at most {MAX_RETRIES} retries, not {}", self.max_retries).into());
}
Ok(())
}
pub(crate) fn backoff(&self, retry: u32, server: Option<Duration>) -> Duration {
let exponential = self
.initial_backoff
.saturating_mul(2u32.saturating_pow(retry))
.min(self.max_backoff);
let half = exponential / 2;
let nanos = u64::try_from(half.as_nanos()).unwrap_or(u64::MAX);
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| u64::from(elapsed.subsec_nanos()));
let jitter = Duration::from_nanos(fastrand::Rng::with_seed(seed).u64(0..=nanos));
let wait = half.saturating_add(jitter);
match server {
Some(server) => wait.max(server.min(self.max_backoff)),
None => wait,
}
}
pub(crate) async fn run<T, F, A>(
&self,
mut attempt: impl FnMut() -> A,
) -> StdResult<T, (F, u32)>
where
F: Transient,
A: Future<Output = StdResult<T, F>>,
{
let mut attempts = 0;
loop {
attempts += 1;
let failure = match attempt().await {
Ok(value) => return Ok(value),
Err(failure) => failure,
};
let retry = attempts - 1;
if !failure.is_transient() || retry >= self.max_retries {
return Err((failure, attempts));
}
sleep(self.backoff(retry, failure.retry_after())).await;
}
}
}
impl Default for Retry {
fn default() -> Self {
Self {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(5),
}
}
}
pub(crate) trait Transient {
fn is_transient(&self) -> bool;
fn retry_after(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{Retry, Transient};
struct Failure(bool);
impl Transient for Failure {
fn is_transient(&self) -> bool {
self.0
}
}
fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(future)
}
const FAST: Retry = Retry {
max_retries: 3,
initial_backoff: Duration::from_millis(1),
max_backoff: Duration::from_millis(2),
};
#[test]
fn backoff_grows_and_is_capped() {
let retry = Retry {
max_retries: 10,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_millis(300),
};
for _ in 0..20 {
let first = retry.backoff(0, None);
assert!((Duration::from_millis(50)..=Duration::from_millis(100)).contains(&first));
assert!(retry.backoff(9, None) <= Duration::from_millis(300));
assert!(retry.backoff(9, None) >= Duration::from_millis(150));
assert_eq!(
retry.backoff(0, Some(Duration::from_secs(60))),
Duration::from_millis(300)
);
}
let huge = Retry {
max_retries: 32,
initial_backoff: Duration::MAX,
max_backoff: Duration::MAX,
};
assert!(huge.backoff(u32::MAX, Some(Duration::MAX)) >= Duration::MAX / 2);
}
#[test]
fn retries_transient_failures_until_success() {
let mut calls = 0;
let result = block_on(FAST.run(|| {
calls += 1;
let result = if calls < 3 {
Err(Failure(true))
} else {
Ok(calls)
};
async move { result }
}));
assert!(matches!(result, Ok(3)));
}
#[test]
fn gives_up_on_a_lasting_failure_or_a_permanent_one() {
let mut calls = 0;
let result = block_on(FAST.run(|| {
calls += 1;
async { Err::<(), _>(Failure(true)) }
}));
assert!(matches!(result, Err((Failure(true), 4))));
assert_eq!(calls, 4);
let mut calls = 0;
let result = block_on(FAST.run(|| {
calls += 1;
async { Err::<(), _>(Failure(false)) }
}));
assert!(matches!(result, Err((Failure(false), 1))));
assert_eq!(calls, 1);
let result = block_on(Retry::NONE.run(|| async { Err::<(), _>(Failure(true)) }));
assert!(matches!(result, Err((_, 1))));
}
#[test]
fn refuses_more_than_32_retries() {
let retry = |max_retries| Retry {
max_retries,
..Retry::default()
};
assert!(retry(32).check().is_ok());
assert!(retry(33).check().is_err());
}
}