1use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
54pub enum DelayStrategy {
55 TTL,
57 DelayedExchange,
67}
68
69impl Default for DelayStrategy {
70 fn default() -> Self {
71 Self::TTL
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum RetryMechanism {
78 Exponential {
81 base_delay: Duration,
82 max_delay: Duration,
83 },
84
85 Linear { delay: Duration },
88
89 Custom { delays: Vec<Duration> },
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RetryConfig {
97 pub max_retries: u32,
99
100 pub mechanism: RetryMechanism,
102
103 #[serde(default)]
105 pub delay_strategy: DelayStrategy,
106
107 pub dead_letter_exchange: Option<String>,
110
111 pub dead_letter_queue: Option<String>,
114
115 pub dlq_ttl: Option<Duration>,
119}
120
121impl RetryConfig {
122 pub fn exponential_default() -> Self {
124 Self {
125 max_retries: 5,
126 mechanism: RetryMechanism::Exponential {
127 base_delay: Duration::from_secs(1),
128 max_delay: Duration::from_secs(60),
129 },
130 delay_strategy: DelayStrategy::default(),
131 dead_letter_exchange: None,
132 dead_letter_queue: None,
133 dlq_ttl: None,
134 }
135 }
136
137 pub fn exponential(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
139 Self {
140 max_retries,
141 mechanism: RetryMechanism::Exponential {
142 base_delay,
143 max_delay,
144 },
145 delay_strategy: DelayStrategy::default(),
146 dead_letter_exchange: None,
147 dead_letter_queue: None,
148 dlq_ttl: None,
149 }
150 }
151
152 pub fn linear(max_retries: u32, delay: Duration) -> Self {
154 Self {
155 max_retries,
156 mechanism: RetryMechanism::Linear { delay },
157 delay_strategy: DelayStrategy::default(),
158 dead_letter_exchange: None,
159 dead_letter_queue: None,
160 dlq_ttl: None,
161 }
162 }
163
164 pub fn custom(delays: Vec<Duration>) -> Self {
166 let max_retries = delays.len() as u32;
167 Self {
168 max_retries,
169 mechanism: RetryMechanism::Custom { delays },
170 delay_strategy: DelayStrategy::default(),
171 dead_letter_exchange: None,
172 dead_letter_queue: None,
173 dlq_ttl: None,
174 }
175 }
176
177 pub fn no_retry() -> Self {
179 Self {
180 max_retries: 0,
181 mechanism: RetryMechanism::Linear {
182 delay: Duration::from_secs(0),
183 },
184 delay_strategy: DelayStrategy::default(),
185 dead_letter_exchange: None,
186 dead_letter_queue: None,
187 dlq_ttl: None,
188 }
189 }
190
191 pub fn with_dead_letter(mut self, exchange: String, queue: String) -> Self {
193 self.dead_letter_exchange = Some(exchange);
194 self.dead_letter_queue = Some(queue);
195 self
196 }
197
198 pub fn with_delay_strategy(mut self, strategy: DelayStrategy) -> Self {
200 self.delay_strategy = strategy;
201 self
202 }
203
204 pub fn with_dlq_ttl(mut self, ttl: Duration) -> Self {
213 self.dlq_ttl = Some(ttl);
214 self
215 }
216
217 pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
219 if attempt >= self.max_retries {
220 return None; }
222
223 let delay = match &self.mechanism {
224 RetryMechanism::Exponential {
225 base_delay,
226 max_delay,
227 } => {
228 let exponential_delay =
229 Duration::from_millis(base_delay.as_millis() as u64 * 2_u64.pow(attempt));
230 std::cmp::min(exponential_delay, *max_delay)
231 }
232 RetryMechanism::Linear { delay } => *delay,
233 RetryMechanism::Custom { delays } => {
234 if (attempt as usize) < delays.len() {
235 delays[attempt as usize]
236 } else {
237 return None; }
239 }
240 };
241
242 Some(delay)
243 }
244
245 pub fn get_dead_letter_exchange(&self, queue_name: &str) -> String {
247 self.dead_letter_exchange
248 .clone()
249 .unwrap_or_else(|| format!("{}.dlx", queue_name))
250 }
251
252 pub fn get_dead_letter_queue(&self, queue_name: &str) -> String {
254 self.dead_letter_queue
255 .clone()
256 .unwrap_or_else(|| format!("{}.dlq", queue_name))
257 }
258
259 pub fn get_retry_queue_name(&self, queue_name: &str, attempt: u32) -> String {
261 format!("{}.retry.{}", queue_name, attempt + 1)
262 }
263
264 pub fn get_delay_exchange(&self, queue_name: &str) -> String {
266 format!("{}.delay", queue_name)
267 }
268}
269
270impl Default for RetryConfig {
271 fn default() -> Self {
272 Self::exponential_default()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_exponential_retry() {
282 let config = RetryConfig::exponential(5, Duration::from_secs(1), Duration::from_secs(30));
283
284 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); }
291
292 #[test]
293 fn test_exponential_retry_with_cap() {
294 let config = RetryConfig::exponential(10, Duration::from_secs(1), Duration::from_secs(5));
295
296 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))); }
302
303 #[test]
304 fn test_linear_retry() {
305 let config = RetryConfig::linear(3, Duration::from_secs(5));
306
307 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(5)));
308 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
309 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(5)));
310 assert_eq!(config.calculate_delay(3), None); }
312
313 #[test]
314 fn test_custom_retry() {
315 let delays = vec![
316 Duration::from_secs(1),
317 Duration::from_secs(5),
318 Duration::from_secs(30),
319 ];
320 let config = RetryConfig::custom(delays);
321
322 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1)));
323 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
324 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(30)));
325 assert_eq!(config.calculate_delay(3), None); }
327
328 #[test]
329 fn test_no_retry() {
330 let config = RetryConfig::no_retry();
331
332 assert_eq!(config.calculate_delay(0), None); }
334
335 #[test]
336 fn test_dead_letter_names() {
337 let config = RetryConfig::exponential_default();
338
339 assert_eq!(config.get_dead_letter_exchange("orders"), "orders.dlx");
340 assert_eq!(config.get_dead_letter_queue("orders"), "orders.dlq");
341
342 let config_custom =
343 config.with_dead_letter("custom.dlx".to_string(), "custom.dlq".to_string());
344 assert_eq!(
345 config_custom.get_dead_letter_exchange("orders"),
346 "custom.dlx"
347 );
348 assert_eq!(config_custom.get_dead_letter_queue("orders"), "custom.dlq");
349 }
350
351 #[test]
352 fn test_retry_queue_names() {
353 let config = RetryConfig::exponential_default();
354
355 assert_eq!(config.get_retry_queue_name("orders", 0), "orders.retry.1");
356 assert_eq!(config.get_retry_queue_name("orders", 1), "orders.retry.2");
357 assert_eq!(config.get_retry_queue_name("orders", 4), "orders.retry.5");
358 }
359
360 #[test]
361 fn test_delay_exchange_names() {
362 let config = RetryConfig::exponential_default();
363
364 assert_eq!(config.get_delay_exchange("orders"), "orders.delay");
365 }
366
367 #[test]
368 fn test_delay_strategy_default() {
369 let config = RetryConfig::exponential_default();
370
371 assert!(matches!(config.delay_strategy, DelayStrategy::TTL));
372 }
373
374 #[test]
375 fn test_delay_strategy_configuration() {
376 let config =
377 RetryConfig::exponential_default().with_delay_strategy(DelayStrategy::DelayedExchange);
378
379 assert!(matches!(
380 config.delay_strategy,
381 DelayStrategy::DelayedExchange
382 ));
383 }
384}