Skip to main content

retry_if/
configuration.rs

1use std::time::Duration;
2
3/// Configuration for an exponential backoff, allowing control over the entire strategy.
4///
5/// This will retry a failing operation up to `max_tries`, waiting for a duration of `t_wait` on the
6/// first failure and `t_wait * backoff**attempt` for all remaining attempts. The maximum single wait
7/// can be capped by `backoff_max`, and the total waiting time before failing can be set with `t_wait_max`.
8///
9/// The behavior of `t_wait_max` is such that the function guarantees it will not begin sleeping if
10/// sleeping would cause the function to exceed a total execution time of `t_wait_max`. It is
11/// however possible for the execution to exceed `t_wait_max` if the decorated code
12/// (e.g. calling an API) causes it to exceed this time.
13///
14///
15/// # Example: Classic Exponential Backoff
16/// This backoff configuration will retry up to 5 times, waiting 1 second at first, then 2 seconds,
17/// 4 seconds, etc.
18/// ```
19/// # use crate::retry_if::ExponentialBackoffConfig;
20/// # use tokio::time::Duration;
21///
22/// const BACKOFF_CONFIG: ExponentialBackoffConfig = ExponentialBackoffConfig {
23///     max_retries: 5,
24///     t_wait: Duration::from_secs(1),
25///     backoff: 2.0,
26///     t_wait_max: None,
27///     backoff_max: None,
28/// };
29/// ```
30///
31/// # Example: Constant Linear Retries
32/// This configuration will retry up to five times, with no exponential behavior, since the backoff
33/// exponent is `1.0`. It will wait 1 second for all retry attempts.
34/// ```
35/// # use crate::retry_if::ExponentialBackoffConfig;
36/// # use tokio::time::Duration;
37///
38/// const BACKOFF_CONFIG: ExponentialBackoffConfig = ExponentialBackoffConfig {
39///     max_retries: 5,
40///     t_wait: Duration::from_secs(1),
41///     backoff: 1.0,
42///     t_wait_max: None,
43///     backoff_max: None,
44/// };
45/// ```
46///
47/// # Example: Backoff With a Maximum Wait Time
48/// This backoff configuration will retry up to 25 times, doubling the wait with each retry.
49///
50/// Setting `t_wait_max` to 10 minutes means that regardless of the number of retries or backoff
51/// exponent, it will return in 10 minutes or less.
52///
53/// ```
54/// # use crate::retry_if::ExponentialBackoffConfig;
55/// # use tokio::time::Duration;
56///
57/// const BACKOFF_CONFIG: ExponentialBackoffConfig = ExponentialBackoffConfig {
58///     max_retries: 25,
59///     t_wait: Duration::from_secs(1),
60///     backoff: 2.0,
61///     t_wait_max: Some(Duration::from_secs(600)),
62///     backoff_max: None,
63/// };
64/// ```
65///
66/// # Example: Limited Backoff
67/// This backoff configuration will retry up to 15 times, waiting 1 second at first, then 2 seconds,
68/// 4 seconds, etc.
69///
70/// Setting `backoff_max` to 30 seconds ensures it will exponentially back off until reaching a 30s
71/// wait time, then will continue to retry every 30s until it succeeds, exhausts retries, or hits
72/// `t_wait_max` (not configured in this case).
73/// ```
74/// # use crate::retry_if::ExponentialBackoffConfig;
75/// # use tokio::time::Duration;
76///
77/// const BACKOFF_CONFIG: ExponentialBackoffConfig = ExponentialBackoffConfig {
78///     max_retries: 15,
79///     t_wait: Duration::from_secs(1),
80///     backoff: 2.0,
81///     t_wait_max: None,
82///     backoff_max: Some(Duration::from_secs(30)),
83/// };
84/// ```
85#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct ExponentialBackoffConfig {
88    /// maximum number of retry attempts to make
89    pub max_retries: u32,
90    /// initial duration to wait
91    pub t_wait: Duration,
92    /// backoff exponent, e.g. `2.0` for a classic exponential backoff
93    pub backoff: f64,
94    /// maximum time to attempt retries before returning the last result
95    pub t_wait_max: Option<Duration>,
96    /// maximum time to wait for any single retry, i.e. backoff exponentially up to this duration,
97    /// then wait in constant time of `backoff_max`
98    pub backoff_max: Option<Duration>,
99}
100
101impl ExponentialBackoffConfig {
102    /// Calculate the backoff for the provided attempt.
103    ///
104    /// Example: attempt 3 with an initial t_wait of 2s would be 2.0^3 = 8s
105    ///
106    /// Note: does not take into account the maximum number of retries and will produce wait times
107    /// that the usage in the macro would not.
108    pub fn get_backoff_duration(&self, attempt: u32) -> Duration {
109        self.t_wait
110            .mul_f64(self.backoff.powi(attempt as i32))
111            .min(self.backoff_max.unwrap_or(Duration::MAX))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[cfg(feature = "serde")]
120    #[test]
121    fn test_simple_deserialization() {
122        let raw = r#"{"max_retries": 3, "t_wait": {"secs": 5,"nanos": 0}, "backoff": 2}"#;
123        let expected_config = ExponentialBackoffConfig {
124            max_retries: 3,
125            t_wait: Duration::from_secs(5),
126            backoff: 2.0,
127            t_wait_max: None,
128            backoff_max: None,
129        };
130
131        let config: ExponentialBackoffConfig = serde_json::from_str(raw).unwrap();
132
133        assert_eq!(expected_config, config);
134    }
135
136    #[cfg(feature = "serde")]
137    #[test]
138    fn test_deserialization_with_optionals() {
139        let raw = r#"{
140            "max_retries": 3,
141            "t_wait": {"secs": 5,"nanos": 0},
142            "backoff": 2,
143            "t_wait_max": {"secs": 120,"nanos": 0},
144            "backoff_max": {"secs": 15,"nanos": 0}
145        }"#;
146        let expected_config = ExponentialBackoffConfig {
147            max_retries: 3,
148            t_wait: Duration::from_secs(5),
149            backoff: 2.0,
150            t_wait_max: Some(Duration::from_secs(120)),
151            backoff_max: Some(Duration::from_secs(15)),
152        };
153
154        let config: ExponentialBackoffConfig = serde_json::from_str(raw).unwrap();
155
156        assert_eq!(expected_config, config);
157    }
158
159    #[test]
160    fn test_backoff_duration() {
161        let backoff = ExponentialBackoffConfig {
162            max_retries: 3,
163            t_wait: Duration::from_secs(1),
164            backoff: 2.0,
165            t_wait_max: None,
166            backoff_max: None,
167        };
168
169        assert_eq!(Duration::from_secs(1), backoff.get_backoff_duration(0));
170        assert_eq!(Duration::from_secs(2), backoff.get_backoff_duration(1));
171        assert_eq!(Duration::from_secs(4), backoff.get_backoff_duration(2));
172    }
173
174    #[test]
175    fn test_linear_backoff_duration() {
176        let backoff = ExponentialBackoffConfig {
177            max_retries: 3,
178            t_wait: Duration::from_secs(2),
179            backoff: 1.0,
180            t_wait_max: None,
181            backoff_max: None,
182        };
183
184        assert_eq!(Duration::from_secs(2), backoff.get_backoff_duration(0));
185        assert_eq!(Duration::from_secs(2), backoff.get_backoff_duration(1));
186        assert_eq!(Duration::from_secs(2), backoff.get_backoff_duration(2));
187    }
188
189    #[test]
190    fn test_backoff_duration_exponent() {
191        let backoff = ExponentialBackoffConfig {
192            max_retries: 3,
193            t_wait: Duration::from_secs(1),
194            backoff: 3.0,
195            t_wait_max: None,
196            backoff_max: None,
197        };
198
199        assert_eq!(Duration::from_secs(1), backoff.get_backoff_duration(0));
200        assert_eq!(Duration::from_secs(3), backoff.get_backoff_duration(1));
201        assert_eq!(Duration::from_secs(9), backoff.get_backoff_duration(2));
202    }
203
204    #[test]
205    fn test_backoff_duration_max_wait() {
206        let backoff = ExponentialBackoffConfig {
207            max_retries: 3,
208            t_wait: Duration::from_secs(1),
209            backoff: 2.0,
210            t_wait_max: None,
211            backoff_max: Some(Duration::from_secs(3)),
212        };
213
214        assert_eq!(Duration::from_secs(3), backoff.get_backoff_duration(2));
215    }
216}