cloud-lite-core-rs 0.1.1

Shared utilities for cloud-lite provider crates
Documentation
//! Retry and backoff configuration for HTTP requests.

use std::time::Duration;

/// Configuration for retry behavior
#[derive(Debug, Clone)]
pub struct RetryConfig {
    /// Maximum number of retry attempts
    pub max_retries: u32,

    /// Initial backoff duration
    pub initial_backoff: Duration,

    /// Maximum backoff duration
    pub max_backoff: Duration,

    /// Backoff multiplier (exponential backoff factor)
    pub backoff_multiplier: f64,

    /// Whether to add random jitter to backoff
    pub jitter: bool,

    /// Whether to retry on 401 (with token refresh)
    pub retry_on_401: bool,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: 3,
            initial_backoff: Duration::from_secs(1),
            max_backoff: Duration::from_secs(30),
            backoff_multiplier: 2.0,
            jitter: true,
            retry_on_401: true,
        }
    }
}

impl RetryConfig {
    /// Compute the next backoff duration.
    ///
    /// # Arguments
    ///
    /// * `current_backoff` - Current backoff duration
    /// * `retry_after` - Optional retry-after duration from server
    ///
    /// # Returns
    ///
    /// The duration to wait before the next retry
    pub fn compute_backoff(
        &self,
        current_backoff: Duration,
        retry_after: Option<Duration>,
    ) -> Duration {
        // If server specified retry-after, use that (don't cap it)
        if let Some(retry_after) = retry_after {
            return retry_after;
        }

        // Otherwise use calculated backoff
        let backoff = current_backoff.min(self.max_backoff);

        // Add jitter if enabled
        if self.jitter {
            self.add_jitter(backoff)
        } else {
            backoff
        }
    }

    fn add_jitter(&self, duration: Duration) -> Duration {
        use std::collections::hash_map::RandomState;
        use std::hash::BuildHasher;

        // Simple jitter: ±20% of duration
        let millis = duration.as_millis() as f64;
        let jitter_range = millis * 0.2;

        // Use RandomState for quick randomness
        let random =
            (RandomState::new().hash_one(std::time::SystemTime::now()) % 100) as f64 / 100.0;

        let jittered = millis + (jitter_range * (random * 2.0 - 1.0));
        Duration::from_millis(jittered.max(0.0) as u64)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn jitter_stays_within_bounds() {
        let config = RetryConfig::default();
        let base = Duration::from_secs(10);

        for _ in 0..100 {
            let jittered = config.add_jitter(base);
            let millis = jittered.as_millis();

            // Should be within ±20% of base (8s - 12s)
            assert!(millis >= 8000);
            assert!(millis <= 12000);
        }
    }
}