1use std::future::Future;
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum BackoffStrategy {
6 Fixed,
7 Exponential,
8}
9
10#[derive(Debug, Clone)]
11pub struct RetryPolicy {
12 pub max_attempts: u32,
13 pub backoff_ms: u64,
14 pub strategy: BackoffStrategy,
15}
16
17impl RetryPolicy {
18 pub fn new(max_attempts: u32, backoff_ms: u64, strategy: BackoffStrategy) -> Self {
19 RetryPolicy {
20 max_attempts,
21 backoff_ms,
22 strategy,
23 }
24 }
25
26 pub async fn execute<F, Fut, T, E>(
27 &self,
28 mut f: F,
29 timeout: Duration,
30 ) -> Result<T, E>
31 where
32 F: FnMut() -> Fut,
33 Fut: Future<Output = Result<T, E>>,
34 E: std::fmt::Debug,
35 {
36 let mut last_error = None;
37
38 for attempt in 0..self.max_attempts {
39 match tokio::time::timeout(timeout, f()).await {
40 Ok(Ok(result)) => return Ok(result),
41 Ok(Err(err)) => {
42 last_error = Some(err);
43 }
44 Err(_elapsed) => {
45 eprintln!("[etdl] retry attempt {} timed out", attempt + 1);
46 }
47 }
48
49 if attempt < self.max_attempts - 1 {
50 let delay_ms = match self.strategy {
51 BackoffStrategy::Fixed => self.backoff_ms,
52 BackoffStrategy::Exponential => {
53 self.backoff_ms * 2u64.pow(attempt)
54 }
55 };
56 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
57 }
58 }
59
60 if let Some(err) = last_error {
61 Err(err)
62 } else {
63 panic!("retry exhausted all {} attempts without an error result", self.max_attempts)
64 }
65 }
66}
67
68impl Default for RetryPolicy {
69 fn default() -> Self {
70 RetryPolicy {
71 max_attempts: 1,
72 backoff_ms: 0,
73 strategy: BackoffStrategy::Fixed,
74 }
75 }
76}