aster_forge_utils/
backoff.rs1use std::time::Duration;
7
8#[must_use]
10pub fn exponential_delay(initial_delay: Duration, retry_index: u32) -> Duration {
11 let mut delay = initial_delay;
12 let mut remaining = retry_index.min(127);
13 while remaining > 0 && delay < Duration::MAX {
14 delay = delay.saturating_add(delay);
15 remaining -= 1;
16 }
17 delay
18}
19
20#[must_use]
22pub fn cap_delay(delay: Duration, max_delay: Duration) -> Duration {
23 delay.min(max_delay)
24}
25
26#[must_use]
28pub fn apply_jitter(delay: Duration, percent: u16) -> Duration {
29 let nanos = delay.as_nanos().saturating_mul(u128::from(percent));
30 let nanos = nanos.checked_div(100).unwrap_or(0);
31 duration_from_nanos(nanos)
32}
33
34#[must_use]
39pub fn randomized_jitter(delay: Duration, min_percent: u16, max_percent: u16) -> Duration {
40 use rand::RngExt;
41
42 let min_percent = min_percent.min(1000);
43 let max_percent = max_percent.min(1000).max(min_percent);
44 apply_jitter(delay, rand::rng().random_range(min_percent..=max_percent))
45}
46
47fn duration_from_nanos(nanos: u128) -> Duration {
48 let max_nanos = Duration::MAX.as_nanos();
49 let nanos = nanos.min(max_nanos);
50 let seconds = nanos / 1_000_000_000;
51 let subsec_nanos = u32::try_from(nanos % 1_000_000_000).unwrap_or(999_999_999);
52 Duration::new(u64::try_from(seconds).unwrap_or(u64::MAX), subsec_nanos)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::{apply_jitter, cap_delay, exponential_delay};
58 use std::time::Duration;
59
60 #[test]
61 fn exponential_delay_doubles_and_saturates() {
62 let initial = Duration::from_millis(100);
63 assert_eq!(exponential_delay(initial, 0), Duration::from_millis(100));
64 assert_eq!(exponential_delay(initial, 1), Duration::from_millis(200));
65 assert_eq!(exponential_delay(initial, 2), Duration::from_millis(400));
66 assert_eq!(exponential_delay(initial, u32::MAX), Duration::MAX);
67 }
68
69 #[test]
70 fn exponential_delay_handles_zero_and_duration_max() {
71 assert_eq!(exponential_delay(Duration::ZERO, u32::MAX), Duration::ZERO);
72 assert_eq!(exponential_delay(Duration::MAX, 0), Duration::MAX);
73 assert_eq!(exponential_delay(Duration::MAX, 1), Duration::MAX);
74 }
75
76 #[test]
77 fn cap_delay_handles_zero_equal_and_inverted_bounds() {
78 assert_eq!(
79 cap_delay(Duration::from_secs(1), Duration::ZERO),
80 Duration::ZERO
81 );
82 assert_eq!(
83 cap_delay(Duration::from_secs(1), Duration::from_secs(1)),
84 Duration::from_secs(1)
85 );
86 assert_eq!(
87 cap_delay(Duration::from_secs(1), Duration::from_secs(2)),
88 Duration::from_secs(1)
89 );
90 }
91
92 #[test]
93 fn apply_jitter_supports_exact_boundaries_and_saturates() {
94 let delay = Duration::from_millis(100);
95 assert_eq!(apply_jitter(delay, 0), Duration::ZERO);
96 assert_eq!(apply_jitter(delay, 50), Duration::from_millis(50));
97 assert_eq!(apply_jitter(delay, 100), delay);
98 assert_eq!(apply_jitter(delay, 150), Duration::from_millis(150));
99 assert_eq!(apply_jitter(Duration::MAX, u16::MAX), Duration::MAX);
100 }
101
102 #[test]
103 fn composition_keeps_cap_order_explicit() {
104 let raw = exponential_delay(Duration::from_millis(100), 2);
105 assert_eq!(
106 cap_delay(apply_jitter(raw, 150), Duration::from_millis(250)),
107 Duration::from_millis(250)
108 );
109 assert_eq!(
110 apply_jitter(cap_delay(raw, Duration::from_millis(250)), 150),
111 Duration::from_millis(375)
112 );
113 }
114
115 #[test]
116 fn randomized_jitter_stays_inside_configured_range() {
117 for _ in 0..64 {
118 let delay = super::randomized_jitter(Duration::from_millis(100), 50, 100);
119 assert!((50..=100).contains(&delay.as_millis()));
120 }
121 }
122
123 #[test]
124 fn randomized_jitter_normalizes_degenerate_and_large_bounds() {
125 assert_eq!(
126 super::randomized_jitter(Duration::from_millis(100), 200, 50),
127 Duration::from_millis(200)
128 );
129 assert_eq!(
130 super::randomized_jitter(Duration::from_millis(100), u16::MAX, u16::MAX),
131 Duration::from_secs(1)
132 );
133 assert_eq!(
134 super::randomized_jitter(Duration::ZERO, 50, 150),
135 Duration::ZERO
136 );
137 }
138}