object_store/client/
backoff.rs1use rand::prelude::*;
19use std::time::Duration;
20
21#[allow(missing_copy_implementations)]
25#[derive(Debug, Clone)]
26pub struct BackoffConfig {
27 pub init_backoff: Duration,
29 pub max_backoff: Duration,
31 pub base: f64,
33}
34
35impl Default for BackoffConfig {
36 fn default() -> Self {
37 Self {
38 init_backoff: Duration::from_millis(100),
39 max_backoff: Duration::from_secs(15),
40 base: 2.,
41 }
42 }
43}
44
45pub struct Backoff {
50 init_backoff: f64,
51 next_backoff_secs: f64,
52 max_backoff_secs: f64,
53 base: f64,
54 rng: Option<Box<dyn RngCore + Sync + Send>>,
55}
56
57impl std::fmt::Debug for Backoff {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("Backoff")
60 .field("init_backoff", &self.init_backoff)
61 .field("next_backoff_secs", &self.next_backoff_secs)
62 .field("max_backoff_secs", &self.max_backoff_secs)
63 .field("base", &self.base)
64 .finish()
65 }
66}
67
68impl Backoff {
69 pub fn new(config: &BackoffConfig) -> Self {
71 Self::new_with_rng(config, None)
72 }
73
74 pub fn new_with_rng(
78 config: &BackoffConfig,
79 rng: Option<Box<dyn RngCore + Sync + Send>>,
80 ) -> Self {
81 let init_backoff = config.init_backoff.as_secs_f64();
82 Self {
83 init_backoff,
84 next_backoff_secs: init_backoff,
85 max_backoff_secs: config.max_backoff.as_secs_f64(),
86 base: config.base,
87 rng,
88 }
89 }
90
91 pub fn next(&mut self) -> Duration {
93 let range = self.init_backoff..(self.next_backoff_secs * self.base);
94
95 let rand_backoff = match self.rng.as_mut() {
96 Some(rng) => rng.gen_range(range),
97 None => thread_rng().gen_range(range),
98 };
99
100 let next_backoff = self.max_backoff_secs.min(rand_backoff);
101 Duration::from_secs_f64(std::mem::replace(&mut self.next_backoff_secs, next_backoff))
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use rand::rngs::mock::StepRng;
109
110 #[test]
111 fn test_backoff() {
112 let init_backoff_secs = 1.;
113 let max_backoff_secs = 500.;
114 let base = 3.;
115
116 let config = BackoffConfig {
117 init_backoff: Duration::from_secs_f64(init_backoff_secs),
118 max_backoff: Duration::from_secs_f64(max_backoff_secs),
119 base,
120 };
121
122 let assert_fuzzy_eq = |a: f64, b: f64| assert!((b - a).abs() < 0.0001, "{a} != {b}");
123
124 let rng = Box::new(StepRng::new(0, 0));
126 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
127
128 for _ in 0..20 {
129 assert_eq!(backoff.next().as_secs_f64(), init_backoff_secs);
130 }
131
132 let rng = Box::new(StepRng::new(u64::MAX, 0));
134 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
135
136 for i in 0..20 {
137 let value = (base.powi(i) * init_backoff_secs).min(max_backoff_secs);
138 assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
139 }
140
141 let rng = Box::new(StepRng::new(u64::MAX / 2, 0));
143 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
144
145 let mut value = init_backoff_secs;
146 for _ in 0..20 {
147 assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
148 value =
149 (init_backoff_secs + (value * base - init_backoff_secs) / 2.).min(max_backoff_secs);
150 }
151 }
152}