retry_async/
lib.rs

1use futures::Future;
2use rand::prelude::*;
3use std::{cell::RefCell, thread, time::Duration};
4
5thread_local!(static RNG: RefCell<rand::prelude::ThreadRng> = {
6    RefCell::new(rand::thread_rng())
7});
8
9#[derive(Debug)]
10pub enum Details {
11    Duplicate,
12    Throttled,
13    NotFound,
14    Unspecified,
15}
16
17#[derive(Debug)]
18pub enum Error<K> {
19    Transient(Details),
20    Permanent(Details),
21    Exhausted(Details),
22    CustomTransient(K),
23    CustomPermanent(K),
24    CustomExhausted(K),
25}
26
27impl<K> From<azure_core::error::Error> for Error<K> {
28    fn from(error: azure_core::error::Error) -> Self {
29        match error.kind() {
30            azure_core::error::ErrorKind::HttpResponse {
31                status,
32                error_code: _error_code,
33            } => match status {
34                404 => Error::Permanent(Details::NotFound),
35                409 => Error::Permanent(Details::Duplicate),
36                429 => Error::Transient(Details::Throttled),
37                _ => Error::Permanent(Details::Unspecified),
38            },
39            azure_core::error::ErrorKind::Io => Error::Transient(Details::Unspecified),
40            azure_core::error::ErrorKind::DataConversion => Error::Permanent(Details::Unspecified),
41            azure_core::error::ErrorKind::Credential => Error::Permanent(Details::Unspecified),
42            azure_core::error::ErrorKind::Other => Error::Transient(Details::Unspecified),
43        }
44    }
45}
46
47pub struct Settings<'a> {
48    pub attempts: u8,
49
50    pub initial_delay: Duration,
51
52    pub backoff: f64,
53
54    // In case caller wants to reuse a rand generator.
55    pub rng: Option<&'a mut rand::prelude::ThreadRng>,
56}
57
58pub async fn retry<'a, F, K, E, Fut>(
59    func: F,
60    settings: Option<&mut Settings<'a>>,
61) -> Result<K, Error<E>>
62where
63    F: Fn() -> Fut,
64    Fut: Future<Output = Result<K, Error<E>>>,
65{
66    let mut settings_holder;
67    let settings = match settings {
68        Some(settings) => settings,
69        None => {
70            settings_holder = Settings {
71                attempts: 5,
72                initial_delay: Duration::from_millis(100),
73                backoff: 2.0,
74                rng: None,
75            };
76            &mut settings_holder
77        }
78    };
79
80    // NOTE: We start index at one for easier comparison with attempts.
81    let mut attempt = 1;
82
83    let mut wait = Duration::default();
84    loop {
85        match func().await {
86            Ok(k) => return Ok(k),
87            Err(err) => {
88                if attempt == settings.attempts {
89                    let err = match err {
90                        Error::Transient(err) => Error::Exhausted(err),
91                        Error::Permanent(err) | Error::Exhausted(err) => Error::Exhausted(err),
92                        Error::CustomPermanent(err)
93                        | Error::CustomExhausted(err)
94                        | Error::CustomTransient(err) => Error::CustomExhausted(err),
95                    };
96
97                    return Err(err);
98                }
99
100                match err {
101                    Error::Transient(_) | Error::CustomTransient(_) => {
102                        wait = if attempt == 1 {
103                            settings.initial_delay
104                        } else {
105                            wait.mul_f64(settings.backoff)
106                        };
107
108                        // Randomized exponential backoff, to avoid thundering herd problem.
109
110                        let fuzz: f64 = match &mut settings.rng {
111                            Some(rng) => rng.gen(),
112                            None => RNG.with(|rng| rng.borrow_mut().gen::<f64>()),
113                        };
114
115                        // Adjust random factor.
116                        let fuzz = fuzz / 2.0 + 0.75;
117
118                        // Calculate time to wait, including random factor.
119                        let wait = wait.mul_f64(fuzz);
120
121                        thread::sleep(wait);
122
123                        attempt += 1;
124                        continue;
125                    }
126                    Error::Permanent(_)
127                    | Error::Exhausted(_)
128                    | Error::CustomPermanent(_)
129                    | Error::CustomExhausted(_) => return Err(err),
130                }
131            }
132        };
133    }
134}