google_cloud_gax/
exponential_backoff.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Common implements for exponential backoff.
16//!
17//! This module provides an implementation of truncated [exponential backoff].
18//! It implements the [BackoffPolicy] and [PollingBackoffPolicy] traits.
19//!
20//! [BackoffPolicy]: crate::backoff_policy::BackoffPolicy
21//! [PollingBackoffPolicy]: crate::polling_backoff_policy::PollingBackoffPolicy
22
23use crate::polling_state::PollingState;
24use crate::retry_state::RetryState;
25use std::time::Duration;
26
27/// The error type for exponential backoff creation.
28#[derive(thiserror::Error, Debug)]
29#[non_exhaustive]
30pub enum Error {
31    #[error("the scaling value ({0}) should be >= 1.0")]
32    InvalidScalingFactor(f64),
33    #[error("the initial delay ({0:?}) should be greater than zero")]
34    InvalidInitialDelay(Duration),
35    #[error(
36        "the maximum delay ({maximum:?}) should be greater than or equal to the initial delay ({initial:?})"
37    )]
38    EmptyRange {
39        maximum: Duration,
40        initial: Duration,
41    },
42}
43
44/// Implements truncated exponential backoff with jitter.
45#[derive(Clone, Debug)]
46pub struct ExponentialBackoffBuilder {
47    initial_delay: Duration,
48    maximum_delay: Duration,
49    scaling: f64,
50}
51
52impl ExponentialBackoffBuilder {
53    /// Creates a builder with the default parameters.
54    ///
55    /// # Example
56    /// ```
57    /// # use google_cloud_gax::exponential_backoff::Error;
58    /// # use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
59    /// use std::time::Duration;
60    ///
61    /// let policy = ExponentialBackoffBuilder::new()
62    ///         .with_initial_delay(Duration::from_millis(100))
63    ///         .with_maximum_delay(Duration::from_secs(5))
64    ///         .with_scaling(4.0)
65    ///         .build()?;
66    /// # Ok::<(), Error>(())
67    /// ```
68    pub fn new() -> Self {
69        Self {
70            initial_delay: Duration::from_secs(1),
71            maximum_delay: Duration::from_secs(60),
72            scaling: 2.0,
73        }
74    }
75
76    /// Change the initial delay.
77    pub fn with_initial_delay<V: Into<Duration>>(mut self, v: V) -> Self {
78        self.initial_delay = v.into();
79        self
80    }
81
82    /// Change the maximum delay.
83    pub fn with_maximum_delay<V: Into<Duration>>(mut self, v: V) -> Self {
84        self.maximum_delay = v.into();
85        self
86    }
87
88    /// Change the scaling factor in this backoff policy.
89    pub fn with_scaling<V: Into<f64>>(mut self, v: V) -> Self {
90        self.scaling = v.into();
91        self
92    }
93
94    /// Creates a new exponential backoff policy.
95    ///
96    /// # Example
97    /// ```
98    /// # use google_cloud_gax::exponential_backoff::Error;
99    /// # use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
100    /// # use google_cloud_gax::backoff_policy::BackoffPolicy;
101    /// # use google_cloud_gax::retry_state::RetryState;
102    /// use std::time::Duration;
103    /// let backoff = ExponentialBackoffBuilder::new()
104    ///     .with_initial_delay(Duration::from_secs(5))
105    ///     .with_maximum_delay(Duration::from_secs(50))
106    ///     .with_scaling(2.0)
107    ///     .build()?;
108    /// let p = backoff.on_failure(&RetryState::new(true));
109    /// assert!(p <= Duration::from_secs(5));
110    /// let p = backoff.on_failure(&RetryState::new(true).set_attempt_count(2_u32));
111    /// assert!(p <= Duration::from_secs(10));
112    /// # Ok::<(), Error>(())
113    /// ```
114    pub fn build(self) -> Result<ExponentialBackoff, Error> {
115        if self.scaling < 1.0 {
116            return Err(Error::InvalidScalingFactor(self.scaling));
117        }
118        if self.initial_delay.is_zero() {
119            return Err(Error::InvalidInitialDelay(self.initial_delay));
120        }
121        if self.maximum_delay < self.initial_delay {
122            return Err(Error::EmptyRange {
123                maximum: self.maximum_delay,
124                initial: self.initial_delay,
125            });
126        }
127        Ok(ExponentialBackoff {
128            maximum_delay: self.maximum_delay,
129            scaling: self.scaling,
130            initial_delay: self.initial_delay,
131        })
132    }
133
134    /// Creates a new exponential backoff policy clamping the ranges towards
135    /// recommended values.
136    ///
137    /// The maximum delay is clamped first, to be between one second and one day
138    /// (both inclusive). The upper value is hardly useful, typically the retry
139    /// policy would expire earlier than such a long backoff. The exceptions may
140    /// tests and very long running operations.
141    ///
142    /// Then the initial delay is clamped to be between one millisecond and the
143    /// maximum delay. One millisecond is rarely useful outside of tests, but it
144    /// is unlikely to cause problems.
145    ///
146    /// Finally, the scaling factor is clamped to the `[1.0, 32.0]` range.
147    /// Neither extreme is very useful, but neither are necessarily going to
148    /// cause trouble.
149    ///
150    /// # Example
151    /// ```
152    /// # use google_cloud_gax::*;
153    /// # use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
154    /// # use google_cloud_gax::backoff_policy::BackoffPolicy;
155    /// # use google_cloud_gax::retry_state::RetryState;
156    /// use std::time::Duration;
157    /// let mut backoff = ExponentialBackoffBuilder::new().clamp();
158    /// assert!(backoff.on_failure(&RetryState::new(true)) > Duration::ZERO);
159    /// ```
160    pub fn clamp(self) -> ExponentialBackoff {
161        let scaling = self.scaling.clamp(1.0, 32.0);
162        let maximum_delay = self
163            .maximum_delay
164            .clamp(Duration::from_secs(1), Duration::from_secs(24 * 60 * 60));
165        let current_delay = self
166            .initial_delay
167            .clamp(Duration::from_millis(1), maximum_delay);
168        ExponentialBackoff {
169            initial_delay: current_delay,
170            maximum_delay,
171            scaling,
172        }
173    }
174}
175
176impl Default for ExponentialBackoffBuilder {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Implements truncated exponential backoff.
183#[derive(Debug)]
184pub struct ExponentialBackoff {
185    initial_delay: Duration,
186    maximum_delay: Duration,
187    scaling: f64,
188}
189
190impl ExponentialBackoff {
191    fn delay(&self, _loop_start: std::time::Instant, attempt_count: u32) -> Duration {
192        let exp = std::cmp::min(i32::MAX as u32, attempt_count) as i32;
193        let exp = exp.saturating_sub(1);
194        let scaling = self.scaling.powi(exp);
195        if scaling >= self.maximum_delay.div_duration_f64(self.initial_delay) {
196            self.maximum_delay
197        } else {
198            // .mul_f64() cannot assert because (1) we guarantee scaling >= 1.0,
199            // and (2) we just checked that
200            //     self.initial_delay * scaling < maximum_delay.
201            self.initial_delay.mul_f64(scaling)
202        }
203    }
204
205    fn delay_with_jitter(
206        &self,
207        state: &RetryState,
208        rng: &mut impl rand::Rng,
209    ) -> std::time::Duration {
210        let delay = self.delay(state.start, state.attempt_count);
211        rng.random_range(Duration::ZERO..=delay)
212    }
213}
214
215impl Default for ExponentialBackoff {
216    fn default() -> Self {
217        Self {
218            initial_delay: Duration::from_secs(1),
219            maximum_delay: Duration::from_secs(60),
220            scaling: 2.0,
221        }
222    }
223}
224
225impl crate::polling_backoff_policy::PollingBackoffPolicy for ExponentialBackoff {
226    fn wait_period(&self, state: &PollingState) -> std::time::Duration {
227        self.delay(state.start, state.attempt_count)
228    }
229}
230
231impl crate::backoff_policy::BackoffPolicy for ExponentialBackoff {
232    fn on_failure(&self, state: &RetryState) -> std::time::Duration {
233        self.delay_with_jitter(state, &mut rand::rng())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::mock_rng::MockRng;
241
242    #[test]
243    fn exponential_build_errors() {
244        let b = ExponentialBackoffBuilder::new()
245            .with_initial_delay(Duration::ZERO)
246            .with_maximum_delay(Duration::from_secs(5))
247            .build();
248        assert!(matches!(b, Err(Error::InvalidInitialDelay(_))), "{b:?}");
249        let b = ExponentialBackoffBuilder::new()
250            .with_initial_delay(Duration::from_secs(10))
251            .with_maximum_delay(Duration::from_secs(5))
252            .build();
253        assert!(matches!(b, Err(Error::EmptyRange { .. })), "{b:?}");
254
255        let b = ExponentialBackoffBuilder::new()
256            .with_initial_delay(Duration::from_secs(1))
257            .with_maximum_delay(Duration::from_secs(60))
258            .with_scaling(-1.0)
259            .build();
260        assert!(
261            matches!(b, Err(Error::InvalidScalingFactor { .. })),
262            "{b:?}"
263        );
264
265        let b = ExponentialBackoffBuilder::new()
266            .with_initial_delay(Duration::from_secs(1))
267            .with_maximum_delay(Duration::from_secs(60))
268            .with_scaling(0.0)
269            .build();
270        assert!(
271            matches!(b, Err(Error::InvalidScalingFactor { .. })),
272            "{b:?}"
273        );
274
275        let b = ExponentialBackoffBuilder::new()
276            .with_initial_delay(Duration::ZERO)
277            .build();
278        assert!(matches!(b, Err(Error::InvalidInitialDelay { .. })), "{b:?}");
279    }
280
281    #[test]
282    fn exponential_build_limits() {
283        let r = ExponentialBackoffBuilder::new()
284            .with_initial_delay(Duration::from_secs(1))
285            .with_maximum_delay(Duration::MAX)
286            .build();
287        assert!(r.is_ok(), "{r:?}");
288
289        let r = ExponentialBackoffBuilder::new()
290            .with_initial_delay(Duration::from_nanos(1))
291            .with_maximum_delay(Duration::MAX)
292            .build();
293        assert!(r.is_ok(), "{r:?}");
294
295        let r = ExponentialBackoffBuilder::new()
296            .with_initial_delay(Duration::from_nanos(1))
297            .with_maximum_delay(Duration::MAX)
298            .with_scaling(1.0)
299            .build();
300        assert!(r.is_ok(), "{r:?}");
301    }
302
303    #[test]
304    fn exponential_builder_defaults() {
305        let r = ExponentialBackoffBuilder::new().build();
306        assert!(r.is_ok(), "{r:?}");
307        let r = ExponentialBackoffBuilder::default().build();
308        assert!(r.is_ok(), "{r:?}");
309    }
310
311    #[test_case::test_case(Duration::from_secs(1), Duration::MAX, 0.5; "scaling below range")]
312    #[test_case::test_case(Duration::from_secs(1), Duration::MAX, 1_000_000.0; "scaling over range"
313	)]
314    #[test_case::test_case(Duration::from_secs(1), Duration::MAX, 8.0; "max over range")]
315    #[test_case::test_case(Duration::from_secs(1), Duration::ZERO, 8.0; "max below range")]
316    #[test_case::test_case(Duration::from_secs(10), Duration::ZERO, 8.0; "init over range")]
317    #[test_case::test_case(Duration::ZERO, Duration::ZERO, 8.0; "init below range")]
318    fn exponential_clamp(init: Duration, max: Duration, scaling: f64) {
319        let b = ExponentialBackoffBuilder::new()
320            .with_initial_delay(init)
321            .with_maximum_delay(max)
322            .with_scaling(scaling)
323            .clamp();
324        assert_eq!(b.scaling.clamp(1.0, 32.0), b.scaling);
325        assert_eq!(
326            b.initial_delay
327                .clamp(Duration::from_millis(1), b.maximum_delay),
328            b.initial_delay
329        );
330        assert_eq!(
331            b.maximum_delay
332                .clamp(b.initial_delay, Duration::from_secs(24 * 60 * 60)),
333            b.maximum_delay
334        );
335    }
336
337    #[test]
338    fn exponential_full_jitter() {
339        let b = ExponentialBackoffBuilder::new()
340            .with_initial_delay(Duration::from_secs(10))
341            .with_maximum_delay(Duration::from_secs(10))
342            .build()
343            .expect("should succeed with the hard-coded test values");
344
345        let mut rng = MockRng::new(1);
346        assert_eq!(
347            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(1_u32), &mut rng),
348            Duration::ZERO
349        );
350
351        let mut rng = MockRng::new(u64::MAX / 2);
352        assert_eq!(
353            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(2_u32), &mut rng),
354            Duration::from_secs(5)
355        );
356
357        let mut rng = MockRng::new(u64::MAX);
358        assert_eq!(
359            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(3_u32), &mut rng),
360            Duration::from_secs(10)
361        );
362    }
363
364    #[test]
365    fn exponential_scaling() {
366        let b = ExponentialBackoffBuilder::new()
367            .with_initial_delay(Duration::from_secs(1))
368            .with_maximum_delay(Duration::from_secs(4))
369            .with_scaling(2.0)
370            .build()
371            .expect("should succeed with the hard-coded test values");
372
373        let now = std::time::Instant::now();
374        assert_eq!(b.delay(now, 1), Duration::from_secs(1));
375        assert_eq!(b.delay(now, 2), Duration::from_secs(2));
376        assert_eq!(b.delay(now, 3), Duration::from_secs(4));
377        assert_eq!(b.delay(now, 4), Duration::from_secs(4));
378    }
379
380    #[test]
381    fn wait_period() {
382        use crate::polling_backoff_policy::PollingBackoffPolicy;
383        let b = ExponentialBackoffBuilder::new()
384            .with_initial_delay(Duration::from_secs(1))
385            .with_maximum_delay(Duration::from_secs(4))
386            .with_scaling(2.0)
387            .build()
388            .expect("should succeed with the hard-coded test values");
389
390        assert_eq!(
391            b.wait_period(&PollingState::default().set_attempt_count(1_u32)),
392            Duration::from_secs(1)
393        );
394        assert_eq!(
395            b.wait_period(&PollingState::default().set_attempt_count(2_u32)),
396            Duration::from_secs(2)
397        );
398        assert_eq!(
399            b.wait_period(&PollingState::default().set_attempt_count(3_u32)),
400            Duration::from_secs(4)
401        );
402        assert_eq!(
403            b.wait_period(&PollingState::default().set_attempt_count(4_u32)),
404            Duration::from_secs(4)
405        );
406    }
407
408    #[test]
409    fn exponential_scaling_jitter() {
410        let b = ExponentialBackoffBuilder::new()
411            .with_initial_delay(Duration::from_secs(1))
412            .with_maximum_delay(Duration::from_secs(4))
413            .with_scaling(2.0)
414            .build()
415            .expect("should succeed with the hard-coded test values");
416
417        let mut rng = MockRng::new(u64::MAX);
418        assert_eq!(
419            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(1_u32), &mut rng),
420            Duration::from_secs(1)
421        );
422
423        let mut rng = MockRng::new(u64::MAX);
424        assert_eq!(
425            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(2_u32), &mut rng),
426            Duration::from_secs(2)
427        );
428
429        let mut rng = MockRng::new(u64::MAX);
430        assert_eq!(
431            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(3_u32), &mut rng),
432            Duration::from_secs(4)
433        );
434
435        let mut rng = MockRng::new(u64::MAX);
436        assert_eq!(
437            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(4_u32), &mut rng),
438            Duration::from_secs(4)
439        );
440    }
441
442    #[test]
443    fn on_failure() {
444        use crate::backoff_policy::BackoffPolicy;
445        let b = ExponentialBackoffBuilder::new()
446            .with_initial_delay(Duration::from_secs(1))
447            .with_maximum_delay(Duration::from_secs(4))
448            .with_scaling(2.0)
449            .build()
450            .expect("should succeed with the hard-coded test values");
451
452        let d = b.on_failure(&RetryState::new(true).set_attempt_count(1_u32));
453        assert!(Duration::ZERO <= d && d <= Duration::from_secs(1), "{d:?}");
454        let d = b.on_failure(&RetryState::new(true).set_attempt_count(2_u32));
455        assert!(Duration::ZERO <= d && d <= Duration::from_secs(2), "{d:?}");
456        let d = b.on_failure(&RetryState::new(true).set_attempt_count(3_u32));
457        assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
458        let d = b.on_failure(&RetryState::new(true).set_attempt_count(4_u32));
459        assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
460        let d = b.on_failure(&RetryState::new(true).set_attempt_count(5_u32));
461        assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
462    }
463
464    #[test]
465    fn default() {
466        let b = ExponentialBackoff::default();
467
468        let mut rng = MockRng::new(u64::MAX);
469        let next =
470            2 * b.delay_with_jitter(&RetryState::new(true).set_attempt_count(1_u32), &mut rng);
471
472        let mut rng = MockRng::new(u64::MAX);
473        assert_eq!(
474            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(2_u32), &mut rng),
475            next
476        );
477        let next = 2 * next;
478
479        let mut rng = MockRng::new(u64::MAX);
480        assert_eq!(
481            b.delay_with_jitter(&RetryState::new(true).set_attempt_count(3_u32), &mut rng),
482            next
483        );
484    }
485}