/rust/registry/src/index.crates.io-1949cf8c6b5b557f/backon-1.6.0/src/backoff/exponential.rs
Line | Count | Source |
1 | | use core::time::Duration; |
2 | | |
3 | | use crate::backoff::BackoffBuilder; |
4 | | |
5 | | /// ExponentialBuilder is used to construct an [`ExponentialBackoff`] that offers delays with exponential retries. |
6 | | /// |
7 | | /// # Default |
8 | | /// |
9 | | /// - jitter: false |
10 | | /// - factor: 2 |
11 | | /// - min_delay: 1s |
12 | | /// - max_delay: 60s |
13 | | /// - max_times: 3 |
14 | | /// |
15 | | /// # Examples |
16 | | /// |
17 | | /// ```no_run |
18 | | /// use anyhow::Result; |
19 | | /// use backon::ExponentialBuilder; |
20 | | /// use backon::Retryable; |
21 | | /// |
22 | | /// async fn fetch() -> Result<String> { |
23 | | /// Ok(reqwest::get("https://www.rust-lang.org") |
24 | | /// .await? |
25 | | /// .text() |
26 | | /// .await?) |
27 | | /// } |
28 | | /// |
29 | | /// #[tokio::main(flavor = "current_thread")] |
30 | | /// async fn main() -> Result<()> { |
31 | | /// let content = fetch.retry(ExponentialBuilder::default()).await?; |
32 | | /// println!("fetch succeeded: {}", content); |
33 | | /// |
34 | | /// Ok(()) |
35 | | /// } |
36 | | /// ``` |
37 | | #[derive(Debug, Clone, Copy)] |
38 | | pub struct ExponentialBuilder { |
39 | | jitter: bool, |
40 | | factor: f32, |
41 | | min_delay: Duration, |
42 | | max_delay: Option<Duration>, |
43 | | max_times: Option<usize>, |
44 | | total_delay: Option<Duration>, |
45 | | seed: Option<u64>, |
46 | | } |
47 | | |
48 | | impl Default for ExponentialBuilder { |
49 | 0 | fn default() -> Self { |
50 | 0 | Self::new() |
51 | 0 | } |
52 | | } |
53 | | |
54 | | impl ExponentialBuilder { |
55 | | /// Create a new `ExponentialBuilder` with default values. |
56 | 0 | pub const fn new() -> Self { |
57 | 0 | Self { |
58 | 0 | jitter: false, |
59 | 0 | factor: 2.0, |
60 | 0 | min_delay: Duration::from_secs(1), |
61 | 0 | max_delay: Some(Duration::from_secs(60)), |
62 | 0 | max_times: Some(3), |
63 | 0 | total_delay: None, |
64 | 0 | seed: None, |
65 | 0 | } |
66 | 0 | } |
67 | | |
68 | | /// Enable jitter for the backoff. |
69 | | /// |
70 | | /// When jitter is enabled, [`ExponentialBackoff`] will add a random jitter within `(0, current_delay)` |
71 | | /// to the current delay. |
72 | 0 | pub const fn with_jitter(mut self) -> Self { |
73 | 0 | self.jitter = true; |
74 | 0 | self |
75 | 0 | } |
76 | | |
77 | | /// 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. |
78 | 0 | pub fn with_jitter_seed(mut self, seed: u64) -> Self { |
79 | 0 | self.seed = Some(seed); |
80 | 0 | self |
81 | 0 | } |
82 | | |
83 | | /// Set the factor for the backoff. |
84 | | /// |
85 | | /// Note: Having a factor less than `1.0` does not make any sense as it would create a |
86 | | /// smaller negative backoff. |
87 | 0 | pub const fn with_factor(mut self, factor: f32) -> Self { |
88 | 0 | self.factor = factor; |
89 | 0 | self |
90 | 0 | } |
91 | | |
92 | | /// Set the minimum delay for the backoff. |
93 | 0 | pub const fn with_min_delay(mut self, min_delay: Duration) -> Self { |
94 | 0 | self.min_delay = min_delay; |
95 | 0 | self |
96 | 0 | } |
97 | | |
98 | | /// Set the maximum delay for the backoff. |
99 | | /// |
100 | | /// The delay will not increase if the current delay exceeds the maximum delay. |
101 | 0 | pub const fn with_max_delay(mut self, max_delay: Duration) -> Self { |
102 | 0 | self.max_delay = Some(max_delay); |
103 | 0 | self |
104 | 0 | } |
105 | | |
106 | | /// Set no maximum delay for the backoff. |
107 | | /// |
108 | | /// The delay will keep increasing. |
109 | | /// |
110 | | /// _The delay will saturate at `Duration::MAX` which is an **unrealistic** delay._ |
111 | 0 | pub const fn without_max_delay(mut self) -> Self { |
112 | 0 | self.max_delay = None; |
113 | 0 | self |
114 | 0 | } |
115 | | |
116 | | /// Set the maximum number of attempts for the current backoff. |
117 | | /// |
118 | | /// The backoff will stop if the maximum number of attempts is reached. |
119 | 0 | pub const fn with_max_times(mut self, max_times: usize) -> Self { |
120 | 0 | self.max_times = Some(max_times); |
121 | 0 | self |
122 | 0 | } |
123 | | |
124 | | /// Set no maximum number of attempts for the current backoff. |
125 | | /// |
126 | | /// The backoff will not stop by itself. |
127 | | /// |
128 | | /// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._ |
129 | 0 | pub const fn without_max_times(mut self) -> Self { |
130 | 0 | self.max_times = None; |
131 | 0 | self |
132 | 0 | } |
133 | | |
134 | | /// Set the total delay for the backoff. |
135 | | /// |
136 | | /// The backoff will stop yielding sleep durations once the cumulative sleep time |
137 | | /// plus the next sleep duration would exceed `total_delay`. |
138 | 0 | pub const fn with_total_delay(mut self, total_delay: Option<Duration>) -> Self { |
139 | 0 | self.total_delay = total_delay; |
140 | 0 | self |
141 | 0 | } |
142 | | } |
143 | | |
144 | | impl BackoffBuilder for ExponentialBuilder { |
145 | | type Backoff = ExponentialBackoff; |
146 | | |
147 | 0 | fn build(self) -> Self::Backoff { |
148 | | ExponentialBackoff { |
149 | 0 | jitter: self.jitter, |
150 | 0 | rng: if let Some(seed) = self.seed { |
151 | 0 | fastrand::Rng::with_seed(seed) |
152 | | } else { |
153 | | #[cfg(feature = "std")] |
154 | 0 | let rng = fastrand::Rng::new(); |
155 | | |
156 | | #[cfg(not(feature = "std"))] |
157 | | let rng = fastrand::Rng::with_seed(super::RANDOM_SEED); |
158 | | |
159 | 0 | rng |
160 | | }, |
161 | 0 | factor: self.factor, |
162 | 0 | min_delay: self.min_delay, |
163 | 0 | max_delay: self.max_delay, |
164 | 0 | max_times: self.max_times, |
165 | | |
166 | 0 | current_delay: None, |
167 | | attempts: 0, |
168 | | cumulative_delay: Duration::ZERO, |
169 | 0 | total_delay: self.total_delay, |
170 | | } |
171 | 0 | } |
172 | | } |
173 | | |
174 | | impl BackoffBuilder for &ExponentialBuilder { |
175 | | type Backoff = ExponentialBackoff; |
176 | | |
177 | 0 | fn build(self) -> Self::Backoff { |
178 | 0 | (*self).build() |
179 | 0 | } |
180 | | } |
181 | | |
182 | | /// ExponentialBackoff provides a delay with exponential retries. |
183 | | /// |
184 | | /// This backoff strategy is constructed by [`ExponentialBuilder`]. |
185 | | #[doc(hidden)] |
186 | | #[derive(Debug)] |
187 | | pub struct ExponentialBackoff { |
188 | | jitter: bool, |
189 | | rng: fastrand::Rng, |
190 | | factor: f32, |
191 | | min_delay: Duration, |
192 | | max_delay: Option<Duration>, |
193 | | max_times: Option<usize>, |
194 | | total_delay: Option<Duration>, |
195 | | |
196 | | current_delay: Option<Duration>, |
197 | | cumulative_delay: Duration, |
198 | | attempts: usize, |
199 | | } |
200 | | |
201 | | impl Iterator for ExponentialBackoff { |
202 | | type Item = Duration; |
203 | | |
204 | 0 | fn next(&mut self) -> Option<Self::Item> { |
205 | 0 | if self.attempts >= self.max_times.unwrap_or(usize::MAX) { |
206 | 0 | return None; |
207 | 0 | } |
208 | 0 | self.attempts += 1; |
209 | | |
210 | 0 | let mut tmp_cur = match self.current_delay { |
211 | | None => { |
212 | | // If current_delay is None, it's must be the first time to retry. |
213 | 0 | self.min_delay |
214 | | } |
215 | 0 | Some(mut cur) => { |
216 | | // If current delay larger than max delay, we should stop increment anymore. |
217 | 0 | if let Some(max_delay) = self.max_delay { |
218 | 0 | if cur < max_delay { |
219 | 0 | cur = saturating_mul(cur, self.factor); |
220 | 0 | } |
221 | 0 | if cur > max_delay { |
222 | 0 | cur = max_delay; |
223 | 0 | } |
224 | 0 | } else { |
225 | 0 | cur = saturating_mul(cur, self.factor); |
226 | 0 | } |
227 | 0 | cur |
228 | | } |
229 | | }; |
230 | | |
231 | 0 | let current_delay = tmp_cur; |
232 | | // If jitter is enabled, add random jitter based on min delay. |
233 | 0 | if self.jitter { |
234 | 0 | tmp_cur = tmp_cur.saturating_add(tmp_cur.mul_f32(self.rng.f32())); |
235 | 0 | } |
236 | | |
237 | | // Check if adding the current delay would exceed the total delay limit. |
238 | 0 | let total_delay_check = self |
239 | 0 | .total_delay |
240 | 0 | .is_none_or(|total| self.cumulative_delay + tmp_cur <= total); |
241 | | |
242 | 0 | if !total_delay_check { |
243 | 0 | return None; |
244 | 0 | } |
245 | | |
246 | 0 | if self.total_delay.is_some() { |
247 | 0 | self.cumulative_delay = self.cumulative_delay.saturating_add(tmp_cur); |
248 | 0 | } |
249 | | |
250 | 0 | self.current_delay = Some(current_delay); |
251 | | |
252 | 0 | Some(tmp_cur) |
253 | 0 | } |
254 | | } |
255 | | |
256 | | #[inline] |
257 | 0 | pub(crate) fn saturating_mul(d: Duration, rhs: f32) -> Duration { |
258 | 0 | Duration::try_from_secs_f32(rhs * d.as_secs_f32()).unwrap_or(Duration::MAX) |
259 | 0 | } |
260 | | |
261 | | #[cfg(test)] |
262 | | mod tests { |
263 | | use core::time::Duration; |
264 | | |
265 | | #[cfg(target_arch = "wasm32")] |
266 | | use wasm_bindgen_test::wasm_bindgen_test as test; |
267 | | |
268 | | use crate::BackoffBuilder; |
269 | | use crate::ExponentialBuilder; |
270 | | |
271 | | const TEST_BUILDER: ExponentialBuilder = ExponentialBuilder::new() |
272 | | .with_jitter() |
273 | | .with_factor(1.5) |
274 | | .with_min_delay(Duration::from_secs(2)) |
275 | | .with_max_delay(Duration::from_secs(30)) |
276 | | .with_max_times(5); |
277 | | |
278 | | #[test] |
279 | | fn test_exponential_default() { |
280 | | let mut exp = ExponentialBuilder::default().build(); |
281 | | |
282 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
283 | | assert_eq!(Some(Duration::from_secs(2)), exp.next()); |
284 | | assert_eq!(Some(Duration::from_secs(4)), exp.next()); |
285 | | assert_eq!(None, exp.next()); |
286 | | } |
287 | | |
288 | | #[test] |
289 | | fn test_exponential_factor() { |
290 | | let mut exp = ExponentialBuilder::default().with_factor(1.5).build(); |
291 | | |
292 | | assert_eq!(Some(Duration::from_secs_f32(1.0)), exp.next()); |
293 | | assert_eq!(Some(Duration::from_secs_f32(1.5)), exp.next()); |
294 | | assert_eq!(Some(Duration::from_secs_f32(2.25)), exp.next()); |
295 | | assert_eq!(None, exp.next()); |
296 | | } |
297 | | |
298 | | #[test] |
299 | | fn test_exponential_jitter() { |
300 | | let mut exp = ExponentialBuilder::default().with_jitter().build(); |
301 | | |
302 | | let v = exp.next().expect("value must valid"); |
303 | | assert!(v >= Duration::from_secs(1), "current: {v:?}"); |
304 | | assert!(v < Duration::from_secs(2), "current: {v:?}"); |
305 | | |
306 | | let v = exp.next().expect("value must valid"); |
307 | | assert!(v >= Duration::from_secs(2), "current: {v:?}"); |
308 | | assert!(v < Duration::from_secs(4), "current: {v:?}"); |
309 | | |
310 | | let v = exp.next().expect("value must valid"); |
311 | | assert!(v >= Duration::from_secs(4), "current: {v:?}"); |
312 | | assert!(v < Duration::from_secs(8), "current: {v:?}"); |
313 | | |
314 | | assert_eq!(None, exp.next()); |
315 | | } |
316 | | |
317 | | #[test] |
318 | | fn test_exponential_min_delay() { |
319 | | let mut exp = ExponentialBuilder::default() |
320 | | .with_min_delay(Duration::from_millis(500)) |
321 | | .build(); |
322 | | |
323 | | assert_eq!(Some(Duration::from_millis(500)), exp.next()); |
324 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
325 | | assert_eq!(Some(Duration::from_secs(2)), exp.next()); |
326 | | assert_eq!(None, exp.next()); |
327 | | } |
328 | | |
329 | | #[test] |
330 | | fn test_exponential_total_delay() { |
331 | | let mut exp = ExponentialBuilder::default() |
332 | | .with_min_delay(Duration::from_secs(1)) |
333 | | .with_factor(1.0) |
334 | | .with_total_delay(Some(Duration::from_secs(3))) |
335 | | .with_max_times(5) |
336 | | .build(); |
337 | | |
338 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
339 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
340 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
341 | | assert_eq!(None, exp.next()); |
342 | | } |
343 | | |
344 | | #[test] |
345 | | fn test_exponential_no_max_times_with_default() { |
346 | | let mut exp = ExponentialBuilder::default() |
347 | | .with_min_delay(Duration::from_secs(1)) |
348 | | .with_factor(1_f32) |
349 | | .without_max_times() |
350 | | .build(); |
351 | | |
352 | | // to fully test we would need to call this `usize::MAX` |
353 | | // which seems unreasonable for a test as it would take too long... |
354 | | for _ in 0..10_000 { |
355 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
356 | | } |
357 | | } |
358 | | |
359 | | #[test] |
360 | | fn test_exponential_max_delay_with_default() { |
361 | | let mut exp = ExponentialBuilder::default() |
362 | | .with_max_delay(Duration::from_secs(2)) |
363 | | .build(); |
364 | | |
365 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
366 | | assert_eq!(Some(Duration::from_secs(2)), exp.next()); |
367 | | assert_eq!(Some(Duration::from_secs(2)), exp.next()); |
368 | | assert_eq!(None, exp.next()); |
369 | | } |
370 | | |
371 | | #[test] |
372 | | fn test_exponential_no_max_delay_with_default() { |
373 | | let mut exp = ExponentialBuilder::default() |
374 | | .with_min_delay(Duration::from_secs(1)) |
375 | | .with_factor(10_000_000_000_f32) |
376 | | .without_max_delay() |
377 | | .with_max_times(4) |
378 | | .build(); |
379 | | |
380 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
381 | | assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next()); |
382 | | assert_eq!(Some(Duration::MAX), exp.next()); |
383 | | assert_eq!(Some(Duration::MAX), exp.next()); |
384 | | assert_eq!(None, exp.next()); |
385 | | } |
386 | | |
387 | | #[test] |
388 | | fn test_exponential_max_delay_without_default_1() { |
389 | | let mut exp = ExponentialBuilder { |
390 | | jitter: false, |
391 | | seed: Some(0x2fdb0020ffc7722b), |
392 | | factor: 10_000_000_000_f32, |
393 | | min_delay: Duration::from_secs(1), |
394 | | max_delay: None, |
395 | | max_times: None, |
396 | | total_delay: None, |
397 | | } |
398 | | .build(); |
399 | | |
400 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
401 | | assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next()); |
402 | | assert_eq!(Some(Duration::MAX), exp.next()); |
403 | | assert_eq!(Some(Duration::MAX), exp.next()); |
404 | | } |
405 | | |
406 | | #[test] |
407 | | fn test_exponential_max_delay_without_default_2() { |
408 | | let mut exp = ExponentialBuilder { |
409 | | jitter: true, |
410 | | seed: Some(0x2fdb0020ffc7722b), |
411 | | factor: 10_000_000_000_f32, |
412 | | min_delay: Duration::from_secs(10_000_000_000), |
413 | | max_delay: None, |
414 | | max_times: Some(2), |
415 | | total_delay: None, |
416 | | } |
417 | | .build(); |
418 | | let v = exp.next().expect("value must valid"); |
419 | | assert!(v >= Duration::from_secs(10_000_000_000), "current: {v:?}"); |
420 | | assert!(v < Duration::from_secs(20_000_000_000), "current: {v:?}"); |
421 | | assert_eq!(Some(Duration::MAX), exp.next()); |
422 | | assert_eq!(None, exp.next()); |
423 | | } |
424 | | |
425 | | #[test] |
426 | | fn test_exponential_max_delay_without_default_3() { |
427 | | let mut exp = ExponentialBuilder { |
428 | | jitter: false, |
429 | | seed: Some(0x2fdb0020ffc7722b), |
430 | | factor: 10_000_000_000_f32, |
431 | | min_delay: Duration::from_secs(10_000_000_000), |
432 | | max_delay: Some(Duration::from_secs(60_000_000_000)), |
433 | | max_times: Some(3), |
434 | | total_delay: None, |
435 | | } |
436 | | .build(); |
437 | | assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next()); |
438 | | assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next()); |
439 | | assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next()); |
440 | | assert_eq!(None, exp.next()); |
441 | | } |
442 | | |
443 | | #[test] |
444 | | fn test_exponential_max_times() { |
445 | | let mut exp = ExponentialBuilder::default().with_max_times(1).build(); |
446 | | |
447 | | assert_eq!(Some(Duration::from_secs(1)), exp.next()); |
448 | | assert_eq!(None, exp.next()); |
449 | | } |
450 | | |
451 | | // allow assertions on constants because they are not optimized out by unit tests |
452 | | #[allow(clippy::assertions_on_constants)] |
453 | | #[test] |
454 | | fn test_exponential_const_builder() { |
455 | | assert!(TEST_BUILDER.jitter); |
456 | | assert_eq!(TEST_BUILDER.factor, 1.5); |
457 | | assert_eq!(TEST_BUILDER.min_delay, Duration::from_secs(2)); |
458 | | assert_eq!(TEST_BUILDER.max_delay, Some(Duration::from_secs(30))); |
459 | | assert_eq!(TEST_BUILDER.max_times, Some(5)); |
460 | | } |
461 | | } |