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