1use std::collections::HashSet;
4use std::time::Duration;
5
6#[derive(Debug, Clone)]
8pub struct RetryPolicy {
9 pub max_retries: u32,
11 pub backoff_base: Duration,
13 pub backoff_max: Duration,
15 pub retryable_status_codes: HashSet<u16>,
17 pub retry_on_connection_error: bool,
19 pub retry_on_timeout: bool,
21}
22
23impl RetryPolicy {
24 pub fn new(max_retries: u32) -> Self {
26 let mut retryable = HashSet::new();
27 retryable.insert(429); retryable.insert(500); retryable.insert(502); retryable.insert(503); retryable.insert(504); Self {
35 max_retries,
36 backoff_base: Duration::from_millis(100),
37 backoff_max: Duration::from_secs(30),
38 retryable_status_codes: retryable,
39 retry_on_connection_error: true,
40 retry_on_timeout: true,
41 }
42 }
43
44 pub fn with_backoff_base(mut self, base: Duration) -> Self {
46 self.backoff_base = base;
47 self
48 }
49
50 pub fn with_backoff_max(mut self, max: Duration) -> Self {
52 self.backoff_max = max;
53 self
54 }
55
56 pub fn with_retryable_status_codes(mut self, codes: HashSet<u16>) -> Self {
58 self.retryable_status_codes = codes;
59 self
60 }
61
62 pub fn add_retryable_status(mut self, code: u16) -> Self {
64 self.retryable_status_codes.insert(code);
65 self
66 }
67
68 pub fn with_retry_on_connection_error(mut self, retry: bool) -> Self {
70 self.retry_on_connection_error = retry;
71 self
72 }
73
74 pub fn with_retry_on_timeout(mut self, retry: bool) -> Self {
76 self.retry_on_timeout = retry;
77 self
78 }
79
80 pub fn should_retry_status(&self, status_code: u16) -> bool {
82 self.retryable_status_codes.contains(&status_code)
83 }
84
85 pub fn backoff_delay(&self, attempt: u32) -> Duration {
87 let delay = self
88 .backoff_base
89 .saturating_mul(2u32.saturating_pow(attempt));
90 std::cmp::min(delay, self.backoff_max)
91 }
92}
93
94impl Default for RetryPolicy {
95 fn default() -> Self {
96 Self::new(3)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_default_retry_policy() {
106 let policy = RetryPolicy::default();
107 assert_eq!(policy.max_retries, 3);
108 assert!(policy.should_retry_status(503));
109 assert!(policy.should_retry_status(429));
110 assert!(!policy.should_retry_status(404));
111 }
112
113 #[test]
114 fn test_backoff_delay() {
115 let policy = RetryPolicy::new(5).with_backoff_base(Duration::from_millis(100));
116 assert_eq!(policy.backoff_delay(0), Duration::from_millis(100));
117 assert_eq!(policy.backoff_delay(1), Duration::from_millis(200));
118 assert_eq!(policy.backoff_delay(2), Duration::from_millis(400));
119 assert_eq!(policy.backoff_delay(3), Duration::from_millis(800));
120 }
121
122 #[test]
123 fn test_backoff_delay_capped() {
124 let policy = RetryPolicy::new(5)
125 .with_backoff_base(Duration::from_secs(1))
126 .with_backoff_max(Duration::from_secs(5));
127 assert_eq!(policy.backoff_delay(0), Duration::from_secs(1));
128 assert_eq!(policy.backoff_delay(1), Duration::from_secs(2));
129 assert_eq!(policy.backoff_delay(2), Duration::from_secs(4));
130 assert_eq!(policy.backoff_delay(3), Duration::from_secs(5)); }
132
133 #[test]
134 fn test_custom_retryable_status() {
135 let policy = RetryPolicy::new(1).add_retryable_status(418);
136 assert!(policy.should_retry_status(418));
137 assert!(policy.should_retry_status(503)); }
139}