use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("retry exhausted after {attempts} attempts: {source}")]
pub struct RetryExhaustedError {
pub attempts: usize,
pub source: anyhow::Error,
}
#[derive(Clone)]
pub struct RetryStrategy {
base_delay: Duration,
max_delay: Duration,
max_attempts: usize,
}
impl RetryStrategy {
pub fn new() -> Self {
Self { base_delay: Duration::from_secs(1), max_delay: Duration::from_secs(30), max_attempts: 12 }
}
pub fn with_base_delay(mut self, d: Duration) -> Self { self.base_delay = d; self }
pub fn with_max_delay(mut self, d: Duration) -> Self { self.max_delay = d; self }
pub fn with_max_attempts(mut self, n: usize) -> Self { self.max_attempts = n; self }
pub async fn with_exponential_backoff<F, T, E>(&self, mut f: F) -> Result<T, RetryExhaustedError>
where
F: FnMut() -> futures::future::BoxFuture<'static, Result<T, E>> + Send,
E: std::fmt::Display + Send + Sync + 'static,
T: Send,
{
let mut attempt = 0usize;
let mut delay = self.base_delay;
loop {
attempt += 1;
match f().await {
Ok(v) => return Ok(v),
Err(e) => {
if attempt >= self.max_attempts {
return Err(RetryExhaustedError { attempts: attempt, source: anyhow::anyhow!(e.to_string()) });
}
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, self.max_delay);
}
}
}
}
pub async fn retry<F, Fut, T, E>(&self, mut op: F) -> Result<T, RetryExhaustedError>
where
F: FnMut() -> Fut + Send,
Fut: std::future::Future<Output = Result<T, E>> + Send,
E: std::fmt::Display + Send + Sync + 'static,
{
let mut attempt = 0usize;
let mut delay = self.base_delay;
loop {
attempt += 1;
match op().await {
Ok(v) => return Ok(v),
Err(e) => {
if attempt >= self.max_attempts {
return Err(RetryExhaustedError { attempts: attempt, source: anyhow::anyhow!(e.to_string()) });
}
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, self.max_delay);
}
}
}
}
}
impl Default for RetryStrategy {
fn default() -> Self { Self::new() }
}