1use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
41pub enum DelayStrategy {
42 #[default]
44 TTL,
45 DelayedExchange,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum RetryMechanism {
60 Exponential {
63 base_delay: Duration,
64 max_delay: Duration,
65 },
66
67 Linear { delay: Duration },
70
71 Custom { delays: Vec<Duration> },
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct RetryConfig {
79 pub max_retries: u32,
81
82 pub mechanism: RetryMechanism,
84
85 #[serde(default)]
87 pub delay_strategy: DelayStrategy,
88
89 pub dead_letter_exchange: Option<String>,
92
93 pub dead_letter_queue: Option<String>,
96
97 pub dlq_ttl: Option<Duration>,
101}
102
103impl RetryConfig {
104 pub fn exponential_default() -> Self {
106 Self {
107 max_retries: 5,
108 mechanism: RetryMechanism::Exponential {
109 base_delay: Duration::from_secs(1),
110 max_delay: Duration::from_secs(60),
111 },
112 delay_strategy: DelayStrategy::default(),
113 dead_letter_exchange: None,
114 dead_letter_queue: None,
115 dlq_ttl: None,
116 }
117 }
118
119 pub fn exponential(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
121 Self {
122 max_retries,
123 mechanism: RetryMechanism::Exponential {
124 base_delay,
125 max_delay,
126 },
127 delay_strategy: DelayStrategy::default(),
128 dead_letter_exchange: None,
129 dead_letter_queue: None,
130 dlq_ttl: None,
131 }
132 }
133
134 pub fn linear(max_retries: u32, delay: Duration) -> Self {
136 Self {
137 max_retries,
138 mechanism: RetryMechanism::Linear { delay },
139 delay_strategy: DelayStrategy::default(),
140 dead_letter_exchange: None,
141 dead_letter_queue: None,
142 dlq_ttl: None,
143 }
144 }
145
146 pub fn custom(delays: Vec<Duration>) -> Self {
148 let max_retries = delays.len() as u32;
149 Self {
150 max_retries,
151 mechanism: RetryMechanism::Custom { delays },
152 delay_strategy: DelayStrategy::default(),
153 dead_letter_exchange: None,
154 dead_letter_queue: None,
155 dlq_ttl: None,
156 }
157 }
158
159 pub fn no_retry() -> Self {
161 Self {
162 max_retries: 0,
163 mechanism: RetryMechanism::Linear {
164 delay: Duration::from_secs(0),
165 },
166 delay_strategy: DelayStrategy::default(),
167 dead_letter_exchange: None,
168 dead_letter_queue: None,
169 dlq_ttl: None,
170 }
171 }
172
173 pub fn with_dead_letter(mut self, exchange: String, queue: String) -> Self {
175 self.dead_letter_exchange = Some(exchange);
176 self.dead_letter_queue = Some(queue);
177 self
178 }
179
180 pub fn with_delay_strategy(mut self, strategy: DelayStrategy) -> Self {
182 self.delay_strategy = strategy;
183 self
184 }
185
186 pub fn with_dlq_ttl(mut self, ttl: Duration) -> Self {
197 self.dlq_ttl = Some(ttl);
198 self
199 }
200
201 pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
203 if attempt >= self.max_retries {
204 return None; }
206
207 let delay = match &self.mechanism {
208 RetryMechanism::Exponential {
209 base_delay,
210 max_delay,
211 } => {
212 let exponential_delay =
213 Duration::from_millis(base_delay.as_millis() as u64 * 2_u64.pow(attempt));
214 std::cmp::min(exponential_delay, *max_delay)
215 }
216 RetryMechanism::Linear { delay } => *delay,
217 RetryMechanism::Custom { delays } => {
218 if (attempt as usize) < delays.len() {
219 delays[attempt as usize]
220 } else {
221 return None; }
223 }
224 };
225
226 Some(delay)
227 }
228
229 pub fn get_dead_letter_exchange(&self, queue_name: &str) -> String {
231 self.dead_letter_exchange
232 .clone()
233 .unwrap_or_else(|| format!("{}.dlx", queue_name))
234 }
235
236 pub fn get_dead_letter_queue(&self, queue_name: &str) -> String {
238 self.dead_letter_queue
239 .clone()
240 .unwrap_or_else(|| format!("{}.dlq", queue_name))
241 }
242
243 pub fn get_retry_queue_name(&self, queue_name: &str, attempt: u32) -> String {
245 format!("{}.retry.{}", queue_name, attempt + 1)
246 }
247
248 pub fn get_delay_exchange(&self, queue_name: &str) -> String {
250 format!("{}.delay", queue_name)
251 }
252}
253
254impl Default for RetryConfig {
255 fn default() -> Self {
256 Self::exponential_default()
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_exponential_retry() {
266 let config = RetryConfig::exponential(5, Duration::from_secs(1), Duration::from_secs(30));
267
268 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); }
275
276 #[test]
277 fn test_exponential_retry_with_cap() {
278 let config = RetryConfig::exponential(10, Duration::from_secs(1), Duration::from_secs(5));
279
280 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))); }
286
287 #[test]
288 fn test_linear_retry() {
289 let config = RetryConfig::linear(3, Duration::from_secs(5));
290
291 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(5)));
292 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
293 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(5)));
294 assert_eq!(config.calculate_delay(3), None); }
296
297 #[test]
298 fn test_custom_retry() {
299 let delays = vec![
300 Duration::from_secs(1),
301 Duration::from_secs(5),
302 Duration::from_secs(30),
303 ];
304 let config = RetryConfig::custom(delays);
305
306 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1)));
307 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
308 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(30)));
309 assert_eq!(config.calculate_delay(3), None); }
311
312 #[test]
313 fn test_no_retry() {
314 let config = RetryConfig::no_retry();
315
316 assert_eq!(config.calculate_delay(0), None); }
318
319 #[test]
320 fn test_dead_letter_names() {
321 let config = RetryConfig::exponential_default();
322
323 assert_eq!(config.get_dead_letter_exchange("orders"), "orders.dlx");
324 assert_eq!(config.get_dead_letter_queue("orders"), "orders.dlq");
325
326 let config_custom =
327 config.with_dead_letter("custom.dlx".to_string(), "custom.dlq".to_string());
328 assert_eq!(
329 config_custom.get_dead_letter_exchange("orders"),
330 "custom.dlx"
331 );
332 assert_eq!(config_custom.get_dead_letter_queue("orders"), "custom.dlq");
333 }
334
335 #[test]
336 fn test_retry_queue_names() {
337 let config = RetryConfig::exponential_default();
338
339 assert_eq!(config.get_retry_queue_name("orders", 0), "orders.retry.1");
340 assert_eq!(config.get_retry_queue_name("orders", 1), "orders.retry.2");
341 assert_eq!(config.get_retry_queue_name("orders", 4), "orders.retry.5");
342 }
343
344 #[test]
345 fn test_delay_exchange_names() {
346 let config = RetryConfig::exponential_default();
347
348 assert_eq!(config.get_delay_exchange("orders"), "orders.delay");
349 }
350
351 #[test]
352 fn test_delay_strategy_default() {
353 let config = RetryConfig::exponential_default();
354
355 assert!(matches!(config.delay_strategy, DelayStrategy::TTL));
356 }
357
358 #[test]
359 fn test_delay_strategy_configuration() {
360 let config =
361 RetryConfig::exponential_default().with_delay_strategy(DelayStrategy::DelayedExchange);
362
363 assert!(matches!(
364 config.delay_strategy,
365 DelayStrategy::DelayedExchange
366 ));
367 }
368}