Skip to main content

aster_forge_utils/
backoff.rs

1//! Shared exponential backoff calculations.
2//!
3//! This module owns only mechanical delay arithmetic. Callers retain the
4//! policy for retryability, cancellation, reset conditions, and observability.
5
6use std::time::Duration;
7
8/// Calculates an uncapped exponential delay for a zero-based retry index.
9pub fn exponential_delay(initial_delay: Duration, retry_index: u32) -> Duration {
10    let mut delay = initial_delay;
11    let mut remaining = retry_index.min(127);
12    while remaining > 0 && delay < Duration::MAX {
13        delay = delay.saturating_add(delay);
14        remaining -= 1;
15    }
16    delay
17}
18
19/// Applies a hard upper bound to a delay.
20pub fn cap_delay(delay: Duration, max_delay: Duration) -> Duration {
21    delay.min(max_delay)
22}
23
24/// Applies an explicit percentage to a delay, saturating at `Duration::MAX`.
25pub fn apply_jitter(delay: Duration, percent: u16) -> Duration {
26    let nanos = delay.as_nanos().saturating_mul(u128::from(percent));
27    let nanos = nanos.checked_div(100).unwrap_or(0);
28    duration_from_nanos(nanos)
29}
30
31/// Applies a sampled percentage using the default thread-local random generator.
32///
33/// The bounds are inclusive and clamped to a practical percentage range. Callers that need
34/// deterministic behavior should use [`apply_jitter`] instead.
35pub fn randomized_jitter(delay: Duration, min_percent: u16, max_percent: u16) -> Duration {
36    use rand::RngExt;
37
38    let min_percent = min_percent.min(1000);
39    let max_percent = max_percent.min(1000).max(min_percent);
40    apply_jitter(delay, rand::rng().random_range(min_percent..=max_percent))
41}
42
43fn duration_from_nanos(nanos: u128) -> Duration {
44    let max_nanos = Duration::MAX.as_nanos();
45    let nanos = nanos.min(max_nanos);
46    let seconds = nanos / 1_000_000_000;
47    let subsec_nanos = u32::try_from(nanos % 1_000_000_000).unwrap_or(999_999_999);
48    Duration::new(u64::try_from(seconds).unwrap_or(u64::MAX), subsec_nanos)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::{apply_jitter, cap_delay, exponential_delay};
54    use std::time::Duration;
55
56    #[test]
57    fn exponential_delay_doubles_and_saturates() {
58        let initial = Duration::from_millis(100);
59        assert_eq!(exponential_delay(initial, 0), Duration::from_millis(100));
60        assert_eq!(exponential_delay(initial, 1), Duration::from_millis(200));
61        assert_eq!(exponential_delay(initial, 2), Duration::from_millis(400));
62        assert_eq!(exponential_delay(initial, u32::MAX), Duration::MAX);
63    }
64
65    #[test]
66    fn exponential_delay_handles_zero_and_duration_max() {
67        assert_eq!(exponential_delay(Duration::ZERO, u32::MAX), Duration::ZERO);
68        assert_eq!(exponential_delay(Duration::MAX, 0), Duration::MAX);
69        assert_eq!(exponential_delay(Duration::MAX, 1), Duration::MAX);
70    }
71
72    #[test]
73    fn cap_delay_handles_zero_equal_and_inverted_bounds() {
74        assert_eq!(
75            cap_delay(Duration::from_secs(1), Duration::ZERO),
76            Duration::ZERO
77        );
78        assert_eq!(
79            cap_delay(Duration::from_secs(1), Duration::from_secs(1)),
80            Duration::from_secs(1)
81        );
82        assert_eq!(
83            cap_delay(Duration::from_secs(1), Duration::from_secs(2)),
84            Duration::from_secs(1)
85        );
86    }
87
88    #[test]
89    fn apply_jitter_supports_exact_boundaries_and_saturates() {
90        let delay = Duration::from_millis(100);
91        assert_eq!(apply_jitter(delay, 0), Duration::ZERO);
92        assert_eq!(apply_jitter(delay, 50), Duration::from_millis(50));
93        assert_eq!(apply_jitter(delay, 100), delay);
94        assert_eq!(apply_jitter(delay, 150), Duration::from_millis(150));
95        assert_eq!(apply_jitter(Duration::MAX, u16::MAX), Duration::MAX);
96    }
97
98    #[test]
99    fn composition_keeps_cap_order_explicit() {
100        let raw = exponential_delay(Duration::from_millis(100), 2);
101        assert_eq!(
102            cap_delay(apply_jitter(raw, 150), Duration::from_millis(250)),
103            Duration::from_millis(250)
104        );
105        assert_eq!(
106            apply_jitter(cap_delay(raw, Duration::from_millis(250)), 150),
107            Duration::from_millis(375)
108        );
109    }
110
111    #[test]
112    fn randomized_jitter_stays_inside_configured_range() {
113        for _ in 0..64 {
114            let delay = super::randomized_jitter(Duration::from_millis(100), 50, 100);
115            assert!((50..=100).contains(&delay.as_millis()));
116        }
117    }
118
119    #[test]
120    fn randomized_jitter_normalizes_degenerate_and_large_bounds() {
121        assert_eq!(
122            super::randomized_jitter(Duration::from_millis(100), 200, 50),
123            Duration::from_millis(200)
124        );
125        assert_eq!(
126            super::randomized_jitter(Duration::from_millis(100), u16::MAX, u16::MAX),
127            Duration::from_millis(1_000)
128        );
129        assert_eq!(
130            super::randomized_jitter(Duration::ZERO, 50, 150),
131            Duration::ZERO
132        );
133    }
134}