1use std::time::Duration;
2
3fn is_status_client_error(status: u16) -> bool {
4 (400..500).contains(&status) && status != 429
5}
6
7#[derive(Clone, Debug)]
10pub enum RequestStrategy {
11 Once,
13 Idempotent(IdempotencyKey),
15 Retry(u32),
19 ExponentialBackoff(u32),
23}
24
25impl RequestStrategy {
26 pub fn test(
28 &self,
29 status: Option<u16>,
33 stripe_should_retry: Option<bool>,
34 retry_count: u32,
35 ) -> Outcome {
36 use RequestStrategy::*;
37
38 match stripe_should_retry {
40 Some(false) => return Outcome::Stop,
42 Some(true) => {}
44 None => {
46 if let Some(s) = status {
47 if s != 429 {
48 return Outcome::Stop;
49 }
50 }
51 }
52 }
53
54 match (self, status, retry_count) {
55 (Once | Idempotent(_), _, 0) => Outcome::Continue(None),
57
58 (_, Some(c), _) if is_status_client_error(c) => Outcome::Stop,
62
63 (Retry(n), _, x) if x < *n => Outcome::Continue(None),
66 (ExponentialBackoff(n), _, x) if x < *n => {
67 Outcome::Continue(Some(calculate_backoff(x)))
68 }
69
70 _ => Outcome::Stop,
72 }
73 }
74
75 #[cfg(feature = "uuid")]
77 pub fn idempotent_with_uuid() -> Self {
78 Self::Idempotent(IdempotencyKey::new_uuid_v4())
79 }
80
81 pub fn get_key(&self) -> Option<IdempotencyKey> {
83 match self {
84 RequestStrategy::Once => None,
85 RequestStrategy::Idempotent(key) => Some(key.clone()),
86 #[cfg(feature = "uuid")]
87 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => {
88 Some(IdempotencyKey::new_uuid_v4())
89 }
90 #[cfg(not(feature = "uuid"))]
91 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => None,
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[repr(transparent)]
98pub struct IdempotencyKey(String);
102
103#[derive(Debug, thiserror::Error)]
104pub enum IdempotentKeyError {
106 #[error("Idempotency Key cannot be empty")]
107 EmptyKey,
109 #[error("Idempotency key cannot be longer than 255 characters (you supplied: {0})")]
110 KeyTooLong(usize),
112}
113
114impl IdempotencyKey {
115 pub fn new(val: impl AsRef<str>) -> Result<Self, IdempotentKeyError> {
121 let val = val.as_ref();
122 if val.is_empty() {
123 Err(IdempotentKeyError::EmptyKey)
124 } else if val.len() > 255 {
125 Err(IdempotentKeyError::KeyTooLong(val.len()))
126 } else {
127 Ok(Self(val.to_owned()))
128 }
129 }
130
131 #[cfg(feature = "uuid")]
132 pub fn new_uuid_v4() -> Self {
134 let uuid = uuid::Uuid::new_v4().to_string();
135 Self(uuid)
136 }
137
138 pub fn as_str(&self) -> &str {
140 &self.0
141 }
142
143 pub fn into_inner(self) -> String {
145 self.0
146 }
147}
148
149impl TryFrom<String> for IdempotencyKey {
150 type Error = IdempotentKeyError;
151
152 fn try_from(value: String) -> Result<Self, Self::Error> {
153 Self::new(value)
154 }
155}
156
157#[cfg(feature = "uuid")]
158impl From<uuid::Uuid> for IdempotencyKey {
159 #[inline]
160 fn from(value: uuid::Uuid) -> Self {
161 Self(value.to_string())
162 }
163}
164
165fn calculate_backoff(retry_count: u32) -> Duration {
166 Duration::from_secs(2_u64.pow(retry_count))
167}
168
169#[derive(PartialEq, Eq, Debug)]
171pub enum Outcome {
172 Stop,
174 Continue(Option<Duration>),
177}
178
179#[cfg(test)]
180mod tests {
181 use std::time::Duration;
182
183 use super::{Outcome, RequestStrategy};
184 use crate::IdempotencyKey;
185
186 #[test]
187 fn test_idempotent_strategy() {
188 let key: IdempotencyKey = "key".to_string().try_into().unwrap();
189 let strategy = RequestStrategy::Idempotent(key.clone());
190 assert_eq!(strategy.get_key(), Some(key));
191 }
192
193 #[test]
194 fn test_once_strategy() {
195 let strategy = RequestStrategy::Once;
196 assert_eq!(strategy.get_key(), None);
197 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
198 assert_eq!(strategy.test(None, None, 1), Outcome::Stop);
199 }
200
201 #[test]
202 #[cfg(feature = "uuid")]
203 fn test_uuid_idempotency() {
204 use uuid::Uuid;
205 let strategy = RequestStrategy::Retry(3);
206 assert!(Uuid::parse_str(strategy.get_key().unwrap().as_str()).is_ok());
207 }
208
209 #[test]
210 #[cfg(not(feature = "uuid"))]
211 fn test_uuid_idempotency() {
212 let strategy = RequestStrategy::Retry(3);
213 assert_eq!(strategy.get_key(), None);
214 }
215
216 #[test]
217 fn test_retry_strategy() {
218 let strategy = RequestStrategy::Retry(3);
219 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
220 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(None));
221 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(None));
222 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
223 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
224 }
225
226 #[test]
227 fn test_backoff_strategy() {
228 let strategy = RequestStrategy::ExponentialBackoff(3);
229 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(Some(Duration::from_secs(1))));
230 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(Some(Duration::from_secs(2))));
231 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(Some(Duration::from_secs(4))));
232 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
233 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
234 }
235
236 #[test]
237 fn test_retry_header() {
238 let strategy = RequestStrategy::Retry(3);
239 assert_eq!(strategy.test(None, Some(false), 0), Outcome::Stop);
240 }
241
242 #[test]
243 fn test_stripe_should_retry_true() {
244 let strategy = RequestStrategy::Retry(3);
245 assert_eq!(strategy.test(Some(500), Some(true), 0), Outcome::Continue(None));
247 assert_eq!(strategy.test(Some(503), Some(true), 0), Outcome::Continue(None));
248 assert_eq!(strategy.test(Some(400), Some(true), 0), Outcome::Stop);
250 assert_eq!(strategy.test(Some(404), Some(true), 0), Outcome::Stop);
251 }
252
253 #[test]
254 fn test_stripe_should_retry_false() {
255 let strategy = RequestStrategy::Retry(3);
256 assert_eq!(strategy.test(Some(429), Some(false), 0), Outcome::Stop);
258 assert_eq!(strategy.test(Some(500), Some(false), 0), Outcome::Stop);
259 assert_eq!(strategy.test(Some(200), Some(false), 0), Outcome::Stop);
260 }
261
262 #[test]
263 fn test_stripe_should_retry_absent_429() {
264 let strategy = RequestStrategy::Retry(3);
265 assert_eq!(strategy.test(Some(429), None, 0), Outcome::Continue(None));
267 assert_eq!(strategy.test(Some(429), None, 1), Outcome::Continue(None));
268 }
269
270 #[test]
271 fn test_stripe_should_retry_absent_500() {
272 let strategy = RequestStrategy::Retry(3);
273 assert_eq!(strategy.test(Some(500), None, 0), Outcome::Stop);
275 assert_eq!(strategy.test(Some(503), None, 0), Outcome::Stop);
276 }
277
278 #[test]
279 fn test_stripe_should_retry_absent_4xx() {
280 let strategy = RequestStrategy::Retry(3);
281 assert_eq!(strategy.test(Some(400), None, 0), Outcome::Stop);
283 assert_eq!(strategy.test(Some(404), None, 0), Outcome::Stop);
284 }
285
286 #[test]
287 fn test_backoff_with_stripe_should_retry() {
288 let strategy = RequestStrategy::ExponentialBackoff(3);
289 assert_eq!(
291 strategy.test(Some(500), Some(true), 0),
292 Outcome::Continue(Some(Duration::from_secs(1)))
293 );
294 assert_eq!(
295 strategy.test(Some(500), Some(true), 1),
296 Outcome::Continue(Some(Duration::from_secs(2)))
297 );
298 assert_eq!(
299 strategy.test(Some(500), Some(true), 2),
300 Outcome::Continue(Some(Duration::from_secs(4)))
301 );
302 assert_eq!(strategy.test(Some(500), Some(true), 3), Outcome::Stop);
303 }
304
305 #[test]
306 fn test_backoff_with_429_no_header() {
307 let strategy = RequestStrategy::ExponentialBackoff(3);
308 assert_eq!(
310 strategy.test(Some(429), None, 0),
311 Outcome::Continue(Some(Duration::from_secs(1)))
312 );
313 assert_eq!(
314 strategy.test(Some(429), None, 1),
315 Outcome::Continue(Some(Duration::from_secs(2)))
316 );
317 }
318}