Skip to main content

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