use std::fmt::Debug;
use std::time::Duration;
use tokio::time;
use tracing::warn;
#[derive(Copy, Clone, Debug)]
pub struct Retry {
pub name: &'static str,
pub attempts: u32,
pub base_delay: Duration,
pub delay_factor: f64,
pub enable_jitter: bool,
}
impl Retry {
pub const fn new(name: &'static str) -> Self {
Self {
name,
attempts: 3,
base_delay: Duration::ZERO,
delay_factor: 1.0,
enable_jitter: false,
}
}
pub const fn attempts(mut self, attempts: u32) -> Self {
self.attempts = attempts;
self
}
pub const fn base_delay(mut self, base_delay: Duration) -> Self {
self.base_delay = base_delay;
self
}
pub const fn delay_factor(mut self, delay_factor: f64) -> Self {
self.delay_factor = delay_factor;
self
}
pub const fn jitter(mut self, enabled: bool) -> Self {
self.enable_jitter = enabled;
self
}
fn apply_jitter(&self, delay: Duration) -> Duration {
if self.enable_jitter {
delay.mul_f64(0.5 + fastrand::f64() / 2.0)
} else {
delay
}
}
pub async fn run<T, E: Debug>(
self,
mut func: impl AsyncFnMut() -> Result<T, E>,
) -> Result<T, E> {
assert!(self.attempts > 0, "attempts must be greater than 0");
assert!(
self.base_delay >= Duration::ZERO && self.delay_factor >= 0.0,
"retry delay cannot be negative"
);
let mut delay = self.base_delay;
for i in 0..self.attempts {
match func().await {
Ok(value) => return Ok(value),
Err(err) if i == self.attempts - 1 => return Err(err),
Err(err) => {
warn!(?err, "failed retryable operation {}, retrying", self.name);
time::sleep(self.apply_jitter(delay)).await;
delay = delay.mul_f64(self.delay_factor);
}
}
}
unreachable!();
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tokio::time::Instant;
use super::Retry;
#[tokio::test]
#[should_panic]
async fn zero_retry_attempts() {
let _ = Retry::new("test")
.attempts(0)
.run(async || Ok::<_, std::io::Error>(()))
.await;
}
#[tokio::test]
async fn successful_retry() {
let mut count = 0;
let task = Retry::new("test").run(async || {
count += 1;
Ok::<_, std::io::Error>(())
});
let result = task.await;
assert_eq!(count, 1);
assert!(result.is_ok());
}
#[tokio::test]
async fn failed_retry() {
let mut count = 0;
let retry = Retry::new("test");
let task = retry.run(async || {
count += 1;
Err::<(), ()>(())
});
let result = task.await;
assert_eq!(count, retry.attempts);
assert!(result.is_err());
}
#[tokio::test(start_paused = true)]
async fn delayed_retry() {
let start = Instant::now();
let mut count = 0;
let task = Retry::new("test")
.attempts(5)
.base_delay(Duration::from_secs(1))
.delay_factor(2.0)
.run(async || {
count += 1;
println!("elapsed = {:?}", start.elapsed());
if start.elapsed() < Duration::from_secs(5) {
Err::<(), ()>(())
} else {
Ok(())
}
});
let result = task.await;
assert_eq!(count, 4);
assert!(result.is_ok());
}
#[tokio::test(start_paused = true)]
async fn delayed_retry_with_jitter() {
let start = Instant::now();
let mut count = 0;
let task = Retry::new("test_jitter")
.attempts(4)
.base_delay(Duration::from_millis(100))
.delay_factor(10.0)
.jitter(true)
.run(async || {
count += 1;
println!("elapsed = {:?}", start.elapsed());
if start.elapsed() < Duration::from_millis(500) {
Err::<(), ()>(())
} else {
Ok(())
}
});
let result = task.await;
assert_eq!(count, 3);
assert!(result.is_ok());
}
}