lance_core/utils/
backoff.rs1use rand::{Rng, SeedableRng};
2use std::time::Duration;
3
4pub struct Backoff {
18 base: u32,
19 unit: u32,
20 jitter: i32,
21 min: u32,
22 max: u32,
23 attempt: u32,
24}
25
26impl Default for Backoff {
27 fn default() -> Self {
28 Self {
29 base: 2,
30 unit: 50,
31 jitter: 50,
32 min: 0,
33 max: 5000,
34 attempt: 0,
35 }
36 }
37}
38
39impl Backoff {
40 pub fn with_base(self, base: u32) -> Self {
41 Self { base, ..self }
42 }
43
44 pub fn with_unit(self, unit: u32) -> Self {
45 Self { unit, ..self }
46 }
47
48 pub fn with_jitter(self, jitter: i32) -> Self {
49 Self { jitter, ..self }
50 }
51
52 pub fn with_min(self, min: u32) -> Self {
53 Self { min, ..self }
54 }
55
56 pub fn with_max(self, max: u32) -> Self {
57 Self { max, ..self }
58 }
59
60 pub fn next_backoff(&mut self) -> Duration {
61 let backoff = self
62 .base
63 .saturating_pow(self.attempt)
64 .saturating_mul(self.unit);
65 let jitter = rand::rng().random_range(-self.jitter..=self.jitter);
66 let backoff = (backoff.saturating_add_signed(jitter)).clamp(self.min, self.max);
67 self.attempt += 1;
68 Duration::from_millis(backoff as u64)
69 }
70
71 pub fn attempt(&self) -> u32 {
72 self.attempt
73 }
74
75 pub fn reset(&mut self) {
76 self.attempt = 0;
77 }
78}
79
80const MAX_SLOTS: u32 = 128;
88
89pub struct SlotBackoff {
121 base: u32,
122 unit: u32,
123 starting_i: u32,
124 attempt: u32,
125 rng: rand::rngs::SmallRng,
126}
127
128impl Default for SlotBackoff {
129 fn default() -> Self {
130 Self {
131 base: 2,
132 unit: 50,
133 starting_i: 2, attempt: 0,
135 rng: rand::rngs::SmallRng::from_os_rng(),
136 }
137 }
138}
139
140impl SlotBackoff {
141 pub fn with_unit(self, unit: u32) -> Self {
142 Self { unit, ..self }
143 }
144
145 pub fn attempt(&self) -> u32 {
146 self.attempt
147 }
148
149 pub fn next_backoff(&mut self) -> Duration {
150 let num_slots = self
151 .base
152 .saturating_pow(self.attempt.saturating_add(self.starting_i))
153 .min(MAX_SLOTS);
154 let slot_i = self.rng.random_range(0..num_slots);
155 self.attempt = self.attempt.saturating_add(1);
156 Duration::from_millis(slot_i as u64 * self.unit as u64)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_backoff() {
168 let mut backoff = Backoff::default().with_jitter(0);
169 assert_eq!(backoff.next_backoff().as_millis(), 50);
170 assert_eq!(backoff.attempt(), 1);
171 assert_eq!(backoff.next_backoff().as_millis(), 100);
172 assert_eq!(backoff.attempt(), 2);
173 assert_eq!(backoff.next_backoff().as_millis(), 200);
174 assert_eq!(backoff.attempt(), 3);
175 assert_eq!(backoff.next_backoff().as_millis(), 400);
176 assert_eq!(backoff.attempt(), 4);
177 }
178
179 #[test]
180 fn test_backoff_with_base() {
181 let mut backoff = Backoff::default().with_base(3).with_jitter(0);
182 assert_eq!(backoff.next_backoff().as_millis(), 50); assert_eq!(backoff.next_backoff().as_millis(), 150); assert_eq!(backoff.next_backoff().as_millis(), 450); }
186
187 #[test]
188 fn test_backoff_with_unit() {
189 let mut backoff = Backoff::default().with_unit(100).with_jitter(0);
190 assert_eq!(backoff.next_backoff().as_millis(), 100); assert_eq!(backoff.next_backoff().as_millis(), 200); }
193
194 #[test]
195 fn test_backoff_with_min() {
196 let mut backoff = Backoff::default().with_min(100).with_jitter(0);
197 assert_eq!(backoff.next_backoff().as_millis(), 100); }
199
200 #[test]
201 fn test_backoff_with_max() {
202 let mut backoff = Backoff::default().with_max(75).with_jitter(0);
203 assert_eq!(backoff.next_backoff().as_millis(), 50);
204 assert_eq!(backoff.next_backoff().as_millis(), 75); }
206
207 #[test]
208 fn test_backoff_reset() {
209 let mut backoff = Backoff::default().with_jitter(0);
210 assert_eq!(backoff.next_backoff().as_millis(), 50);
211 assert_eq!(backoff.attempt(), 1);
212 backoff.reset();
213 assert_eq!(backoff.attempt(), 0);
214 assert_eq!(backoff.next_backoff().as_millis(), 50);
215 }
216
217 #[test]
218 fn test_slot_backoff() {
219 #[cfg_attr(coverage, coverage(off))]
220 fn assert_in(value: u128, expected: &[u128]) {
221 assert!(
222 expected.contains(&value),
223 "value {} not in {:?}",
224 value,
225 expected
226 );
227 }
228
229 for _ in 0..10 {
230 let mut backoff = SlotBackoff::default().with_unit(100);
231 assert_in(backoff.next_backoff().as_millis(), &[0, 100, 200, 300]);
232 assert_eq!(backoff.attempt(), 1);
233 assert_in(
234 backoff.next_backoff().as_millis(),
235 &[0, 100, 200, 300, 400, 500, 600, 700],
236 );
237 assert_eq!(backoff.attempt(), 2);
238 assert_in(
239 backoff.next_backoff().as_millis(),
240 &(0..16).map(|i| i * 100).collect::<Vec<_>>(),
241 );
242 assert_eq!(backoff.attempt(), 3);
243 }
244 }
245
246 #[test]
247 fn test_slot_backoff_high_attempt_is_bounded() {
248 let unit = 100_000; let mut backoff = SlotBackoff::default().with_unit(unit);
252 let max_backoff = Duration::from_millis((MAX_SLOTS - 1) as u64 * unit as u64);
253 for _ in 0..40 {
254 assert!(backoff.next_backoff() <= max_backoff);
255 }
256 assert_eq!(backoff.attempt(), 40);
257 }
258
259 #[test]
260 fn test_slot_backoff_large_unit_does_not_overflow() {
261 let unit = u32::MAX;
267 let mut backoff = SlotBackoff::default().with_unit(unit);
268 backoff.rng = rand::rngs::SmallRng::seed_from_u64(0);
269 let mut saw_high_slot = false;
270 for _ in 0..64 {
271 let backoff_ms = backoff.next_backoff().as_millis();
272 assert_eq!(backoff_ms % unit as u128, 0, "{backoff_ms} wrapped");
275 saw_high_slot |= backoff_ms >= 2 * unit as u128;
276 }
277 assert!(saw_high_slot, "expected a slot >= 2 in 64 seeded draws");
278 }
279
280 #[test]
281 fn test_slot_backoff_attempt_saturates() {
282 let mut backoff = SlotBackoff {
285 attempt: u32::MAX,
286 ..Default::default()
287 };
288 let _ = backoff.next_backoff();
289 assert_eq!(backoff.attempt(), u32::MAX);
290 }
291}