object_store/client/
backoff.rs1use rand::{Rng, RngExt, rng};
19use std::time::Duration;
20
21#[allow(missing_copy_implementations)]
30#[derive(Debug, Clone)]
31pub struct BackoffConfig {
32 pub init_backoff: Duration,
34 pub max_backoff: Duration,
36 pub base: f64,
38}
39
40impl Default for BackoffConfig {
41 fn default() -> Self {
42 Self {
43 init_backoff: Duration::from_millis(100),
44 max_backoff: Duration::from_secs(15),
45 base: 2.,
46 }
47 }
48}
49
50pub(crate) struct Backoff {
55 init_backoff: f64,
56 next_backoff_secs: f64,
57 max_backoff_secs: f64,
58 base: f64,
59 rng: Option<Box<dyn Rng + Sync + Send>>,
60}
61
62impl std::fmt::Debug for Backoff {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("Backoff")
65 .field("init_backoff", &self.init_backoff)
66 .field("next_backoff_secs", &self.next_backoff_secs)
67 .field("max_backoff_secs", &self.max_backoff_secs)
68 .field("base", &self.base)
69 .finish()
70 }
71}
72
73impl Backoff {
74 pub(crate) fn new(config: &BackoffConfig) -> Self {
76 Self::new_with_rng(config, None)
77 }
78
79 pub(crate) fn new_with_rng(
83 config: &BackoffConfig,
84 rng: Option<Box<dyn Rng + Sync + Send>>,
85 ) -> Self {
86 let init_backoff = config.init_backoff.as_secs_f64();
87 Self {
88 init_backoff,
89 next_backoff_secs: init_backoff,
90 max_backoff_secs: config.max_backoff.as_secs_f64(),
91 base: config.base,
92 rng,
93 }
94 }
95
96 pub(crate) fn next(&mut self) -> Duration {
98 let range = self.init_backoff..(self.next_backoff_secs * self.base);
99
100 let rand_backoff = match self.rng.as_mut() {
101 Some(rng) => rng.random_range(range),
102 None => rng().random_range(range),
103 };
104
105 let next_backoff = self.max_backoff_secs.min(rand_backoff);
106 Duration::from_secs_f64(std::mem::replace(&mut self.next_backoff_secs, next_backoff))
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::convert::Infallible;
113
114 use rand::{TryRng, rand_core::utils::fill_bytes_via_next_word};
115
116 use super::*;
117
118 struct FixedRng(u64);
119
120 impl TryRng for FixedRng {
121 type Error = Infallible;
122
123 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
124 Ok(self.0 as _)
125 }
126
127 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
128 Ok(self.0)
129 }
130
131 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
132 fill_bytes_via_next_word(dst, || self.try_next_u64())
133 }
134 }
135
136 #[test]
137 fn test_backoff() {
138 let init_backoff_secs = 1.;
139 let max_backoff_secs = 500.;
140 let base = 3.;
141
142 let config = BackoffConfig {
143 init_backoff: Duration::from_secs_f64(init_backoff_secs),
144 max_backoff: Duration::from_secs_f64(max_backoff_secs),
145 base,
146 };
147
148 let assert_fuzzy_eq = |a: f64, b: f64| assert!((b - a).abs() < 0.0001, "{a} != {b}");
149
150 let rng = Box::new(FixedRng(0));
152 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
153
154 for _ in 0..20 {
155 assert_eq!(backoff.next().as_secs_f64(), init_backoff_secs);
156 }
157
158 let rng = Box::new(FixedRng(u64::MAX));
160 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
161
162 for i in 0..20 {
163 let value = (base.powi(i) * init_backoff_secs).min(max_backoff_secs);
164 assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
165 }
166
167 let rng = Box::new(FixedRng(u64::MAX / 2));
169 let mut backoff = Backoff::new_with_rng(&config, Some(rng));
170
171 let mut value = init_backoff_secs;
172 for _ in 0..20 {
173 assert_fuzzy_eq(backoff.next().as_secs_f64(), value);
174 value =
175 (init_backoff_secs + (value * base - init_backoff_secs) / 2.).min(max_backoff_secs);
176 }
177 }
178}