dag-executor 0.1.0

A production-ready DAG executor with state management and advanced patterns
Documentation
//! Retry strategies.

use rand::Rng;
use std::time::Duration;

/// How to space out retry attempts.
#[derive(Debug, Clone, Copy)]
pub enum Backoff {
    /// Wait the same fixed duration between every attempt.
    Fixed(Duration),
    /// Linear growth: `base * attempt`.
    Linear {
        /// Per-attempt increment (the delay before the first retry).
        base: Duration,
        /// Upper bound on any single delay.
        max: Duration,
    },
    /// Exponential growth: `base * factor^attempt`, capped at `max`.
    Exponential {
        /// Delay before the first retry.
        base: Duration,
        /// Growth multiplier per attempt.
        factor: f64,
        /// Upper bound on any single delay.
        max: Duration,
    },
}

impl Backoff {
    /// Compute the base delay (before jitter) preceding retry `attempt`
    /// (1-based: `attempt == 1` is the delay before the *first* retry).
    pub fn delay(&self, attempt: u32) -> Duration {
        let attempt = attempt.max(1);
        match *self {
            Backoff::Fixed(d) => d,
            Backoff::Linear { base, max } => base.checked_mul(attempt).unwrap_or(max).min(max),
            Backoff::Exponential { base, factor, max } => {
                let mult = factor.powi((attempt - 1) as i32);
                let secs = base.as_secs_f64() * mult;
                let capped = secs.min(max.as_secs_f64());
                Duration::from_secs_f64(capped.max(0.0))
            }
        }
    }
}

/// A complete retry policy: how many attempts, how to back off, and whether to
/// add jitter.
#[derive(Debug, Clone, Copy)]
pub struct RetryPolicy {
    /// Maximum total attempts (1 = no retries).
    pub max_attempts: u32,
    /// Backoff schedule between attempts.
    pub backoff: Backoff,
    /// Add up to ±25% randomization to each delay to avoid thundering herds.
    pub jitter: bool,
}

impl RetryPolicy {
    /// A policy that never retries.
    pub fn none() -> Self {
        RetryPolicy {
            max_attempts: 1,
            backoff: Backoff::Fixed(Duration::ZERO),
            jitter: false,
        }
    }

    /// `max_attempts` total tries with a fixed delay between them.
    pub fn fixed(max_attempts: u32, delay: Duration) -> Self {
        RetryPolicy {
            max_attempts,
            backoff: Backoff::Fixed(delay),
            jitter: false,
        }
    }

    /// Exponential backoff with jitter — a sensible production default.
    pub fn exponential(max_attempts: u32, base: Duration) -> Self {
        RetryPolicy {
            max_attempts,
            backoff: Backoff::Exponential {
                base,
                factor: 2.0,
                max: Duration::from_secs(60),
            },
            jitter: true,
        }
    }

    /// Whether another attempt is allowed after `attempts_made` have completed.
    pub fn should_retry(&self, attempts_made: u32) -> bool {
        attempts_made < self.max_attempts
    }

    /// The delay to wait before the retry following `attempts_made`, applying
    /// jitter if enabled.
    pub fn delay_for(&self, attempts_made: u32) -> Duration {
        let base = self.backoff.delay(attempts_made);
        if !self.jitter || base.is_zero() {
            return base;
        }
        let secs = base.as_secs_f64();
        let factor = rand::thread_rng().gen_range(0.75..=1.25);
        Duration::from_secs_f64(secs * factor)
    }
}

impl Default for RetryPolicy {
    fn default() -> Self {
        RetryPolicy::exponential(3, Duration::from_millis(100))
    }
}