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 self.delay_for(
105 RetryClassification::Retryable,
106 attempts_completed,
107 elapsed,
108 retry_after,
109 )
110 }
111
112 pub fn delay_for(
117 &self,
118 classification: RetryClassification,
119 attempts_completed: u32,
120 elapsed: Duration,
121 retry_after: Option<Duration>,
122 ) -> Option<Duration> {
123 if attempts_completed == 0
124 || attempts_completed >= self.max_attempts
125 || elapsed >= self.max_elapsed
126 || classification == RetryClassification::Never
127 {
128 return None;
129 }
130
131 let remaining = self.max_elapsed.saturating_sub(elapsed);
132 if let Some(delay) = retry_after {
133 return (delay <= remaining).then_some(delay);
134 }
135
136 let ceiling = self
137 .delay_ceiling(classification, attempts_completed)
138 .min(remaining);
139 let ceiling_nanos = u64::try_from(ceiling.as_nanos()).unwrap_or(u64::MAX);
140 let nanos = fastrand::u64(0..=ceiling_nanos);
141 Some(Duration::from_nanos(nanos))
142 }
143
144 fn delay_ceiling(
145 &self,
146 classification: RetryClassification,
147 attempts_completed: u32,
148 ) -> Duration {
149 let base = if classification == RetryClassification::Throttled {
150 self.base_delay.max(Duration::from_secs(1))
151 } else {
152 self.base_delay
153 };
154 let exponent = attempts_completed.saturating_sub(1).min(31);
155 let multiplier = 1_u32 << exponent;
156 base.saturating_mul(multiplier).min(self.max_delay)
157 }
158}
159
160impl Default for RetryPolicy {
161 fn default() -> Self {
162 Self::standard()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn policy_rejects_invalid_bounds() {
172 assert!(
173 RetryPolicy::new(
174 0,
175 Duration::from_millis(1),
176 Duration::from_secs(1),
177 Duration::from_secs(2)
178 )
179 .is_err()
180 );
181 assert!(
182 RetryPolicy::new(
183 2,
184 Duration::from_secs(2),
185 Duration::from_secs(1),
186 Duration::from_secs(3)
187 )
188 .is_err()
189 );
190 }
191
192 #[test]
193 fn delay_is_bounded_by_policy_and_elapsed_time() {
194 let policy = RetryPolicy::new(
195 10,
196 Duration::from_secs(1),
197 Duration::from_secs(8),
198 Duration::from_secs(10),
199 )
200 .unwrap();
201
202 assert_eq!(
203 policy.delay(1, Duration::from_secs(1), Some(Duration::from_secs(9))),
204 Some(Duration::from_secs(9))
205 );
206 assert!(
207 policy
208 .delay(9, Duration::from_secs(9), Some(Duration::from_secs(30)))
209 .is_none()
210 );
211 assert!(policy.delay(10, Duration::from_secs(9), None).is_none());
212 }
213
214 #[test]
215 fn throttling_uses_the_standard_longer_backoff_base() {
216 let policy = RetryPolicy::new(
217 4,
218 Duration::from_millis(100),
219 Duration::from_secs(10),
220 Duration::from_secs(30),
221 )
222 .unwrap();
223
224 assert_eq!(
225 policy.delay_ceiling(RetryClassification::Retryable, 1),
226 Duration::from_millis(100)
227 );
228 assert_eq!(
229 policy.delay_ceiling(RetryClassification::Throttled, 1),
230 Duration::from_secs(1)
231 );
232 }
233}