backoff_config/
backoff_config.rs

1use crate::nullable::Nullable;
2use crate::*;
3use duration_string::DurationString;
4use serde::Deserialize;
5
6#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
7#[serde(tag = "strategy")]
8/// Configuration for [Backoff].
9pub enum BackoffConfig {
10    /// Configuration for [Backoff::Constant].
11    Constant {
12        /// Backoff delay.
13        ///
14        /// Defaults to `500 millis` - see [defaults::delay].
15        #[serde(default = "defaults::delay")]
16        delay: DurationString,
17
18        /// Maximum amount of retries.
19        ///
20        /// Removes the retry limit if the value is [Nullable::Null].
21        ///
22        /// Defaults to `4` - see [defaults::max_retries].
23        #[serde(default = "defaults::max_retries")]
24        max_retries: Nullable<usize>,
25
26        /// Whether jitter is enabled.
27        ///
28        /// Defaults to `true` - see [defaults::jitter_enabled].
29        #[serde(default = "defaults::jitter_enabled")]
30        jitter_enabled: bool,
31
32        /// Random seed to initialize the random jitter generator.
33        ///
34        /// Defaults to `None` - see [defaults::jitter_seed].
35        #[serde(default = "defaults::jitter_seed")]
36        jitter_seed: Option<u64>,
37    },
38
39    /// Configuration for [Backoff::Exponential].
40    Exponential {
41        /// Initial backoff delay.
42        ///
43        /// Defaults to `500 millis` - see [defaults::delay].
44        #[serde(default = "defaults::delay")]
45        initial_delay: DurationString,
46
47        /// Backoff factor.
48        ///
49        /// Defaults to `2.0` - see [defaults::factor].
50        #[serde(default = "defaults::factor")]
51        factor: f32,
52
53        /// Maximum backoff delay.
54        ///
55        /// Removes the max backoff delay limit if the value is [Nullable::Null].
56        ///
57        /// Defaults to `60 seconds` - see [defaults::max_delay].
58        #[serde(default = "defaults::max_delay")]
59        max_delay: Nullable<DurationString>,
60
61        /// Maximum amount of retries.
62        ///
63        /// Removes the retry limit if the value is [Nullable::Null].
64        ///
65        /// Defaults to `4` - see [defaults::max_retries].
66        #[serde(default = "defaults::max_retries")]
67        max_retries: Nullable<usize>,
68
69        /// Maximum total backoff delay.
70        ///
71        /// Defaults to `None` - see [defaults::max_total_delay]
72        #[serde(default = "defaults::max_total_delay")]
73        max_total_delay: Option<DurationString>,
74
75        /// Whether jitter is enabled.
76        ///
77        /// Defaults to `true` - see [defaults::jitter_enabled].
78        #[serde(default = "defaults::jitter_enabled")]
79        jitter_enabled: bool,
80
81        /// Random seed to initialize the random jitter generator.
82        ///
83        /// Defaults to `None` - see [defaults::jitter_seed].
84        #[serde(default = "defaults::jitter_seed")]
85        jitter_seed: Option<u64>,
86    },
87
88    /// Configuration for [Backoff::Fibonacci].
89    Fibonacci {
90        /// Initial backoff delay.
91        ///
92        /// Defaults to `500 millis` - see [defaults::delay].
93        #[serde(default = "defaults::delay")]
94        initial_delay: DurationString,
95
96        /// Maximum backoff delay.
97        ///
98        /// Removes the max backoff delay limit if the value is [Nullable::Null].
99        ///
100        /// Defaults to `60 seconds` - see [defaults::max_delay].
101        #[serde(default = "defaults::max_delay")]
102        max_delay: Nullable<DurationString>,
103
104        /// Maximum amount of retries.
105        ///
106        /// Removes the retry limit if the value is [Nullable::Null].
107        ///
108        /// Defaults to `4` - see [defaults::max_retries].
109        #[serde(default = "defaults::max_retries")]
110        max_retries: Nullable<usize>,
111
112        /// Whether jitter is enabled.
113        ///
114        /// Defaults to `true` - see [defaults::jitter_enabled].
115        #[serde(default = "defaults::jitter_enabled")]
116        jitter_enabled: bool,
117
118        /// Random seed to initialize the random jitter generator.
119        ///
120        /// Defaults to `None` - see [defaults::jitter_seed].
121        #[serde(default = "defaults::jitter_seed")]
122        jitter_seed: Option<u64>,
123    },
124}
125
126impl backon::BackoffBuilder for BackoffConfig {
127    type Backoff = Backoff;
128
129    fn build(self) -> Backoff {
130        match self {
131            BackoffConfig::Constant {
132                delay,
133                max_retries,
134                jitter_enabled,
135                jitter_seed,
136            } => {
137                let mut builder = backon::ConstantBuilder::new().with_delay(delay.into());
138
139                builder = match max_retries {
140                    Nullable::Some(max_retries) => builder.with_max_times(max_retries),
141                    Nullable::Null => builder.without_max_times(),
142                };
143
144                if jitter_enabled {
145                    builder = builder.with_jitter();
146                }
147
148                if let Some(jitter_seed) = jitter_seed {
149                    builder = builder.with_jitter_seed(jitter_seed);
150                }
151
152                Backoff::Constant(builder.build())
153            }
154
155            BackoffConfig::Exponential {
156                initial_delay,
157                factor,
158                max_delay,
159                max_retries,
160                max_total_delay,
161                jitter_enabled,
162                jitter_seed,
163            } => {
164                let mut builder = backon::ExponentialBuilder::new()
165                    .with_min_delay(initial_delay.into())
166                    .with_factor(factor);
167
168                builder = match max_delay {
169                    Nullable::Some(max_delay) => builder.with_max_delay(max_delay.into()),
170                    Nullable::Null => builder.without_max_delay(),
171                };
172
173                builder = match max_retries {
174                    Nullable::Some(max_retries) => builder.with_max_times(max_retries),
175                    Nullable::Null => builder.without_max_times(),
176                };
177
178                builder = builder.with_total_delay(max_total_delay.map(Into::into));
179
180                if jitter_enabled {
181                    builder = builder.with_jitter();
182                }
183
184                if let Some(jitter_seed) = jitter_seed {
185                    builder = builder.with_jitter_seed(jitter_seed);
186                }
187
188                Backoff::Exponential(builder.build())
189            }
190
191            BackoffConfig::Fibonacci {
192                initial_delay,
193                max_delay,
194                max_retries,
195                jitter_enabled,
196                jitter_seed,
197            } => {
198                let mut builder =
199                    backon::FibonacciBuilder::new().with_min_delay(initial_delay.into());
200
201                builder = match max_delay {
202                    Nullable::Some(max_delay) => builder.with_max_delay(max_delay.into()),
203                    Nullable::Null => builder.without_max_delay(),
204                };
205
206                builder = match max_retries {
207                    Nullable::Some(max_retries) => builder.with_max_times(max_retries),
208                    Nullable::Null => builder.without_max_times(),
209                };
210
211                if jitter_enabled {
212                    builder = builder.with_jitter();
213                }
214
215                if let Some(jitter_seed) = jitter_seed {
216                    builder = builder.with_jitter_seed(jitter_seed);
217                }
218
219                Backoff::Fibonacci(builder.build())
220            }
221        }
222    }
223}
224
225/// Contains the defaults used by the [crate::BackoffConfig].
226pub mod defaults {
227    use crate::nullable::Nullable;
228    use duration_string::DurationString;
229    use std::time::Duration;
230
231    /// Default value for constant / initial backoff delay.
232    pub fn delay() -> DurationString {
233        Duration::from_millis(500).into()
234    }
235
236    /// Default value for max retries.
237    pub const fn max_retries() -> Nullable<usize> {
238        Nullable::Some(4)
239    }
240
241    /// Default value whether jitter is enabled.
242    pub const fn jitter_enabled() -> bool {
243        true
244    }
245
246    /// Default value for jitter seed.
247    pub const fn jitter_seed() -> Option<u64> {
248        None
249    }
250
251    /// Default value for backoff factor.
252    pub const fn factor() -> f32 {
253        2.0
254    }
255
256    /// Default value for max backoff delay.
257    pub fn max_delay() -> Nullable<DurationString> {
258        Nullable::Some(Duration::from_secs(30).into())
259    }
260
261    /// Default value for max total backoff delay.
262    pub fn max_total_delay() -> Option<DurationString> {
263        Some(Duration::from_secs(60).into())
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use backon::BackoffBuilder;
271    use std::time::Duration;
272
273    #[test]
274    fn constant_backoff_config_to_backoff() {
275        let config = BackoffConfig::Constant {
276            delay: Duration::from_secs(1).into(),
277            max_retries: Nullable::Some(3),
278            jitter_enabled: false,
279            jitter_seed: None,
280        };
281
282        let backoff = config.build();
283        assert!(matches!(backoff, Backoff::Constant(_)));
284
285        assert_eq!(
286            backoff
287                .take(100)
288                .map(|duration| duration.as_millis())
289                .collect::<Vec<_>>(),
290            vec![1000; 3]
291        );
292    }
293
294    #[test]
295    fn exponential_backoff_config_to_backoff() {
296        let config = BackoffConfig::Exponential {
297            initial_delay: Duration::from_millis(100).into(),
298            factor: 2_f32,
299            max_delay: Nullable::Some(Duration::from_millis(800).into()),
300            max_retries: Nullable::Some(5),
301            max_total_delay: None,
302            jitter_enabled: false,
303            jitter_seed: None,
304        };
305
306        let backoff = config.build();
307        assert!(matches!(backoff, Backoff::Exponential(_)));
308
309        assert_eq!(
310            backoff
311                .take(100)
312                .map(|duration| duration.as_millis())
313                .collect::<Vec<_>>(),
314            vec![100, 200, 400, 800, 800]
315        );
316    }
317
318    #[test]
319    fn exponential_backoff_config_to_backoff_with_max_total_delay() {
320        let config = BackoffConfig::Exponential {
321            initial_delay: Duration::from_millis(100).into(),
322            factor: 2_f32,
323            max_delay: Nullable::Some(Duration::from_millis(800).into()),
324            max_retries: Nullable::Some(5),
325            max_total_delay: Some(Duration::from_millis(1500 + 1).into()),
326            jitter_enabled: false,
327            jitter_seed: None,
328        };
329
330        let backoff = config.build();
331        assert!(matches!(backoff, Backoff::Exponential(_)));
332
333        assert_eq!(
334            backoff
335                .take(100)
336                .map(|duration| duration.as_millis())
337                .collect::<Vec<_>>(),
338            vec![100, 200, 400, 800]
339        );
340    }
341
342    #[test]
343    fn fibonacci_backoff_config_to_backoff() {
344        let config = BackoffConfig::Fibonacci {
345            initial_delay: Duration::from_millis(100).into(),
346            max_delay: Nullable::Some(Duration::from_millis(800).into()),
347            max_retries: Nullable::Some(5),
348            jitter_enabled: false,
349            jitter_seed: None,
350        };
351
352        let backoff = config.build();
353        assert!(matches!(backoff, Backoff::Fibonacci(_)));
354
355        assert_eq!(
356            backoff
357                .take(usize::MAX)
358                .map(|duration| duration.as_millis())
359                .collect::<Vec<_>>(),
360            vec![100, 100, 200, 300, 500]
361        );
362    }
363}