saluki_io/net/util/retry/
backoff.rs1use std::{
2 convert::Infallible,
3 fmt,
4 sync::{Arc, Mutex},
5 time::Duration,
6};
7
8use rand::{rand_core::TryRng, rng, Rng as _, RngExt as _};
9
10#[derive(Clone)]
11pub enum BackoffRng {
12 SecureDefault,
18
19 Shared(Arc<Mutex<Box<dyn TryRng<Error = Infallible> + Send + Sync>>>),
21}
22
23impl fmt::Debug for BackoffRng {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 BackoffRng::SecureDefault => f.debug_tuple("SecureDefault").finish(),
27 BackoffRng::Shared(_) => f.debug_tuple("Shared").finish(),
28 }
29 }
30}
31
32impl TryRng for BackoffRng {
33 type Error = Infallible;
34
35 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
36 Ok(match self {
37 BackoffRng::SecureDefault => rng().next_u32(),
38 BackoffRng::Shared(rng) => rng.lock().unwrap().next_u32(),
39 })
40 }
41
42 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
43 Ok(match self {
44 BackoffRng::SecureDefault => rng().next_u64(),
45 BackoffRng::Shared(rng) => rng.lock().unwrap().next_u64(),
46 })
47 }
48
49 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
50 match self {
51 BackoffRng::SecureDefault => rng().fill_bytes(dst),
52 BackoffRng::Shared(rng) => rng.lock().unwrap().fill_bytes(dst),
53 }
54 Ok(())
55 }
56}
57
58#[derive(Clone, Debug)]
64pub struct ExponentialBackoff {
65 min_backoff: Duration,
66 max_backoff: Duration,
67 min_backoff_factor: f64,
68 rng: BackoffRng,
69}
70
71impl ExponentialBackoff {
72 pub fn new(min_backoff: Duration, max_backoff: Duration) -> Self {
76 Self {
77 min_backoff,
78 max_backoff,
79 min_backoff_factor: 1.0,
80 rng: BackoffRng::SecureDefault,
81 }
82 }
83
84 pub fn with_jitter(min_backoff: Duration, max_backoff: Duration, min_backoff_factor: f64) -> Self {
96 Self {
97 min_backoff,
98 max_backoff,
99 min_backoff_factor: min_backoff_factor.max(1.0),
100 rng: BackoffRng::SecureDefault,
101 }
102 }
103
104 pub fn with_rng<R>(self, rng: R) -> Self
111 where
112 R: TryRng<Error = Infallible> + Send + Sync + 'static,
113 {
114 ExponentialBackoff {
115 min_backoff: self.min_backoff,
116 max_backoff: self.max_backoff,
117 min_backoff_factor: self.min_backoff_factor,
118 rng: BackoffRng::Shared(Arc::new(Mutex::new(Box::new(rng)))),
119 }
120 }
121
122 pub fn get_backoff_duration(&mut self, error_count: u32) -> Duration {
127 if error_count == 0 {
128 return self.min_backoff;
129 }
130
131 let mut backoff = self.min_backoff.saturating_mul(2u32.saturating_pow(error_count));
132
133 if self.min_backoff_factor > 1.0 {
135 let backoff_lower = backoff.div_f64(self.min_backoff_factor);
136 let backoff_upper = backoff;
137 backoff = self.rng.random_range(backoff_lower..=backoff_upper)
138 }
139
140 backoff.clamp(self.min_backoff, self.max_backoff)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use std::time::Duration;
147
148 use proptest::prelude::*;
149
150 use crate::net::util::retry::ExponentialBackoff;
151
152 fn arb_exponential_backoff(min_backoff_factor: f64) -> impl Strategy<Value = ExponentialBackoff> {
153 (1u64..=u64::MAX, 1u64..u64::MAX)
154 .prop_map(move |(min_backoff, max_backoff)| {
155 let max_backoff = min_backoff.saturating_add(max_backoff);
156 ExponentialBackoff::with_jitter(
157 Duration::from_nanos(min_backoff),
158 Duration::from_nanos(max_backoff),
159 min_backoff_factor,
160 )
161 })
162 .prop_perturb(|backoff, rng| backoff.with_rng(rng))
163 }
164
165 proptest! {
166 #[test]
167 fn property_test_exponential_backoff_no_jitter(
168 mut backoff in arb_exponential_backoff(1.0),
169 error_count in 0..u32::MAX,
170 error_count_increase in 1..5u32
171 ) {
172 let first = backoff.get_backoff_duration(error_count);
175 let first_followup = backoff.get_backoff_duration(error_count);
176 let second = backoff.get_backoff_duration(error_count.saturating_add(error_count_increase));
177 let second_followup = backoff.get_backoff_duration(error_count.saturating_add(error_count_increase));
178
179 assert_eq!(first, first_followup);
180 assert_eq!(second, second_followup);
181 assert!(first <= second);
182 assert!(first >= backoff.min_backoff);
183 assert!(first <= backoff.max_backoff);
184 assert!(second >= backoff.min_backoff);
185 assert!(second <= backoff.max_backoff);
186 }
187
188 #[test]
189 fn property_test_exponential_backoff_default_jitter(
190 mut backoff in arb_exponential_backoff(2.0),
191 error_count in 0..u32::MAX,
192 error_count_increase in 1..5u32
193 ) {
194 let first = backoff.get_backoff_duration(error_count);
197 let second = backoff.get_backoff_duration(error_count.saturating_add(error_count_increase));
198
199 assert!(first <= second);
200 assert!(first >= backoff.min_backoff);
201 assert!(first <= backoff.max_backoff);
202 assert!(second >= backoff.min_backoff);
203 assert!(second <= backoff.max_backoff);
204 }
205 }
206}