stripe/client/
request_strategy.rs

1use std::time::Duration;
2
3use http_types::StatusCode;
4
5#[derive(Clone, Debug)]
6pub enum RequestStrategy {
7    Once,
8    /// Run it once with a given idempotency key.
9    Idempotent(String),
10    /// This strategy will retry the request up to the
11    /// specified number of times using the same, random,
12    /// idempotency key, up to n times.
13    Retry(u32),
14    /// This strategy will retry the request up to the
15    /// specified number of times using the same, random,
16    /// idempotency key with exponential backoff, up to n times.
17    ExponentialBackoff(u32),
18}
19
20impl RequestStrategy {
21    pub fn test(
22        &self,
23        status: Option<StatusCode>,
24        stripe_should_retry: Option<bool>,
25        retry_count: u32,
26    ) -> Outcome {
27        // if stripe explicitly says not to retry then don't
28        if !stripe_should_retry.unwrap_or(true) {
29            return Outcome::Stop;
30        }
31
32        use RequestStrategy::*;
33
34        match (self, status, retry_count) {
35            // a strategy of once or idempotent should run once
36            (Once | Idempotent(_), _, 0) => Outcome::Continue(None),
37
38            // requests with idempotency keys that hit client
39            // errors usually cannot be solved with retries
40            // see: https://stripe.com/docs/error-handling#content-errors
41            (_, Some(c), _) if c.is_client_error() => Outcome::Stop,
42
43            // a strategy of retry or exponential backoff should retry with
44            // the appropriate delay if the number of retries is less than the max
45            (Retry(n), _, x) if x < *n => Outcome::Continue(None),
46            (ExponentialBackoff(n), _, x) if x < *n => {
47                Outcome::Continue(Some(calculate_backoff(x)))
48            }
49
50            // unknown cases should be stopped to prevent infinite loops
51            _ => Outcome::Stop,
52        }
53    }
54
55    #[cfg(feature = "uuid")]
56    pub fn idempotent_with_uuid() -> Self {
57        use uuid::Uuid;
58        Self::Idempotent(Uuid::new_v4().to_string())
59    }
60
61    pub fn get_key(&self) -> Option<String> {
62        match self {
63            RequestStrategy::Once => None,
64            RequestStrategy::Idempotent(key) => Some(key.clone()),
65            #[cfg(feature = "uuid")]
66            RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => {
67                Some(uuid::Uuid::new_v4().to_string())
68            }
69            #[cfg(not(feature = "uuid"))]
70            RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => None,
71        }
72    }
73}
74
75fn calculate_backoff(retry_count: u32) -> Duration {
76    Duration::from_secs(2_u64.pow(retry_count))
77}
78
79#[derive(PartialEq, Eq, Debug)]
80pub enum Outcome {
81    Stop,
82    Continue(Option<Duration>),
83}
84
85#[cfg(test)]
86mod tests {
87    use std::time::Duration;
88
89    use super::{Outcome, RequestStrategy};
90
91    #[test]
92    fn test_idempotent_strategy() {
93        let strategy = RequestStrategy::Idempotent("key".to_string());
94        assert_eq!(strategy.get_key(), Some("key".to_string()));
95    }
96
97    #[test]
98    fn test_once_strategy() {
99        let strategy = RequestStrategy::Once;
100        assert_eq!(strategy.get_key(), None);
101        assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
102        assert_eq!(strategy.test(None, None, 1), Outcome::Stop);
103    }
104
105    #[test]
106    #[cfg(feature = "uuid")]
107    fn test_uuid_idempotency() {
108        use uuid::Uuid;
109        let strategy = RequestStrategy::Retry(3);
110        assert!(Uuid::parse_str(&strategy.get_key().unwrap()).is_ok());
111    }
112
113    #[test]
114    #[cfg(not(feature = "uuid"))]
115    fn test_uuid_idempotency() {
116        let strategy = RequestStrategy::Retry(3);
117        assert_eq!(strategy.get_key(), None);
118    }
119
120    #[test]
121    fn test_retry_strategy() {
122        let strategy = RequestStrategy::Retry(3);
123        assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
124        assert_eq!(strategy.test(None, None, 1), Outcome::Continue(None));
125        assert_eq!(strategy.test(None, None, 2), Outcome::Continue(None));
126        assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
127        assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
128    }
129
130    #[test]
131    fn test_backoff_strategy() {
132        let strategy = RequestStrategy::ExponentialBackoff(3);
133        assert_eq!(strategy.test(None, None, 0), Outcome::Continue(Some(Duration::from_secs(1))));
134        assert_eq!(strategy.test(None, None, 1), Outcome::Continue(Some(Duration::from_secs(2))));
135        assert_eq!(strategy.test(None, None, 2), Outcome::Continue(Some(Duration::from_secs(4))));
136        assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
137        assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
138    }
139
140    #[test]
141    fn test_retry_header() {
142        let strategy = RequestStrategy::Retry(3);
143        assert_eq!(strategy.test(None, Some(false), 0), Outcome::Stop);
144    }
145}