1use std::time::Duration;
4
5pub fn calculate_backoff(attempt: u32, min_delay: Duration, max_delay: Duration) -> Duration {
23 if max_delay <= min_delay {
24 return min_delay;
25 }
26 if attempt <= 1 {
27 return min_delay;
28 }
29 if min_delay.is_zero() {
30 return min_delay;
31 }
32
33 let mut delay = min_delay;
34 for _ in 1..attempt {
35 let Some(next) = delay.checked_mul(2) else {
36 return max_delay;
37 };
38 if next >= max_delay {
39 return max_delay;
40 }
41 delay = next;
42 }
43 delay
44}
45
46#[must_use]
51pub fn calculate_jittered_backoff(
52 attempt: u32,
53 min_delay: Duration,
54 max_delay: Duration,
55 jitter_permyriad: u16,
56) -> Option<Duration> {
57 if jitter_permyriad > 10_000 {
58 return None;
59 }
60 let limit = calculate_backoff(attempt, min_delay, max_delay);
61 if limit <= min_delay {
62 return Some(min_delay);
63 }
64
65 let min_nanos = min_delay.as_nanos();
66 let span_nanos = limit.as_nanos().saturating_sub(min_nanos);
67 let jitter_nanos = span_nanos
68 .checked_mul(u128::from(jitter_permyriad))?
69 .checked_div(10_000)?;
70 min_delay.checked_add(duration_from_nanos(jitter_nanos)?)
71}
72
73fn duration_from_nanos(nanos: u128) -> Option<Duration> {
74 let secs = nanos / 1_000_000_000;
75 let subsec_nanos = nanos % 1_000_000_000;
76 Some(Duration::new(
77 u64::try_from(secs).ok()?,
78 u32::try_from(subsec_nanos).ok()?,
79 ))
80}
81
82#[derive(Debug, Clone, Copy)]
84pub struct ExponentialBackoff {
85 min_delay: Duration,
86 max_delay: Duration,
87}
88
89impl ExponentialBackoff {
90 #[must_use]
92 pub const fn new(min_delay: Duration, max_delay: Duration) -> Self {
93 Self {
94 min_delay,
95 max_delay,
96 }
97 }
98
99 #[must_use]
101 pub fn delay(&self, attempt: u32) -> Duration {
102 calculate_backoff(attempt, self.min_delay, self.max_delay)
103 }
104
105 #[must_use]
107 pub fn jittered_delay(&self, attempt: u32, jitter_permyriad: u16) -> Option<Duration> {
108 calculate_jittered_backoff(attempt, self.min_delay, self.max_delay, jitter_permyriad)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_calculate_backoff_bounds() {
118 let min = Duration::from_millis(100);
119 let max = Duration::from_secs(5);
120
121 for attempt in 1..10 {
122 let backoff = calculate_backoff(attempt, min, max);
123 assert!(backoff >= min, "delay {backoff:?} < min {min:?}");
124 assert!(backoff <= max, "delay {backoff:?} > max {max:?}");
125 }
126 }
127
128 #[test]
129 fn test_calculate_backoff_reaches_cap_for_high_attempts() {
130 let min = Duration::from_nanos(1);
131 let max = Duration::from_secs(10);
132
133 assert_eq!(calculate_backoff(64, min, max), max);
134 }
135
136 #[test]
137 fn test_calculate_jittered_backoff_is_deterministic() {
138 let min = Duration::from_millis(100);
139 let max = Duration::from_secs(5);
140
141 assert_eq!(calculate_jittered_backoff(3, min, max, 0), Some(min));
142 assert_eq!(
143 calculate_jittered_backoff(3, min, max, 10_000),
144 Some(calculate_backoff(3, min, max))
145 );
146 assert_eq!(calculate_jittered_backoff(3, min, max, 10_001), None);
147 }
148
149 #[test]
150 fn test_exponential_backoff_struct() {
151 let backoff = ExponentialBackoff::new(Duration::from_millis(50), Duration::from_secs(1));
152 let delay_1 = backoff.delay(1);
153 assert!(delay_1 >= Duration::from_millis(50));
154 assert!(delay_1 <= Duration::from_secs(1));
155 assert!(backoff.jittered_delay(1, 5_000).is_some());
156 }
157}