backon/backoff/
constant.rs

1use core::time::Duration;
2
3use crate::backoff::BackoffBuilder;
4
5/// ConstantBuilder is used to create a [`ConstantBackoff`], providing a steady delay with a fixed number of retries.
6///
7/// # Default
8///
9/// - delay: 1s
10/// - max_times: 3
11///
12/// # Examples
13///
14/// ```no_run
15/// use anyhow::Result;
16/// use backon::ConstantBuilder;
17/// use backon::Retryable;
18///
19/// async fn fetch() -> Result<String> {
20///     Ok(reqwest::get("https://www.rust-lang.org")
21///         .await?
22///         .text()
23///         .await?)
24/// }
25///
26/// #[tokio::main(flavor = "current_thread")]
27/// async fn main() -> Result<()> {
28///     let content = fetch.retry(ConstantBuilder::default()).await?;
29///     println!("fetch succeeded: {}", content);
30///
31///     Ok(())
32/// }
33/// ```
34#[derive(Debug, Clone, Copy)]
35pub struct ConstantBuilder {
36    delay: Duration,
37    max_times: Option<usize>,
38    jitter: bool,
39    seed: Option<u64>,
40}
41
42impl Default for ConstantBuilder {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl ConstantBuilder {
49    /// Create a new `ConstantBuilder` with default values.
50    pub const fn new() -> Self {
51        Self {
52            delay: Duration::from_secs(1),
53            max_times: Some(3),
54            jitter: false,
55            seed: None,
56        }
57    }
58
59    /// Set the delay for the backoff.
60    pub const fn with_delay(mut self, delay: Duration) -> Self {
61        self.delay = delay;
62        self
63    }
64
65    /// Set the maximum number of attempts to be made.
66    pub const fn with_max_times(mut self, max_times: usize) -> Self {
67        self.max_times = Some(max_times);
68        self
69    }
70
71    /// Enable jitter for the backoff.
72    ///
73    /// Jitter is a random value added to the delay to prevent a thundering herd problem.
74    pub const fn with_jitter(mut self) -> Self {
75        self.jitter = true;
76        self
77    }
78
79    /// Set the seed value for the jitter random number generator. If no seed is given, a random seed is used in std and default seed is used in no_std.
80    pub fn with_jitter_seed(mut self, seed: u64) -> Self {
81        self.seed = Some(seed);
82        self
83    }
84
85    /// Set no max times for the backoff.
86    ///
87    /// The backoff will not stop by itself.
88    ///
89    /// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
90    pub const fn without_max_times(mut self) -> Self {
91        self.max_times = None;
92        self
93    }
94}
95
96impl BackoffBuilder for ConstantBuilder {
97    type Backoff = ConstantBackoff;
98
99    fn build(self) -> Self::Backoff {
100        ConstantBackoff {
101            delay: self.delay,
102            max_times: self.max_times,
103
104            attempts: 0,
105            jitter: self.jitter,
106            rng: if let Some(seed) = self.seed {
107                fastrand::Rng::with_seed(seed)
108            } else {
109                #[cfg(feature = "std")]
110                let rng = fastrand::Rng::new();
111
112                #[cfg(not(feature = "std"))]
113                let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
114
115                rng
116            },
117        }
118    }
119}
120
121impl BackoffBuilder for &ConstantBuilder {
122    type Backoff = ConstantBackoff;
123
124    fn build(self) -> Self::Backoff {
125        (*self).build()
126    }
127}
128
129/// ConstantBackoff offers a consistent delay with a limited number of retries.
130///
131/// This backoff strategy is constructed by [`ConstantBuilder`].
132#[doc(hidden)]
133#[derive(Debug)]
134pub struct ConstantBackoff {
135    delay: Duration,
136    max_times: Option<usize>,
137
138    attempts: usize,
139    jitter: bool,
140    rng: fastrand::Rng,
141}
142
143impl Iterator for ConstantBackoff {
144    type Item = Duration;
145
146    fn next(&mut self) -> Option<Self::Item> {
147        let mut delay = || match self.jitter {
148            true => self.delay + self.delay.mul_f32(self.rng.f32()),
149            false => self.delay,
150        };
151        match self.max_times {
152            None => Some(delay()),
153            Some(max_times) => {
154                if self.attempts >= max_times {
155                    None
156                } else {
157                    self.attempts += 1;
158                    Some(delay())
159                }
160            }
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use core::time::Duration;
168
169    #[cfg(target_arch = "wasm32")]
170    use wasm_bindgen_test::wasm_bindgen_test as test;
171
172    use super::*;
173
174    const TEST_BUILDER: ConstantBuilder = ConstantBuilder::new()
175        .with_delay(Duration::from_secs(2))
176        .with_max_times(5)
177        .with_jitter();
178
179    #[test]
180    fn test_constant_default() {
181        let mut it = ConstantBuilder::default().build();
182
183        assert_eq!(Some(Duration::from_secs(1)), it.next());
184        assert_eq!(Some(Duration::from_secs(1)), it.next());
185        assert_eq!(Some(Duration::from_secs(1)), it.next());
186        assert_eq!(None, it.next());
187    }
188
189    #[test]
190    fn test_constant_with_delay() {
191        let mut it = ConstantBuilder::default()
192            .with_delay(Duration::from_secs(2))
193            .build();
194
195        assert_eq!(Some(Duration::from_secs(2)), it.next());
196        assert_eq!(Some(Duration::from_secs(2)), it.next());
197        assert_eq!(Some(Duration::from_secs(2)), it.next());
198        assert_eq!(None, it.next());
199    }
200
201    #[test]
202    fn test_constant_with_times() {
203        let mut it = ConstantBuilder::default().with_max_times(1).build();
204
205        assert_eq!(Some(Duration::from_secs(1)), it.next());
206        assert_eq!(None, it.next());
207    }
208
209    #[test]
210    fn test_constant_with_jitter() {
211        let mut it = ConstantBuilder::default().with_jitter().build();
212
213        let dur = it.next().unwrap();
214        fastrand::seed(7);
215        assert!(dur > Duration::from_secs(1));
216    }
217
218    #[test]
219    fn test_constant_without_max_times() {
220        let mut it = ConstantBuilder::default().without_max_times().build();
221
222        for _ in 0..10_000 {
223            assert_eq!(Some(Duration::from_secs(1)), it.next());
224        }
225    }
226
227    // allow assertions on constants because they are not optimized out by unit tests
228    #[allow(clippy::assertions_on_constants)]
229    #[test]
230    fn test_constant_const_builder() {
231        assert_eq!(TEST_BUILDER.delay, Duration::from_secs(2));
232        assert_eq!(TEST_BUILDER.max_times, Some(5));
233        assert!(TEST_BUILDER.jitter);
234    }
235}