1use std::time::Duration;
4
5use crate::error::{RetryClassification, S3Error};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct RetryPolicy {
10 max_attempts: u32,
11 base_delay: Duration,
12 max_delay: Duration,
13 max_elapsed: Duration,
14}
15
16impl RetryPolicy {
17 pub fn standard() -> Self {
19 Self {
20 max_attempts: 4,
21 base_delay: Duration::from_millis(100),
22 max_delay: Duration::from_secs(10),
23 max_elapsed: Duration::from_secs(30),
24 }
25 }
26
27 pub fn new(
29 max_attempts: u32,
30 base_delay: Duration,
31 max_delay: Duration,
32 max_elapsed: Duration,
33 ) -> Result<Self, S3Error> {
34 if max_attempts == 0 {
35 return Err(S3Error::configuration(
36 "retry max_attempts must be at least one",
37 ));
38 }
39 if base_delay.is_zero() {
40 return Err(S3Error::configuration(
41 "retry base_delay must be greater than zero",
42 ));
43 }
44 if max_delay < base_delay {
45 return Err(S3Error::configuration(
46 "retry max_delay must not be shorter than base_delay",
47 ));
48 }
49 if max_elapsed.is_zero() {
50 return Err(S3Error::configuration(
51 "retry max_elapsed must be greater than zero",
52 ));
53 }
54 Ok(Self {
55 max_attempts,
56 base_delay,
57 max_delay,
58 max_elapsed,
59 })
60 }
61
62 pub fn max_attempts(&self) -> u32 {
64 self.max_attempts
65 }
66
67 pub fn base_delay(&self) -> Duration {
69 self.base_delay
70 }
71
72 pub fn max_delay(&self) -> Duration {
74 self.max_delay
75 }
76
77 pub fn max_elapsed(&self) -> Duration {
79 self.max_elapsed
80 }
81
82 pub fn permits_retry(
84 &self,
85 classification: RetryClassification,
86 attempts_completed: u32,
87 elapsed: Duration,
88 ) -> bool {
89 classification != RetryClassification::Never
90 && attempts_completed < self.max_attempts
91 && elapsed < self.max_elapsed
92 }
93
94 pub fn delay(
99 &self,
100 attempts_completed: u32,
101 elapsed: Duration,
102 retry_after: Option<Duration>,
103 ) -> Option<Duration> {
104 if attempts_completed == 0
105 || attempts_completed >= self.max_attempts
106 || elapsed >= self.max_elapsed
107 {
108 return None;
109 }
110
111 let remaining = self.max_elapsed.saturating_sub(elapsed);
112 if let Some(delay) = retry_after {
113 return (delay <= remaining).then_some(delay);
114 }
115
116 let exponent = attempts_completed.saturating_sub(1).min(31);
117 let multiplier = 1_u32 << exponent;
118 let ceiling = self.base_delay.saturating_mul(multiplier);
119 let ceiling = ceiling.min(self.max_delay).min(remaining);
120 let ceiling_nanos = u64::try_from(ceiling.as_nanos()).unwrap_or(u64::MAX);
121 let nanos = fastrand::u64(0..=ceiling_nanos);
122 Some(Duration::from_nanos(nanos))
123 }
124}
125
126impl Default for RetryPolicy {
127 fn default() -> Self {
128 Self::standard()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn policy_rejects_invalid_bounds() {
138 assert!(
139 RetryPolicy::new(
140 0,
141 Duration::from_millis(1),
142 Duration::from_secs(1),
143 Duration::from_secs(2)
144 )
145 .is_err()
146 );
147 assert!(
148 RetryPolicy::new(
149 2,
150 Duration::from_secs(2),
151 Duration::from_secs(1),
152 Duration::from_secs(3)
153 )
154 .is_err()
155 );
156 }
157
158 #[test]
159 fn delay_is_bounded_by_policy_and_elapsed_time() {
160 let policy = RetryPolicy::new(
161 10,
162 Duration::from_secs(1),
163 Duration::from_secs(8),
164 Duration::from_secs(10),
165 )
166 .unwrap();
167
168 assert_eq!(
169 policy.delay(1, Duration::from_secs(1), Some(Duration::from_secs(9))),
170 Some(Duration::from_secs(9))
171 );
172 assert!(
173 policy
174 .delay(9, Duration::from_secs(9), Some(Duration::from_secs(30)))
175 .is_none()
176 );
177 assert!(policy.delay(10, Duration::from_secs(9), None).is_none());
178 }
179}