1use crate::EntryRetryInfo;
2use std::cmp;
3use std::time::Duration;
4
5#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
7pub enum OnMaxAttempts {
8 #[default]
10 FailAsTerminal,
11 Pause,
14}
15
16#[derive(Debug, Clone, Default)]
18pub enum RetryPolicy {
19 #[default]
23 Infinite,
24 None,
28 FixedDelay {
32 interval: Option<Duration>,
36
37 max_attempts: Option<u32>,
43
44 max_duration: Option<Duration>,
50
51 on_max_attempts: OnMaxAttempts,
55 },
56 Exponential {
60 initial_interval: Duration,
64
65 factor: f32,
69
70 max_interval: Option<Duration>,
74
75 max_attempts: Option<u32>,
81
82 max_duration: Option<Duration>,
88
89 on_max_attempts: OnMaxAttempts,
93 },
94}
95
96#[derive(Debug, Clone, Eq, PartialEq)]
97pub(crate) enum NextRetry {
98 Retry(Option<Duration>),
99 FailAsTerminal,
100 Pause,
101}
102
103impl RetryPolicy {
104 pub fn fixed_delay(
105 interval: Option<Duration>,
106 max_attempts: Option<u32>,
107 max_duration: Option<Duration>,
108 on_max_attempts: OnMaxAttempts,
109 ) -> Self {
110 Self::FixedDelay {
111 interval,
112 max_attempts,
113 max_duration,
114 on_max_attempts,
115 }
116 }
117
118 pub fn exponential(
119 initial_interval: Duration,
120 factor: f32,
121 max_attempts: Option<u32>,
122 max_interval: Option<Duration>,
123 max_duration: Option<Duration>,
124 on_max_attempts: OnMaxAttempts,
125 ) -> Self {
126 Self::Exponential {
127 initial_interval,
128 factor,
129 max_attempts,
130 max_interval,
131 max_duration,
132 on_max_attempts,
133 }
134 }
135
136 pub(crate) fn should_pause_on_max_attempts(&self) -> bool {
137 matches!(
138 self,
139 RetryPolicy::FixedDelay {
140 on_max_attempts: OnMaxAttempts::Pause,
141 ..
142 } | RetryPolicy::Exponential {
143 on_max_attempts: OnMaxAttempts::Pause,
144 ..
145 }
146 )
147 }
148
149 pub(crate) fn next_retry(&self, retry_info: EntryRetryInfo) -> NextRetry {
150 match self {
151 RetryPolicy::Infinite => NextRetry::Retry(None),
152 RetryPolicy::None => NextRetry::FailAsTerminal,
153 RetryPolicy::FixedDelay {
154 interval,
155 max_attempts,
156 max_duration,
157 on_max_attempts,
158 } => {
159 if max_attempts.is_some_and(|max_attempts| max_attempts <= retry_info.retry_count)
160 || max_duration
161 .is_some_and(|max_duration| max_duration <= retry_info.retry_loop_duration)
162 {
163 return match on_max_attempts {
165 OnMaxAttempts::FailAsTerminal => NextRetry::FailAsTerminal,
166 OnMaxAttempts::Pause => NextRetry::Pause,
167 };
168 }
169
170 NextRetry::Retry(*interval)
172 }
173 RetryPolicy::Exponential {
174 initial_interval,
175 factor,
176 max_interval,
177 max_attempts,
178 max_duration,
179 on_max_attempts,
180 } => {
181 if max_attempts.is_some_and(|max_attempts| max_attempts <= retry_info.retry_count)
182 || max_duration
183 .is_some_and(|max_duration| max_duration <= retry_info.retry_loop_duration)
184 {
185 return match on_max_attempts {
187 OnMaxAttempts::FailAsTerminal => NextRetry::FailAsTerminal,
188 OnMaxAttempts::Pause => NextRetry::Pause,
189 };
190 }
191
192 let max_interval = max_interval.unwrap_or(Duration::MAX);
193
194 let exponent =
198 i32::try_from(retry_info.retry_count.saturating_sub(1)).unwrap_or(i32::MAX);
199 let Ok(next_interval) = Duration::try_from_secs_f32(
200 initial_interval.as_secs_f32() * factor.powi(exponent),
201 ) else {
202 return NextRetry::Retry(Some(max_interval));
204 };
205
206 NextRetry::Retry(Some(cmp::min(max_interval, next_interval)))
207 }
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use googletest::prelude::*;
216 use rstest::rstest;
217
218 #[test]
220 fn exponential_policy_does_not_panic_on_overflow() {
221 let policy = RetryPolicy::Exponential {
222 initial_interval: Duration::from_secs(1),
223 factor: 2.0,
224 max_interval: None,
225 max_attempts: None,
226 max_duration: None,
227 on_max_attempts: OnMaxAttempts::FailAsTerminal,
228 };
229
230 for retry_count in 1..=200 {
233 assert_that!(
234 policy.next_retry(EntryRetryInfo {
235 retry_count,
236 retry_loop_duration: Duration::ZERO,
237 }),
238 pat!(NextRetry::Retry(some(le(Duration::MAX)))),
239 "retry_count={retry_count}"
240 );
241 }
242 }
243
244 #[rstest]
245 #[case::first_retry_uses_initial(Duration::from_secs(1), 2.0, None, 1, Duration::from_secs(1))]
247 #[case::in_range_grows_by_factor(Duration::from_secs(1), 2.0, None, 3, Duration::from_secs(4))]
248 #[case::overflow_boundary_unbounded(Duration::from_secs(1), 2.0, None, 70, Duration::MAX)]
250 #[case::large_retry_count_unbounded(Duration::from_secs(1), 2.0, None, 128, Duration::MAX)]
251 #[case::max_retry_count_unbounded(Duration::from_secs(1), 2.0, None, u32::MAX, Duration::MAX)]
252 #[case::overflow_boundary_bounded(
254 Duration::from_secs(1),
255 2.0,
256 Some(Duration::from_secs(30)),
257 70,
258 Duration::from_secs(30)
259 )]
260 #[case::large_retry_count_bounded(
261 Duration::from_secs(1),
262 2.0,
263 Some(Duration::from_secs(30)),
264 128,
265 Duration::from_secs(30)
266 )]
267 #[case::max_retry_count_bounded(
268 Duration::from_secs(1),
269 2.0,
270 Some(Duration::from_secs(30)),
271 u32::MAX,
272 Duration::from_secs(30)
273 )]
274 #[case::factor_one_never_grows(Duration::from_secs(2), 1.0, None, 1000, Duration::from_secs(2))]
276 #[case::huge_factor_bounded(
278 Duration::from_secs(1),
279 1e30,
280 Some(Duration::from_secs(30)),
281 5,
282 Duration::from_secs(30)
283 )]
284 #[case::huge_factor_unbounded(Duration::from_secs(1), 1e30, None, 5, Duration::MAX)]
285 #[case::nan_factor_bounded(
286 Duration::from_secs(1),
287 f32::NAN,
288 Some(Duration::from_secs(30)),
289 5,
290 Duration::from_secs(30)
291 )]
292 #[case::infinite_factor_unbounded(
293 Duration::from_secs(1),
294 f32::INFINITY,
295 None,
296 5,
297 Duration::MAX
298 )]
299 fn exponential_policy_saturation(
300 #[case] initial_interval: Duration,
301 #[case] factor: f32,
302 #[case] max_interval: Option<Duration>,
303 #[case] retry_count: u32,
304 #[case] expected: Duration,
305 ) {
306 let policy = RetryPolicy::Exponential {
307 initial_interval,
308 factor,
309 max_interval,
310 max_attempts: None,
311 max_duration: None,
312 on_max_attempts: OnMaxAttempts::FailAsTerminal,
313 };
314
315 assert_eq!(
316 policy.next_retry(EntryRetryInfo {
317 retry_count,
318 retry_loop_duration: Duration::ZERO,
319 }),
320 NextRetry::Retry(Some(expected))
321 );
322 }
323
324 #[test]
325 fn test_exponential_policy() {
326 let policy = RetryPolicy::Exponential {
329 initial_interval: Duration::from_millis(125),
330 factor: 2.0,
331 max_interval: Some(Duration::from_millis(750)),
332 max_attempts: None,
333 max_duration: Some(Duration::from_secs(10)),
334 on_max_attempts: OnMaxAttempts::FailAsTerminal,
335 };
336
337 assert_eq!(
339 policy.next_retry(EntryRetryInfo {
340 retry_count: 2,
341 retry_loop_duration: Duration::from_secs(1)
342 }),
343 NextRetry::Retry(Some(Duration::from_millis(250)))
344 );
345 assert_eq!(
347 policy.next_retry(EntryRetryInfo {
348 retry_count: 3,
349 retry_loop_duration: Duration::from_secs(1)
350 }),
351 NextRetry::Retry(Some(Duration::from_millis(500)))
352 );
353 assert_eq!(
355 policy.next_retry(EntryRetryInfo {
356 retry_count: 4,
357 retry_loop_duration: Duration::from_secs(1)
358 }),
359 NextRetry::Retry(Some(Duration::from_millis(750)))
360 );
361 assert_eq!(
362 policy.next_retry(EntryRetryInfo {
363 retry_count: 4,
364 retry_loop_duration: Duration::from_secs(10)
365 }),
366 NextRetry::FailAsTerminal
367 );
368 }
369}