object_store/client/
backoff.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use rand::prelude::*;
19use std::time::Duration;
20
21/// Exponential backoff with jitter
22///
23/// See <https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/>
24#[allow(missing_copy_implementations)]
25#[derive(Debug, Clone)]
26pub struct BackoffConfig {
27    /// The initial backoff duration
28    pub init_backoff: Duration,
29    /// The maximum backoff duration
30    pub max_backoff: Duration,
31    /// The base of the exponential to use
32    pub base: f64,
33}
34
35impl Default for BackoffConfig {
36    fn default() -> Self {
37        Self {
38            init_backoff: Duration::from_millis(100),
39            max_backoff: Duration::from_secs(15),
40            base: 2.,
41        }
42    }
43}
44
45/// [`Backoff`] can be created from a [`BackoffConfig`]
46///
47/// Consecutive calls to [`Backoff::next`] will return the next backoff interval
48///
49pub struct Backoff {
50    init_backoff: f64,
51    next_backoff_secs: f64,
52    max_backoff_secs: f64,
53    base: f64,
54    rng: Option<Box<dyn RngCore + Sync + Send>>,
55}
56
57impl std::fmt::Debug for Backoff {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct("Backoff")
60            .field("init_backoff", &self.init_backoff)
61            .field("next_backoff_secs", &self.next_backoff_secs)
62            .field("max_backoff_secs", &self.max_backoff_secs)
63            .field("base", &self.base)
64            .finish()
65    }
66}
67
68impl Backoff {
69    /// Create a new [`Backoff`] from the provided [`BackoffConfig`]
70    pub fn new(config: &BackoffConfig) -> Self {
71        Self::new_with_rng(config, None)
72    }
73
74    /// Creates a new `Backoff` with the optional `rng`
75    ///
76    /// Used [`rand::thread_rng()`] if no rng provided
77    pub fn new_with_rng(
78        config: &BackoffConfig,
79        rng: Option<Box<dyn RngCore + Sync + Send>>,
80    ) -> Self {
81        let init_backoff = config.init_backoff.as_secs_f64();
82        Self {
83            init_backoff,
84            next_backoff_secs: init_backoff,
85            max_backoff_secs: config.max_backoff.as_secs_f64(),
86            base: config.base,
87            rng,
88        }
89    }
90
91    /// Returns the next backoff duration to wait for
92    pub fn next(&mut self) -> Duration {
93        let range = self.init_backoff..(self.next_backoff_secs * self.base);
94
95        let rand_backoff = match self.rng.as_mut() {
96            Some(rng) => rng.gen_range(range),
97            None => thread_rng().gen_range(range),
98        };
99
100        let next_backoff = self.max_backoff_secs.min(rand_backoff);
101        Duration::from_secs_f64(std::mem::replace(&mut self.next_backoff_secs, next_backoff))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use rand::rngs::mock::StepRng;
109
110    #[test]
111    fn test_backoff() {
112        let init_backoff_secs = 1.;
113        let max_backoff_secs = 500.;
114        let base = 3.;
115
116        let config = BackoffConfig {
117            init_backoff: Duration::from_secs_f64(init_backoff_secs),
118            max_backoff: Duration::from_secs_f64(max_backoff_secs),
119            base,
120        };
121
122        let assert_fuzzy_eq = |a: f64, b: f64| assert!((b - a).abs() < 0.0001, "{a} != {b}");
123
124        // Create a static rng that takes the minimum of the range
125        let rng = Box::new(StepRng::new(0, 0));
126        let mut backoff = Backoff::new_with_rng(&config, Some(rng));
127
128        for _ in 0..20 {
129            assert_eq!(backoff.next().as_secs_f64(), init_backoff_secs);
130        }
131
132        // Create a static rng that takes the maximum of the range
133        let rng = Box::new(StepRng::new(u64::MAX, 0));
134        let mut backoff = Backoff::new_with_rng(&config, Some(rng));
135
136        for i in 0..20 {
137            let value = (base.powi(i) * init_backoff_secs).min(max_backoff_secs);
138            assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
139        }
140
141        // Create a static rng that takes the mid point of the range
142        let rng = Box::new(StepRng::new(u64::MAX / 2, 0));
143        let mut backoff = Backoff::new_with_rng(&config, Some(rng));
144
145        let mut value = init_backoff_secs;
146        for _ in 0..20 {
147            assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
148            value =
149                (init_backoff_secs + (value * base - init_backoff_secs) / 2.).min(max_backoff_secs);
150        }
151    }
152}