//! Exponential backoff with jitter. //! //! Three use cases (subagent, provider, workflow) all share the same formula //! with different caps. This module provides a single implementation. use std::convert::TryInto; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Compute an exponential backoff with ±25% jitter. /// /// `attempt` is 0-based (first retry -> attempt=0 -> base=1s, /// second retry -> attempt=1 -> base=2s, etc.). /// `max_secs` sets the cap. pub fn backoff_seconds(attempt: u32, max_secs: u64) -> Duration { let base_secs = (2u64).pow(attempt).min(max_secs); // 25% of base (in nanoseconds), floored at 100ms so very low // attempts still have meaningful jitter. let quarter = (base_secs * 250_000_000).max(100_000_000); let offset = jitter_ns(quarter * 2); // [0, 50% of base) // ±25%: offset in [0, 2×quarter), result = base + offset - quarter // which lies in [base - 25%, base + 25%). let ns = base_secs * 1_000_000_000 + offset - quarter; Duration::from_nanos(ns) } /// Return a jitter offset in the range [0, range_ns). /// /// Uses sub-nanosecond wall-clock bits as a cheap PRNG source — no /// need for a full RNG for ±25% backoff jitter. fn jitter_ns(range_ns: u64) -> u64 { let dur = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); let nanos: u64 = dur.as_nanos().try_into().unwrap_or(u64::MAX); nanos % range_ns }