tuwunel_core/utils/time/
exponential_backoff.rs1use std::time::Duration;
8
9#[inline]
11#[must_use]
12pub fn continue_exponential_backoff_secs(
13 min: u64,
14 max: u64,
15 elapsed: Duration,
16 tries: u32,
17) -> bool {
18 exponential_backoff_remaining_secs(min, max, elapsed, tries).is_some()
19}
20
21#[inline]
23#[must_use]
24pub fn continue_exponential_backoff(
25 min: Duration,
26 max: Duration,
27 elapsed: Duration,
28 tries: u32,
29) -> bool {
30 exponential_backoff_remaining(min, max, elapsed, tries).is_some()
31}
32
33#[inline]
38#[must_use]
39pub fn exponential_backoff_remaining_secs(
40 min: u64,
41 max: u64,
42 elapsed: Duration,
43 tries: u32,
44) -> Option<Duration> {
45 let min = Duration::from_secs(min);
46 let max = Duration::from_secs(max);
47
48 exponential_backoff_remaining(min, max, elapsed, tries)
49}
50
51#[inline]
56#[must_use]
57pub fn exponential_backoff_remaining(
58 min: Duration,
59 max: Duration,
60 elapsed: Duration,
61 tries: u32,
62) -> Option<Duration> {
63 let window = min
64 .saturating_mul(tries)
65 .saturating_mul(tries)
66 .min(max);
67
68 window
69 .checked_sub(elapsed)
70 .filter(|remaining| !remaining.is_zero())
71}
72
73#[inline]
79#[must_use]
80pub fn exponential_backoff_streak_cap(min: Duration, max: Duration) -> u32 {
81 let min_secs = min.as_secs().max(1);
82 let ratio = max.as_secs().checked_div(min_secs).unwrap_or(0);
83 let floor = ratio.isqrt();
84 let ceil = if floor.saturating_mul(floor) < ratio {
85 floor.saturating_add(1)
86 } else {
87 floor
88 };
89
90 u32::try_from(ceil).unwrap_or(u32::MAX).max(1)
91}
92
93#[cfg(test)]
94mod tests {
95 use std::time::Duration;
96
97 use super::{exponential_backoff_remaining, exponential_backoff_remaining_secs};
98
99 #[test]
100 fn remaining_tracks_the_quadratic_window() {
101 let min = Duration::from_secs(3);
102 let max = Duration::from_mins(1);
103 let elapsed = Duration::from_secs(5);
104
105 assert_eq!(
106 exponential_backoff_remaining(min, max, elapsed, 2),
107 Some(Duration::from_secs(7)),
108 );
109 }
110
111 #[test]
112 fn remaining_caps_and_expires_at_the_boundary() {
113 let min = Duration::from_secs(3);
114 let max = Duration::from_mins(1);
115 let elapsed = Duration::from_mins(1);
116
117 assert_eq!(
118 exponential_backoff_remaining(min, max, Duration::from_secs(15), 100),
119 Some(Duration::from_secs(45)),
120 );
121
122 assert_eq!(exponential_backoff_remaining(min, max, elapsed, 100), None);
123 assert_eq!(
124 exponential_backoff_remaining(Duration::ZERO, max, Duration::ZERO, u32::MAX),
125 None,
126 );
127 assert_eq!(
128 exponential_backoff_remaining(
129 Duration::MAX,
130 Duration::MAX,
131 Duration::from_secs(1),
132 u32::MAX,
133 ),
134 Duration::MAX.checked_sub(Duration::from_secs(1)),
135 );
136 }
137
138 #[test]
139 fn seconds_wrapper_matches_duration_bounds() {
140 let elapsed = Duration::from_secs(5);
141
142 assert_eq!(
143 exponential_backoff_remaining_secs(3, 60, elapsed, 2),
144 exponential_backoff_remaining(
145 Duration::from_secs(3),
146 Duration::from_mins(1),
147 elapsed,
148 2,
149 ),
150 );
151 }
152}