payjp_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 Retry(u32),
17 ExponentialBackoff(u32),
21}
22
23impl RequestStrategy {
24 pub fn test(
26 &self,
27 status: Option<u16>,
31 retry_count: u32,
32 ) -> Outcome {
33 use RequestStrategy::*;
34
35 match (self, status, retry_count) {
36 (Once, _, 0) => Outcome::Continue(None),
38
39 (_, Some(c), _) if is_status_client_error(c) => Outcome::Stop,
42
43 (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 _ => Outcome::Stop,
52 }
53 }
54
55 pub fn get_key(&self) -> Option<String> {
57 match self {
58 RequestStrategy::Once => None,
59 #[cfg(feature = "uuid")]
60 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => {
61 Some(uuid::Uuid::new_v4().to_string())
62 }
63 #[cfg(not(feature = "uuid"))]
64 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => None,
65 }
66 }
67}
68
69fn calculate_backoff(retry_count: u32) -> Duration {
70 Duration::from_secs(2_u64.pow(retry_count))
71}
72
73#[derive(PartialEq, Eq, Debug)]
75pub enum Outcome {
76 Stop,
78 Continue(Option<Duration>),
81}
82
83#[cfg(test)]
84mod tests {
85 use std::time::Duration;
86
87 use super::{Outcome, RequestStrategy};
88
89 #[test]
90 fn test_once_strategy() {
91 let strategy = RequestStrategy::Once;
92 assert_eq!(strategy.get_key(), None);
93 assert_eq!(strategy.test(None, 0), Outcome::Continue(None));
94 assert_eq!(strategy.test(None, 1), Outcome::Stop);
95 }
96
97 #[test]
98 #[cfg(feature = "uuid")]
99 fn test_uuid_idempotency() {
100 use uuid::Uuid;
101 let strategy = RequestStrategy::Retry(3);
102 assert!(Uuid::parse_str(&strategy.get_key().unwrap()).is_ok());
103 }
104
105 #[test]
106 #[cfg(not(feature = "uuid"))]
107 fn test_uuid_idempotency() {
108 let strategy = RequestStrategy::Retry(3);
109 assert_eq!(strategy.get_key(), None);
110 }
111
112 #[test]
113 fn test_retry_strategy() {
114 let strategy = RequestStrategy::Retry(3);
115 assert_eq!(strategy.test(None, 0), Outcome::Continue(None));
116 assert_eq!(strategy.test(None, 1), Outcome::Continue(None));
117 assert_eq!(strategy.test(None, 2), Outcome::Continue(None));
118 assert_eq!(strategy.test(None, 3), Outcome::Stop);
119 assert_eq!(strategy.test(None, 4), Outcome::Stop);
120 }
121
122 #[test]
123 fn test_backoff_strategy() {
124 let strategy = RequestStrategy::ExponentialBackoff(3);
125 assert_eq!(strategy.test(None, 0), Outcome::Continue(Some(Duration::from_secs(1))));
126 assert_eq!(strategy.test(None, 1), Outcome::Continue(Some(Duration::from_secs(2))));
127 assert_eq!(strategy.test(None, 2), Outcome::Continue(Some(Duration::from_secs(4))));
128 assert_eq!(strategy.test(None, 3), Outcome::Stop);
129 assert_eq!(strategy.test(None, 4), Outcome::Stop);
130 }
131
132 #[test]
133 fn test_retry_header() {
134 let strategy = RequestStrategy::Retry(3);
135 assert_eq!(strategy.test(None, 0), Outcome::Stop);
136 }
137}