stripe_client_core/
request_strategy.rs1use std::time::Duration;
2
3fn is_status_client_error(status: u16) -> bool {
4 (400..500).contains(&status)
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 if !stripe_should_retry.unwrap_or(true) {
38 return Outcome::Stop;
39 }
40
41 use RequestStrategy::*;
42
43 match (self, status, retry_count) {
44 (Once | Idempotent(_), _, 0) => Outcome::Continue(None),
46
47 (_, Some(c), _) if is_status_client_error(c) => Outcome::Stop,
51
52 (Retry(n), _, x) if x < *n => Outcome::Continue(None),
55 (ExponentialBackoff(n), _, x) if x < *n => {
56 Outcome::Continue(Some(calculate_backoff(x)))
57 }
58
59 _ => Outcome::Stop,
61 }
62 }
63
64 #[cfg(feature = "uuid")]
66 pub fn idempotent_with_uuid() -> Self {
67 Self::Idempotent(IdempotencyKey::new_uuid_v4())
68 }
69
70 pub fn get_key(&self) -> Option<IdempotencyKey> {
72 match self {
73 RequestStrategy::Once => None,
74 RequestStrategy::Idempotent(key) => Some(key.clone()),
75 #[cfg(feature = "uuid")]
76 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => {
77 Some(IdempotencyKey::new_uuid_v4())
78 }
79 #[cfg(not(feature = "uuid"))]
80 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => None,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86#[repr(transparent)]
87pub struct IdempotencyKey(String);
91
92#[derive(Debug, thiserror::Error)]
93pub enum IdempotentKeyError {
95 #[error("Idempotency Key cannot be empty")]
96 EmptyKey,
98 #[error("Idempotency key cannot be longer than 255 characters (you supplied: {0})")]
99 KeyTooLong(usize),
101}
102
103impl IdempotencyKey {
104 pub fn new(val: impl AsRef<str>) -> Result<Self, IdempotentKeyError> {
110 let val = val.as_ref();
111 if val.is_empty() {
112 Err(IdempotentKeyError::EmptyKey)
113 } else if val.len() > 255 {
114 Err(IdempotentKeyError::KeyTooLong(val.len()))
115 } else {
116 Ok(Self(val.to_owned()))
117 }
118 }
119
120 #[cfg(feature = "uuid")]
121 pub fn new_uuid_v4() -> Self {
123 let uuid = uuid::Uuid::new_v4().to_string();
124 Self(uuid)
125 }
126
127 pub fn as_str(&self) -> &str {
129 &self.0
130 }
131
132 pub fn into_inner(self) -> String {
134 self.0
135 }
136}
137
138impl TryFrom<String> for IdempotencyKey {
139 type Error = IdempotentKeyError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::new(value)
143 }
144}
145
146fn calculate_backoff(retry_count: u32) -> Duration {
147 Duration::from_secs(2_u64.pow(retry_count))
148}
149
150#[derive(PartialEq, Eq, Debug)]
152pub enum Outcome {
153 Stop,
155 Continue(Option<Duration>),
158}
159
160#[cfg(test)]
161mod tests {
162 use std::time::Duration;
163
164 use super::{Outcome, RequestStrategy};
165 use crate::IdempotencyKey;
166
167 #[test]
168 fn test_idempotent_strategy() {
169 let key: IdempotencyKey = "key".to_string().try_into().unwrap();
170 let strategy = RequestStrategy::Idempotent(key.clone());
171 assert_eq!(strategy.get_key(), Some(key));
172 }
173
174 #[test]
175 fn test_once_strategy() {
176 let strategy = RequestStrategy::Once;
177 assert_eq!(strategy.get_key(), None);
178 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
179 assert_eq!(strategy.test(None, None, 1), Outcome::Stop);
180 }
181
182 #[test]
183 #[cfg(feature = "uuid")]
184 fn test_uuid_idempotency() {
185 use uuid::Uuid;
186 let strategy = RequestStrategy::Retry(3);
187 assert!(Uuid::parse_str(&strategy.get_key().unwrap().as_str()).is_ok());
188 }
189
190 #[test]
191 #[cfg(not(feature = "uuid"))]
192 fn test_uuid_idempotency() {
193 let strategy = RequestStrategy::Retry(3);
194 assert_eq!(strategy.get_key(), None);
195 }
196
197 #[test]
198 fn test_retry_strategy() {
199 let strategy = RequestStrategy::Retry(3);
200 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
201 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(None));
202 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(None));
203 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
204 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
205 }
206
207 #[test]
208 fn test_backoff_strategy() {
209 let strategy = RequestStrategy::ExponentialBackoff(3);
210 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(Some(Duration::from_secs(1))));
211 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(Some(Duration::from_secs(2))));
212 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(Some(Duration::from_secs(4))));
213 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
214 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
215 }
216
217 #[test]
218 fn test_retry_header() {
219 let strategy = RequestStrategy::Retry(3);
220 assert_eq!(strategy.test(None, Some(false), 0), Outcome::Stop);
221 }
222}