aster_forge_utils/
backoff.rs1use std::time::Duration;
7
8pub 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
19pub fn cap_delay(delay: Duration, max_delay: Duration) -> Duration {
21 delay.min(max_delay)
22}
23
24pub 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
31pub 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}