1use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum RetryMechanism {
14 Exponential {
17 base_delay: Duration,
18 max_delay: Duration,
19 },
20
21 Linear { delay: Duration },
24
25 Custom { delays: Vec<Duration> },
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RetryConfig {
33 pub max_retries: u32,
35
36 pub mechanism: RetryMechanism,
38
39 pub dead_letter_exchange: Option<String>,
42
43 pub dead_letter_queue: Option<String>,
46}
47
48impl RetryConfig {
49 pub fn exponential_default() -> Self {
51 Self {
52 max_retries: 5,
53 mechanism: RetryMechanism::Exponential {
54 base_delay: Duration::from_secs(1),
55 max_delay: Duration::from_secs(60),
56 },
57 dead_letter_exchange: None,
58 dead_letter_queue: None,
59 }
60 }
61
62 pub fn exponential(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
64 Self {
65 max_retries,
66 mechanism: RetryMechanism::Exponential {
67 base_delay,
68 max_delay,
69 },
70 dead_letter_exchange: None,
71 dead_letter_queue: None,
72 }
73 }
74
75 pub fn linear(max_retries: u32, delay: Duration) -> Self {
77 Self {
78 max_retries,
79 mechanism: RetryMechanism::Linear { delay },
80 dead_letter_exchange: None,
81 dead_letter_queue: None,
82 }
83 }
84
85 pub fn custom(delays: Vec<Duration>) -> Self {
87 let max_retries = delays.len() as u32;
88 Self {
89 max_retries,
90 mechanism: RetryMechanism::Custom { delays },
91 dead_letter_exchange: None,
92 dead_letter_queue: None,
93 }
94 }
95
96 pub fn no_retry() -> Self {
98 Self {
99 max_retries: 0,
100 mechanism: RetryMechanism::Linear {
101 delay: Duration::from_secs(0),
102 },
103 dead_letter_exchange: None,
104 dead_letter_queue: None,
105 }
106 }
107
108 pub fn with_dead_letter(mut self, exchange: String, queue: String) -> Self {
110 self.dead_letter_exchange = Some(exchange);
111 self.dead_letter_queue = Some(queue);
112 self
113 }
114
115 pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
117 if attempt >= self.max_retries {
118 return None; }
120
121 let delay = match &self.mechanism {
122 RetryMechanism::Exponential {
123 base_delay,
124 max_delay,
125 } => {
126 let exponential_delay =
127 Duration::from_millis(base_delay.as_millis() as u64 * 2_u64.pow(attempt));
128 std::cmp::min(exponential_delay, *max_delay)
129 }
130 RetryMechanism::Linear { delay } => *delay,
131 RetryMechanism::Custom { delays } => {
132 if (attempt as usize) < delays.len() {
133 delays[attempt as usize]
134 } else {
135 return None; }
137 }
138 };
139
140 Some(delay)
141 }
142
143 pub fn get_dead_letter_exchange(&self, queue_name: &str) -> String {
145 self.dead_letter_exchange
146 .clone()
147 .unwrap_or_else(|| format!("{}.dlx", queue_name))
148 }
149
150 pub fn get_dead_letter_queue(&self, queue_name: &str) -> String {
152 self.dead_letter_queue
153 .clone()
154 .unwrap_or_else(|| format!("{}.dlq", queue_name))
155 }
156
157 pub fn get_retry_queue_name(&self, queue_name: &str, attempt: u32) -> String {
159 format!("{}.retry.{}", queue_name, attempt + 1)
160 }
161}
162
163impl Default for RetryConfig {
164 fn default() -> Self {
165 Self::exponential_default()
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_exponential_retry() {
175 let config = RetryConfig::exponential(5, Duration::from_secs(1), Duration::from_secs(30));
176
177 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1))); assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(2))); assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(4))); assert_eq!(config.calculate_delay(3), Some(Duration::from_secs(8))); assert_eq!(config.calculate_delay(4), Some(Duration::from_secs(16))); assert_eq!(config.calculate_delay(5), None); }
184
185 #[test]
186 fn test_exponential_retry_with_cap() {
187 let config = RetryConfig::exponential(10, Duration::from_secs(1), Duration::from_secs(5));
188
189 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1))); assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(2))); assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(4))); assert_eq!(config.calculate_delay(3), Some(Duration::from_secs(5))); assert_eq!(config.calculate_delay(4), Some(Duration::from_secs(5))); }
195
196 #[test]
197 fn test_linear_retry() {
198 let config = RetryConfig::linear(3, Duration::from_secs(5));
199
200 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(5)));
201 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
202 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(5)));
203 assert_eq!(config.calculate_delay(3), None); }
205
206 #[test]
207 fn test_custom_retry() {
208 let delays = vec![
209 Duration::from_secs(1),
210 Duration::from_secs(5),
211 Duration::from_secs(30),
212 ];
213 let config = RetryConfig::custom(delays);
214
215 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1)));
216 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
217 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(30)));
218 assert_eq!(config.calculate_delay(3), None); }
220
221 #[test]
222 fn test_no_retry() {
223 let config = RetryConfig::no_retry();
224
225 assert_eq!(config.calculate_delay(0), None); }
227
228 #[test]
229 fn test_dead_letter_names() {
230 let config = RetryConfig::exponential_default();
231
232 assert_eq!(config.get_dead_letter_exchange("orders"), "orders.dlx");
233 assert_eq!(config.get_dead_letter_queue("orders"), "orders.dlq");
234
235 let config_custom =
236 config.with_dead_letter("custom.dlx".to_string(), "custom.dlq".to_string());
237 assert_eq!(
238 config_custom.get_dead_letter_exchange("orders"),
239 "custom.dlx"
240 );
241 assert_eq!(config_custom.get_dead_letter_queue("orders"), "custom.dlq");
242 }
243
244 #[test]
245 fn test_retry_queue_names() {
246 let config = RetryConfig::exponential_default();
247
248 assert_eq!(config.get_retry_queue_name("orders", 0), "orders.retry.1");
249 assert_eq!(config.get_retry_queue_name("orders", 1), "orders.retry.2");
250 assert_eq!(config.get_retry_queue_name("orders", 4), "orders.retry.5");
251 }
252}