use rand::Rng; pub fn compute_next_attempt_at(now: i64, attempt_count: i64) -> i64 { let capped_attempts = attempt_count.min(30) as u32; let base_delay_ms = 1000_i64.saturating_mul(1 << capped_attempts); let capped_delay_ms = base_delay_ms.min(3_600_000); let jitter_factor = rand::thread_rng().gen_range(0.9..=1.1); let delay_with_jitter = (capped_delay_ms as f64 * jitter_factor) as i64; now.saturating_add(delay_with_jitter) } #[cfg(test)] mod tests { use super::*; const MAX_DELAY_MS: i64 = 3_600_000; #[test] fn test_exponential_curve() { let now = 1_000_000_000_i64; for attempt in 1..=10 { let result = compute_next_attempt_at(now, attempt); let delay = result - now; let expected_base = 1000_i64 * (1 << attempt); let min_expected = (expected_base as f64 * 0.89) as i64; let max_expected = (expected_base as f64 * 1.11) as i64; assert!( delay >= min_expected && delay <= max_expected, "attempt {attempt}: delay {delay} not in [{min_expected}, {max_expected}]" ); } } #[test] fn test_cap_at_one_hour() { let now = 1_000_000_000_i64; for attempt in [20, 25, 30, 50, 100] { let result = compute_next_attempt_at(now, attempt); let delay = result - now; let max_with_jitter = (MAX_DELAY_MS as f64 * 1.11) as i64; assert!( delay <= max_with_jitter, "attempt {attempt}: delay {delay} exceeds cap {max_with_jitter}" ); } } #[test] fn test_jitter_range() { let now = 1_000_000_000_i64; let attempt = 5; let base = 1000_i64 * (1 << attempt); let min_delay = (base as f64 * 0.89) as i64; let max_delay = (base as f64 * 1.11) as i64; for _ in 0..100 { let result = compute_next_attempt_at(now, attempt); let delay = result - now; assert!( delay >= min_delay && delay <= max_delay, "delay {delay} not in jitter range [{min_delay}, {max_delay}]" ); } } #[test] fn test_first_retry_is_about_two_seconds() { let now = 1_000_000_000_i64; let result = compute_next_attempt_at(now, 1); let delay = result - now; assert!( (1800..=2200).contains(&delay), "first retry delay: {delay}ms" ); } #[test] fn test_overflow_safety() { let now = i64::MAX / 2; let result = compute_next_attempt_at(now, i64::MAX); assert!(result > now); } #[test] fn test_saturating_add_prevents_overflow() { let now = i64::MAX - 10; let result = compute_next_attempt_at(now, 30); assert_eq!(result, i64::MAX); } }