maybe_backoff/
exponential.rs

1use std::marker::PhantomData;
2use std::time::Duration;
3#[cfg(not(target_family = "wasm"))]
4use std::time::Instant;
5#[cfg(target_family = "wasm")]
6use web_time::Instant;
7
8use crate::backoff::Backoff;
9use crate::clock::Clock;
10use crate::default;
11
12#[derive(Debug)]
13pub struct ExponentialBackoff<C> {
14    /// The current retry interval.
15    pub current_interval: Duration,
16    /// The initial retry interval.
17    pub initial_interval: Duration,
18    /// The randomization factor to use for creating a range around the retry interval.
19    ///
20    /// A randomization factor of 0.5 results in a random period ranging between 50% below and 50%
21    /// above the retry interval.
22    pub randomization_factor: f64,
23    /// The value to multiply the current interval with for each retry attempt.
24    pub multiplier: f64,
25    /// The maximum value of the back off period. Once the retry interval reaches this
26    /// value it stops increasing.
27    pub max_interval: Duration,
28    /// The system time. It is calculated when an [`ExponentialBackoff`](struct.ExponentialBackoff.html) instance is
29    /// created and is reset when [`retry`](../trait.Operation.html#method.retry) is called.
30    pub start_time: Instant,
31    /// The maximum elapsed time after instantiating [`ExponentialBackfff`](struct.ExponentialBackoff.html) or calling
32    /// [`reset`](trait.Backoff.html#method.reset) after which [`next_backoff`](../trait.Backoff.html#method.reset) returns `None`.
33    pub max_elapsed_time: Option<Duration>,
34    /// The clock used to get the current time.
35    pub clock: C,
36}
37
38impl<C> Default for ExponentialBackoff<C>
39where
40    C: Clock + Default,
41{
42    fn default() -> ExponentialBackoff<C> {
43        let mut eb = ExponentialBackoff {
44            current_interval: Duration::from_millis(default::INITIAL_INTERVAL_MILLIS),
45            initial_interval: Duration::from_millis(default::INITIAL_INTERVAL_MILLIS),
46            randomization_factor: default::RANDOMIZATION_FACTOR,
47            multiplier: default::MULTIPLIER,
48            max_interval: Duration::from_millis(default::MAX_INTERVAL_MILLIS),
49            max_elapsed_time: Some(Duration::from_millis(default::MAX_ELAPSED_TIME_MILLIS)),
50            clock: C::default(),
51            start_time: Instant::now(),
52        };
53        eb.reset();
54        eb
55    }
56}
57
58impl<C: Clock> ExponentialBackoff<C> {
59    /// Returns the elapsed time since start_time.
60    pub fn get_elapsed_time(&self) -> Duration {
61        self.clock.now().duration_since(self.start_time)
62    }
63
64    fn get_random_value_from_interval(
65        randomization_factor: f64,
66        random: f64,
67        current_interval: Duration,
68    ) -> Duration {
69        let current_interval_nanos = duration_to_nanos(current_interval);
70
71        let delta = randomization_factor * current_interval_nanos;
72        let min_interval = current_interval_nanos - delta;
73        let max_interval = current_interval_nanos + delta;
74        // Get a random value from the range [minInterval, maxInterval].
75        // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
76        // we want a 33% chance for selecting either 1, 2 or 3.
77        let diff = max_interval - min_interval;
78        let nanos = min_interval + (random * (diff + 1.0));
79        nanos_to_duration(nanos)
80    }
81
82    fn increment_current_interval(&mut self) -> Duration {
83        let current_interval_nanos = duration_to_nanos(self.current_interval);
84        let max_interval_nanos = duration_to_nanos(self.max_interval);
85        // Check for overflow, if overflow is detected set the current interval to the max interval.
86        if current_interval_nanos >= max_interval_nanos / self.multiplier {
87            self.max_interval
88        } else {
89            let nanos = current_interval_nanos * self.multiplier;
90            nanos_to_duration(nanos)
91        }
92    }
93}
94
95fn duration_to_nanos(d: Duration) -> f64 {
96    d.as_secs() as f64 * 1_000_000_000.0 + f64::from(d.subsec_nanos())
97}
98
99fn nanos_to_duration(nanos: f64) -> Duration {
100    let secs = nanos / 1_000_000_000.0;
101    let nanos = nanos as u64 % 1_000_000_000;
102    Duration::new(secs as u64, nanos as u32)
103}
104
105impl<C> Backoff for ExponentialBackoff<C>
106where
107    C: Clock,
108{
109    fn reset(&mut self) {
110        self.current_interval = self.initial_interval;
111        self.start_time = self.clock.now();
112    }
113
114    fn next_backoff(&mut self) -> Option<Duration> {
115        let elapsed_time = self.get_elapsed_time();
116
117        match self.max_elapsed_time {
118            Some(v) if elapsed_time > v => None,
119            _ => {
120                let random = rand::random::<f64>();
121                let randomized_interval = Self::get_random_value_from_interval(
122                    self.randomization_factor,
123                    random,
124                    self.current_interval,
125                );
126                self.current_interval = self.increment_current_interval();
127
128                if let Some(max_elapsed_time) = self.max_elapsed_time {
129                    if elapsed_time + randomized_interval <= max_elapsed_time {
130                        Some(randomized_interval)
131                    } else {
132                        None
133                    }
134                } else {
135                    Some(randomized_interval)
136                }
137            }
138        }
139    }
140}
141
142impl<C> Clone for ExponentialBackoff<C>
143where
144    C: Clone,
145{
146    fn clone(&self) -> Self {
147        let clock = self.clock.clone();
148        ExponentialBackoff { clock, ..*self }
149    }
150}
151
152/// Builder for [`ExponentialBackoff`](type.ExponentialBackoff.html).
153///
154/// TODO: Example
155#[derive(Debug)]
156pub struct ExponentialBackoffBuilder<C> {
157    initial_interval: Duration,
158    randomization_factor: f64,
159    multiplier: f64,
160    max_interval: Duration,
161    max_elapsed_time: Option<Duration>,
162    _clock: PhantomData<C>,
163}
164
165impl<C> Default for ExponentialBackoffBuilder<C> {
166    fn default() -> Self {
167        Self {
168            initial_interval: Duration::from_millis(default::INITIAL_INTERVAL_MILLIS),
169            randomization_factor: default::RANDOMIZATION_FACTOR,
170            multiplier: default::MULTIPLIER,
171            max_interval: Duration::from_millis(default::MAX_INTERVAL_MILLIS),
172            max_elapsed_time: Some(Duration::from_millis(default::MAX_ELAPSED_TIME_MILLIS)),
173            _clock: PhantomData,
174        }
175    }
176}
177
178impl<C> ExponentialBackoffBuilder<C>
179where
180    C: Clock + Default,
181{
182    pub fn new() -> Self {
183        Default::default()
184    }
185
186    /// The initial retry interval.
187    pub fn with_initial_interval(&mut self, initial_interval: Duration) -> &mut Self {
188        self.initial_interval = initial_interval;
189        self
190    }
191
192    /// The randomization factor to use for creating a range around the retry interval.
193    ///
194    /// A randomization factor of 0.5 results in a random period ranging between 50% below and 50%
195    /// above the retry interval.
196    pub fn with_randomization_factor(&mut self, randomization_factor: f64) -> &mut Self {
197        self.randomization_factor = randomization_factor;
198        self
199    }
200
201    /// The value to multiply the current interval with for each retry attempt.
202    pub fn with_multiplier(&mut self, multiplier: f64) -> &mut Self {
203        self.multiplier = multiplier;
204        self
205    }
206
207    /// The maximum value of the back off period. Once the retry interval reaches this
208    /// value it stops increasing.
209    pub fn with_max_interval(&mut self, max_interval: Duration) -> &mut Self {
210        self.max_interval = max_interval;
211        self
212    }
213
214    /// The maximum elapsed time after instantiating [`ExponentialBackfff`](struct.ExponentialBackoff.html) or calling
215    /// [`reset`](trait.Backoff.html#method.reset) after which [`next_backoff`](../trait.Backoff.html#method.reset) returns `None`.
216    pub fn with_max_elapsed_time(&mut self, max_elapsed_time: Option<Duration>) -> &mut Self {
217        self.max_elapsed_time = max_elapsed_time;
218        self
219    }
220
221    pub fn build(&self) -> ExponentialBackoff<C> {
222        ExponentialBackoff {
223            current_interval: self.initial_interval,
224            initial_interval: self.initial_interval,
225            randomization_factor: self.randomization_factor,
226            multiplier: self.multiplier,
227            max_interval: self.max_interval,
228            max_elapsed_time: self.max_elapsed_time,
229            clock: C::default(),
230            start_time: Instant::now(),
231        }
232    }
233}
234
235#[cfg(test)]
236use crate::clock::SystemClock;
237
238#[test]
239fn get_randomized_interval() {
240    // 33% chance of being 1.
241    let f = ExponentialBackoff::<SystemClock>::get_random_value_from_interval;
242    assert_eq!(Duration::new(0, 1), f(0.5, 0.0, Duration::new(0, 2)));
243    assert_eq!(Duration::new(0, 1), f(0.5, 0.33, Duration::new(0, 2)));
244    // 33% chance of being 2.
245    assert_eq!(Duration::new(0, 2), f(0.5, 0.34, Duration::new(0, 2)));
246    assert_eq!(Duration::new(0, 2), f(0.5, 0.66, Duration::new(0, 2)));
247    // 33% chance of being 3.
248    assert_eq!(Duration::new(0, 3), f(0.5, 0.67, Duration::new(0, 2)));
249    assert_eq!(Duration::new(0, 3), f(0.5, 0.99, Duration::new(0, 2)));
250}
251
252#[test]
253fn exponential_backoff_builder() {
254    let initial_interval = Duration::from_secs(1);
255    let max_interval = Duration::from_secs(2);
256    let multiplier = 3.0;
257    let randomization_factor = 4.0;
258    let backoff: ExponentialBackoff<SystemClock> = ExponentialBackoffBuilder::new()
259        .with_initial_interval(initial_interval)
260        .with_multiplier(multiplier)
261        .with_randomization_factor(randomization_factor)
262        .with_max_interval(max_interval)
263        .with_max_elapsed_time(None)
264        .build();
265    assert_eq!(backoff.initial_interval, initial_interval);
266    assert_eq!(backoff.current_interval, initial_interval);
267    assert_eq!(backoff.multiplier, multiplier);
268    assert_eq!(backoff.randomization_factor, randomization_factor);
269    assert_eq!(backoff.max_interval, max_interval);
270    assert_eq!(backoff.max_elapsed_time, None);
271}
272
273#[test]
274fn exponential_backoff_default_builder() {
275    let backoff: ExponentialBackoff<SystemClock> = ExponentialBackoffBuilder::new().build();
276    assert_eq!(
277        backoff.initial_interval,
278        Duration::from_millis(default::INITIAL_INTERVAL_MILLIS)
279    );
280    assert_eq!(
281        backoff.current_interval,
282        Duration::from_millis(default::INITIAL_INTERVAL_MILLIS)
283    );
284    assert_eq!(backoff.multiplier, default::MULTIPLIER);
285    assert_eq!(backoff.randomization_factor, default::RANDOMIZATION_FACTOR);
286    assert_eq!(
287        backoff.max_interval,
288        Duration::from_millis(default::MAX_INTERVAL_MILLIS)
289    );
290    assert_eq!(
291        backoff.max_elapsed_time,
292        Some(Duration::from_millis(default::MAX_ELAPSED_TIME_MILLIS))
293    );
294}