deltalake-catalog-unity 0.16.0

Native Delta Lake implementation in Rust
Documentation
//!  Exponential backoff with jitter
use rand::prelude::*;
use std::time::Duration;

/// Exponential backoff with jitter
///
/// 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 base of the exponential to use
    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::tick`] will return the next backoff interval
pub 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 fn new(config: &BackoffConfig) -> Self {
        Self::new_with_rng(config, None)
    }

    /// Creates a new `Backoff` with the optional `rng`
    ///
    /// Used [`rand::random_range()`] if no rng provided
    pub 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 fn tick(&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 => rand::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 super::*;
    use rand::rngs::SmallRng;

    #[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 rng: SmallRng = rand::make_rng();
        let mut backoff = Backoff::new_with_rng(&config, Some(Box::new(rng)));

        for _ in 0..10 {
            let tick = backoff.tick().as_secs_f64();
            assert!(tick >= init_backoff_secs, "{tick} > {init_backoff_secs}");
            assert!(tick <= max_backoff_secs, "{tick} <= {max_backoff_secs}");
        }
    }
}