use std::future::Future;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BackoffStrategy {
Fixed,
Exponential,
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub backoff_ms: u64,
pub strategy: BackoffStrategy,
}
impl RetryPolicy {
pub fn new(max_attempts: u32, backoff_ms: u64, strategy: BackoffStrategy) -> Self {
RetryPolicy {
max_attempts,
backoff_ms,
strategy,
}
}
pub async fn execute<F, Fut, T, E>(
&self,
mut f: F,
timeout: Duration,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
let mut last_error = None;
for attempt in 0..self.max_attempts {
match tokio::time::timeout(timeout, f()).await {
Ok(Ok(result)) => return Ok(result),
Ok(Err(err)) => {
last_error = Some(err);
}
Err(_elapsed) => {
eprintln!("[etdl] retry attempt {} timed out", attempt + 1);
}
}
if attempt < self.max_attempts - 1 {
let delay_ms = match self.strategy {
BackoffStrategy::Fixed => self.backoff_ms,
BackoffStrategy::Exponential => {
self.backoff_ms * 2u64.pow(attempt)
}
};
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
if let Some(err) = last_error {
Err(err)
} else {
panic!("retry exhausted all {} attempts without an error result", self.max_attempts)
}
}
}
impl Default for RetryPolicy {
fn default() -> Self {
RetryPolicy {
max_attempts: 1,
backoff_ms: 0,
strategy: BackoffStrategy::Fixed,
}
}
}