Skip to main content

headless_lms_utils/
backoff.rs

1//! Exponential backoff with jitter, and the "has this retry window run out" check that goes with
2//! it. Shared by every worker that reschedules a failed row instead of giving up on it outright.
3
4use rand::RngExt;
5
6use crate::prelude::*;
7
8/// `base_secs * 2^attempt`, capped at `max_secs`. `attempt` is clamped to keep the shift in range
9/// of `i64`.
10pub fn exponential_backoff_secs(base_secs: i64, max_secs: i64, attempt: i32) -> i64 {
11    let shift = attempt.clamp(0, 62) as u32;
12    base_secs
13        .saturating_mul(2_i64.saturating_pow(shift))
14        .min(max_secs)
15}
16
17/// `now + delay_secs`, plus a uniformly random `0..=jitter_max_secs` so a batch that failed
18/// together does not all come back at once.
19pub fn next_attempt_at(now: DateTime<Utc>, delay_secs: i64, jitter_max_secs: i64) -> DateTime<Utc> {
20    let jitter = rand::rng().random_range(0..=jitter_max_secs);
21    now + chrono::Duration::seconds(delay_secs.saturating_add(jitter))
22}
23
24/// Whether `max_age_secs` have passed since `reference`. `None` (never failed) is never expired.
25pub fn window_expired(
26    reference: Option<DateTime<Utc>>,
27    now: DateTime<Utc>,
28    max_age_secs: i64,
29) -> bool {
30    reference.is_some_and(|reference| (now - reference).num_seconds() >= max_age_secs)
31}