| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use rand::{Rng, RngExt, rng}; |
| use std::time::Duration; |
| |
| /// Exponential backoff with decorrelated jitter algorithm |
| /// |
| /// The first backoff will always be `init_backoff`. |
| /// |
| /// Subsequent backoffs will pick a random value between `init_backoff` and |
| /// `base * previous` where `previous` is the duration of the previous backoff |
| /// |
| /// See <https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/> |
| #[allow(missing_copy_implementations)] |
| #[derive(Debug, Clone)] |
| pub struct BackoffConfig { |
| /// The initial backoff duration |
| pub init_backoff: Duration, |
| /// The maximum backoff duration |
| pub max_backoff: Duration, |
| /// The multiplier to use for the next backoff duration |
| pub base: f64, |
| } |
| |
| impl Default for BackoffConfig { |
| fn default() -> Self { |
| Self { |
| init_backoff: Duration::from_millis(100), |
| max_backoff: Duration::from_secs(15), |
| base: 2., |
| } |
| } |
| } |
| |
| /// [`Backoff`] can be created from a [`BackoffConfig`] |
| /// |
| /// Consecutive calls to [`Backoff::next`] will return the next backoff interval |
| /// |
| pub(crate) struct Backoff { |
| init_backoff: f64, |
| next_backoff_secs: f64, |
| max_backoff_secs: f64, |
| base: f64, |
| rng: Option<Box<dyn Rng + Sync + Send>>, |
| } |
| |
| impl std::fmt::Debug for Backoff { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("Backoff") |
| .field("init_backoff", &self.init_backoff) |
| .field("next_backoff_secs", &self.next_backoff_secs) |
| .field("max_backoff_secs", &self.max_backoff_secs) |
| .field("base", &self.base) |
| .finish() |
| } |
| } |
| |
| impl Backoff { |
| /// Create a new [`Backoff`] from the provided [`BackoffConfig`] |
| pub(crate) fn new(config: &BackoffConfig) -> Self { |
| Self::new_with_rng(config, None) |
| } |
| |
| /// Creates a new `Backoff` with the optional `rng` |
| /// |
| /// Used [`rand::rng()`] if no rng provided |
| pub(crate) fn new_with_rng( |
| config: &BackoffConfig, |
| rng: Option<Box<dyn Rng + Sync + Send>>, |
| ) -> Self { |
| let init_backoff = config.init_backoff.as_secs_f64(); |
| Self { |
| init_backoff, |
| next_backoff_secs: init_backoff, |
| max_backoff_secs: config.max_backoff.as_secs_f64(), |
| base: config.base, |
| rng, |
| } |
| } |
| |
| /// Returns the next backoff duration to wait for |
| pub(crate) fn next(&mut self) -> Duration { |
| let range = self.init_backoff..(self.next_backoff_secs * self.base); |
| |
| let rand_backoff = match self.rng.as_mut() { |
| Some(rng) => rng.random_range(range), |
| None => rng().random_range(range), |
| }; |
| |
| let next_backoff = self.max_backoff_secs.min(rand_backoff); |
| Duration::from_secs_f64(std::mem::replace(&mut self.next_backoff_secs, next_backoff)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use std::convert::Infallible; |
| |
| use rand::{TryRng, rand_core::utils::fill_bytes_via_next_word}; |
| |
| use super::*; |
| |
| struct FixedRng(u64); |
| |
| impl TryRng for FixedRng { |
| type Error = Infallible; |
| |
| fn try_next_u32(&mut self) -> Result<u32, Self::Error> { |
| Ok(self.0 as _) |
| } |
| |
| fn try_next_u64(&mut self) -> Result<u64, Self::Error> { |
| Ok(self.0) |
| } |
| |
| fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { |
| fill_bytes_via_next_word(dst, || self.try_next_u64()) |
| } |
| } |
| |
| #[test] |
| fn test_backoff() { |
| let init_backoff_secs = 1.; |
| let max_backoff_secs = 500.; |
| let base = 3.; |
| |
| let config = BackoffConfig { |
| init_backoff: Duration::from_secs_f64(init_backoff_secs), |
| max_backoff: Duration::from_secs_f64(max_backoff_secs), |
| base, |
| }; |
| |
| let assert_fuzzy_eq = |a: f64, b: f64| assert!((b - a).abs() < 0.0001, "{a} != {b}"); |
| |
| // Create a static rng that takes the minimum of the range |
| let rng = Box::new(FixedRng(0)); |
| let mut backoff = Backoff::new_with_rng(&config, Some(rng)); |
| |
| for _ in 0..20 { |
| assert_eq!(backoff.next().as_secs_f64(), init_backoff_secs); |
| } |
| |
| // Create a static rng that takes the maximum of the range |
| let rng = Box::new(FixedRng(u64::MAX)); |
| let mut backoff = Backoff::new_with_rng(&config, Some(rng)); |
| |
| for i in 0..20 { |
| let value = (base.powi(i) * init_backoff_secs).min(max_backoff_secs); |
| assert_fuzzy_eq(backoff.next().as_secs_f64(), value); |
| } |
| |
| // Create a static rng that takes the mid point of the range |
| let rng = Box::new(FixedRng(u64::MAX / 2)); |
| let mut backoff = Backoff::new_with_rng(&config, Some(rng)); |
| |
| let mut value = init_backoff_secs; |
| for _ in 0..20 { |
| assert_fuzzy_eq(backoff.next().as_secs_f64(), value); |
| value = |
| (init_backoff_secs + (value * base - init_backoff_secs) / 2.).min(max_backoff_secs); |
| } |
| } |
| } |