#![forbid(unsafe_code)]
use std::future::Future;
use std::time::Duration;
use tokio::time::{sleep, timeout};
const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
const DEFAULT_INITIAL_DELAY: Duration = Duration::from_millis(100);
#[derive(Clone, Copy)]
enum DelayStrategy {
Fixed { delay: Duration },
Exponential {
base_delay: Duration,
max_delay: Duration,
factor: f64,
},
}
#[must_use = "retry builder does nothing unless `.run()` is called"]
pub struct Retry<E> {
max_attempts: usize,
strategy: DelayStrategy,
timeout_per_attempt: Option<Duration>,
condition: Option<Box<dyn Fn(&E) -> bool + Send + Sync>>,
on_retry: Option<Box<dyn Fn(usize, &E, Duration) + Send + Sync>>,
}
impl<E> Retry<E> {
pub fn new() -> Self {
Self {
max_attempts: 3,
strategy: DelayStrategy::Exponential {
base_delay: DEFAULT_INITIAL_DELAY,
max_delay: DEFAULT_MAX_DELAY,
factor: 2.0,
},
timeout_per_attempt: None,
condition: None,
on_retry: None,
}
}
pub fn times(mut self, attempts: usize) -> Self {
self.max_attempts = attempts.max(1);
self
}
pub fn fixed(mut self, delay: Duration) -> Self {
self.strategy = DelayStrategy::Fixed { delay };
self
}
pub fn exponential(mut self) -> Self {
self.strategy = DelayStrategy::Exponential {
base_delay: DEFAULT_INITIAL_DELAY,
max_delay: DEFAULT_MAX_DELAY,
factor: 2.0,
};
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout_per_attempt = Some(duration);
self
}
pub fn when<F>(mut self, condition: F) -> Self
where
F: Fn(&E) -> bool + Send + Sync + 'static,
{
self.condition = Some(Box::new(condition));
self
}
pub fn on_retry<F>(mut self, hook: F) -> Self
where
F: Fn(usize, &E, Duration) + Send + Sync + 'static,
{
self.on_retry = Some(Box::new(hook));
self
}
pub async fn run<Op, Fut, T>(self, mut op: Op) -> Result<T, E>
where
Op: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
E: From<tokio::time::error::Elapsed>,
{
let mut attempt = 0usize;
loop {
attempt += 1;
let result = if let Some(dur) = self.timeout_per_attempt {
match timeout(dur, op()).await {
Ok(res) => res,
Err(elapsed) => Err(E::from(elapsed)),
}
} else {
op().await
};
match result {
Ok(value) => return Ok(value),
Err(err) => {
let should_retry = attempt < self.max_attempts
&& self
.condition
.as_ref()
.map(|f| f(&err))
.unwrap_or(true);
if !should_retry {
return Err(err);
}
let delay = self.next_delay(attempt);
if let Some(ref hook) = self.on_retry {
hook(attempt, &err, delay);
}
sleep(delay).await;
}
}
}
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn next_delay(&self, attempt: usize) -> Duration {
match self.strategy {
DelayStrategy::Fixed { delay } => delay,
DelayStrategy::Exponential {
base_delay,
max_delay,
factor,
} => {
let exp = (attempt.saturating_sub(1)) as u32;
let multiplier = factor.powi(exp as i32);
let nanos =
(base_delay.as_nanos() as f64 * multiplier).min(max_delay.as_nanos() as f64);
Duration::from_nanos(nanos as u64)
}
}
}
}
pub fn retry<E>() -> Retry<E> {
Retry::new()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Debug, Clone)]
struct MyError;
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "my error")
}
}
impl std::error::Error for MyError {}
impl From<tokio::time::error::Elapsed> for MyError {
fn from(_: tokio::time::error::Elapsed) -> Self {
MyError
}
}
#[tokio::test]
async fn retries_until_success() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let result: usize = retry::<MyError>()
.times(3)
.fixed(Duration::from_millis(1))
.run(move || {
let counter = counter_clone.clone();
async move {
let current = counter.fetch_add(1, Ordering::SeqCst);
if current < 2 {
Err(MyError)
} else {
Ok(current)
}
}
})
.await
.unwrap();
assert_eq!(result, 2);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 3);
}
#[tokio::test]
async fn obeys_when_filter() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
#[derive(Debug, Clone)]
enum ApiError {
Timeout,
Fatal,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::Timeout => write!(f, "timeout"),
ApiError::Fatal => write!(f, "fatal"),
}
}
}
impl std::error::Error for ApiError {}
impl From<tokio::time::error::Elapsed> for ApiError {
fn from(_: tokio::time::error::Elapsed) -> Self {
ApiError::Timeout
}
}
let err = retry::<ApiError>()
.times(5)
.fixed(Duration::from_millis(1))
.when(|err| matches!(err, ApiError::Timeout))
.run(move || {
let counter = counter_clone.clone();
async move {
let n = counter.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Err::<(), ApiError>(ApiError::Timeout)
} else {
Err::<(), ApiError>(ApiError::Fatal)
}
}
})
.await
.unwrap_err();
assert!(matches!(err, ApiError::Fatal));
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
}