1use std::collections::BTreeSet;
6use std::fmt;
7use std::sync::Arc;
8use std::time::Duration;
9
10use http::StatusCode;
11
12use crate::error::{Error, Result};
13
14pub type RetryPredicate = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
16
17#[derive(Clone)]
22#[non_exhaustive]
23pub struct RetryPolicy {
24 pub max_retries: u32,
26 pub backoff_initial: Duration,
28 pub backoff_max: Duration,
30 pub backoff_jitter: f64,
32 pub http_statuses: BTreeSet<StatusCode>,
34 pub respect_retry_after: bool,
36 pub retry_connection_errors: bool,
38 pub retry_timeouts: bool,
40 pub predicate: Option<RetryPredicate>,
42 pub budget: Option<Duration>,
44}
45
46impl Default for RetryPolicy {
47 fn default() -> Self {
48 let mut statuses: BTreeSet<StatusCode> = (500..600)
49 .filter_map(|s| StatusCode::from_u16(s).ok())
50 .collect();
51 statuses.extend([StatusCode::REQUEST_TIMEOUT, StatusCode::TOO_MANY_REQUESTS]);
52 Self {
53 max_retries: 2,
54 backoff_initial: Duration::from_millis(500),
55 backoff_max: Duration::from_secs(5),
56 backoff_jitter: 0.25,
57 http_statuses: statuses,
58 respect_retry_after: true,
59 retry_connection_errors: true,
60 retry_timeouts: true,
61 predicate: None,
62 budget: Some(Duration::from_secs(30)),
63 }
64 }
65}
66
67impl fmt::Debug for RetryPolicy {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.debug_struct("RetryPolicy")
70 .field("max_retries", &self.max_retries)
71 .field("backoff_initial", &self.backoff_initial)
72 .field("backoff_max", &self.backoff_max)
73 .field("backoff_jitter", &self.backoff_jitter)
74 .field("http_statuses", &self.http_statuses)
75 .field("respect_retry_after", &self.respect_retry_after)
76 .field("retry_connection_errors", &self.retry_connection_errors)
77 .field("retry_timeouts", &self.retry_timeouts)
78 .field("predicate", &self.predicate.as_ref().map(|_| "<fn>"))
79 .field("budget", &self.budget)
80 .finish()
81 }
82}
83
84impl RetryPolicy {
85 pub fn none() -> Self {
87 Self {
88 max_retries: 0,
89 ..Self::default()
90 }
91 }
92
93 #[must_use]
95 pub fn max_retries(mut self, n: u32) -> Self {
96 self.max_retries = n;
97 self
98 }
99
100 #[must_use]
102 pub fn backoff(mut self, initial: Duration, max: Duration) -> Self {
103 self.backoff_initial = initial;
104 self.backoff_max = max;
105 self
106 }
107
108 #[must_use]
110 pub fn jitter(mut self, fraction: f64) -> Self {
111 self.backoff_jitter = fraction;
112 self
113 }
114
115 #[must_use]
127 pub fn statuses(mut self, statuses: impl IntoIterator<Item = StatusCode>) -> Self {
128 self.http_statuses = statuses.into_iter().collect();
129 self
130 }
131
132 #[must_use]
134 pub fn budget(mut self, budget: Option<Duration>) -> Self {
135 self.budget = budget;
136 self
137 }
138
139 #[must_use]
141 pub fn respect_retry_after(mut self, yes: bool) -> Self {
142 self.respect_retry_after = yes;
143 self
144 }
145
146 #[must_use]
148 pub fn retry_connection_errors(mut self, yes: bool) -> Self {
149 self.retry_connection_errors = yes;
150 self
151 }
152
153 #[must_use]
155 pub fn retry_timeouts(mut self, yes: bool) -> Self {
156 self.retry_timeouts = yes;
157 self
158 }
159
160 #[must_use]
162 pub fn retry_if(mut self, f: impl Fn(&Error) -> bool + Send + Sync + 'static) -> Self {
163 self.predicate = Some(Arc::new(f));
164 self
165 }
166
167 pub(crate) fn validate(&self) -> Result<()> {
168 if !(0.0..=1.0).contains(&self.backoff_jitter) {
169 return Err(Error::config("backoff_jitter must be between zero and one"));
170 }
171 if self.budget == Some(Duration::ZERO) {
172 return Err(Error::config("retry budget must be a positive duration"));
173 }
174 Ok(())
175 }
176
177 pub(crate) fn is_retryable(&self, err: &Error) -> bool {
178 let builtin = match err {
179 Error::Timeout(_) => self.retry_timeouts,
180 Error::Connection(_) => self.retry_connection_errors,
181 Error::Api(e) => self.http_statuses.contains(&e.status),
182 _ => false,
183 };
184 builtin || self.predicate.as_ref().is_some_and(|p| p(err))
185 }
186
187 pub(crate) fn delay(&self, attempt: u32, err: &Error) -> Duration {
189 if self.respect_retry_after
190 && let Some(d) = err.as_api().and_then(|e| e.retry_after())
191 {
192 return d;
193 }
194 backoff(
195 attempt,
196 self.backoff_initial,
197 self.backoff_max,
198 self.backoff_jitter,
199 rand::random::<f64>(),
200 )
201 }
202
203 pub(crate) fn should_stop(&self, attempts: u32, elapsed: Duration, upcoming: Duration) -> bool {
205 attempts > self.max_retries
206 || self
207 .budget
208 .is_some_and(|b| elapsed.saturating_add(upcoming) >= b)
209 }
210}
211
212fn backoff(attempt: u32, initial: Duration, max: Duration, jitter: f64, r: f64) -> Duration {
213 let (initial, max) = (initial.as_secs_f64(), max.as_secs_f64());
214 if initial == 0.0 || max == 0.0 {
215 return Duration::ZERO;
216 }
217 let exponent = attempt.saturating_sub(1) as f64;
218 let exponential = if exponent >= max.log2() - initial.log2() {
219 max
220 } else {
221 initial * 2f64.powf(exponent)
222 };
223 let delay = exponential * (1.0 - r * jitter);
224 let rounded = (delay * 1000.0).round() / 1000.0;
225 Duration::from_secs_f64(exponential.min(rounded))
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn backoff_doubles_and_caps() {
234 let (i, m) = (Duration::from_millis(500), Duration::from_secs(5));
235 let d = |a| backoff(a, i, m, 0.25, 0.0);
236 assert_eq!(d(1), Duration::from_millis(500));
237 assert_eq!(d(2), Duration::from_secs(1));
238 assert_eq!(d(4), Duration::from_secs(4));
239 assert_eq!(d(5), Duration::from_secs(5));
240 assert_eq!(d(40), Duration::from_secs(5));
241 assert_eq!(backoff(1, i, m, 0.25, 1.0), Duration::from_millis(375));
243 assert_eq!(backoff(1, Duration::ZERO, m, 0.25, 0.5), Duration::ZERO);
244 }
245
246 #[test]
247 fn stop_rules() {
248 let p = RetryPolicy::default();
249 assert!(!p.should_stop(1, Duration::ZERO, Duration::from_secs(1)));
250 assert!(!p.should_stop(2, Duration::ZERO, Duration::from_secs(1)));
251 assert!(p.should_stop(3, Duration::ZERO, Duration::from_secs(1)));
252 assert!(p.should_stop(1, Duration::from_secs(29), Duration::from_secs(1)));
253 assert!(!RetryPolicy::default().budget(None).should_stop(
254 1,
255 Duration::from_secs(99),
256 Duration::from_secs(1)
257 ));
258 }
259
260 #[test]
261 fn default_statuses_cover_529() {
262 let p = RetryPolicy::default();
263 for s in [408, 429, 500, 503, 529, 599] {
264 assert!(p.http_statuses.contains(&StatusCode::from_u16(s).unwrap()));
265 }
266 assert!(!p.http_statuses.contains(&StatusCode::UNPROCESSABLE_ENTITY));
267 assert!(RetryPolicy::default().jitter(1.5).validate().is_err());
268 }
269}