1use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
54pub enum DelayStrategy {
55 #[default]
57 TTL,
58 DelayedExchange,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum RetryMechanism {
73 Exponential {
76 base_delay: Duration,
77 max_delay: Duration,
78 },
79
80 Linear { delay: Duration },
83
84 Custom { delays: Vec<Duration> },
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct RetryConfig {
92 pub max_retries: u32,
94
95 pub mechanism: RetryMechanism,
97
98 #[serde(default)]
100 pub delay_strategy: DelayStrategy,
101
102 pub dead_letter_exchange: Option<String>,
105
106 pub dead_letter_queue: Option<String>,
109
110 pub dlq_ttl: Option<Duration>,
114}
115
116impl RetryConfig {
117 pub fn exponential_default() -> Self {
119 Self {
120 max_retries: 5,
121 mechanism: RetryMechanism::Exponential {
122 base_delay: Duration::from_secs(1),
123 max_delay: Duration::from_secs(60),
124 },
125 delay_strategy: DelayStrategy::default(),
126 dead_letter_exchange: None,
127 dead_letter_queue: None,
128 dlq_ttl: None,
129 }
130 }
131
132 pub fn exponential(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
134 Self {
135 max_retries,
136 mechanism: RetryMechanism::Exponential {
137 base_delay,
138 max_delay,
139 },
140 delay_strategy: DelayStrategy::default(),
141 dead_letter_exchange: None,
142 dead_letter_queue: None,
143 dlq_ttl: None,
144 }
145 }
146
147 pub fn linear(max_retries: u32, delay: Duration) -> Self {
149 Self {
150 max_retries,
151 mechanism: RetryMechanism::Linear { delay },
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 custom(delays: Vec<Duration>) -> Self {
161 let max_retries = delays.len() as u32;
162 Self {
163 max_retries,
164 mechanism: RetryMechanism::Custom { delays },
165 delay_strategy: DelayStrategy::default(),
166 dead_letter_exchange: None,
167 dead_letter_queue: None,
168 dlq_ttl: None,
169 }
170 }
171
172 pub fn no_retry() -> Self {
174 Self {
175 max_retries: 0,
176 mechanism: RetryMechanism::Linear {
177 delay: Duration::from_secs(0),
178 },
179 delay_strategy: DelayStrategy::default(),
180 dead_letter_exchange: None,
181 dead_letter_queue: None,
182 dlq_ttl: None,
183 }
184 }
185
186 pub fn with_dead_letter(mut self, exchange: String, queue: String) -> Self {
188 self.dead_letter_exchange = Some(exchange);
189 self.dead_letter_queue = Some(queue);
190 self
191 }
192
193 pub fn with_delay_strategy(mut self, strategy: DelayStrategy) -> Self {
195 self.delay_strategy = strategy;
196 self
197 }
198
199 pub fn with_dlq_ttl(mut self, ttl: Duration) -> Self {
208 self.dlq_ttl = Some(ttl);
209 self
210 }
211
212 pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
214 if attempt >= self.max_retries {
215 return None; }
217
218 let delay = match &self.mechanism {
219 RetryMechanism::Exponential {
220 base_delay,
221 max_delay,
222 } => {
223 let exponential_delay =
224 Duration::from_millis(base_delay.as_millis() as u64 * 2_u64.pow(attempt));
225 std::cmp::min(exponential_delay, *max_delay)
226 }
227 RetryMechanism::Linear { delay } => *delay,
228 RetryMechanism::Custom { delays } => {
229 if (attempt as usize) < delays.len() {
230 delays[attempt as usize]
231 } else {
232 return None; }
234 }
235 };
236
237 Some(delay)
238 }
239
240 pub fn get_dead_letter_exchange(&self, queue_name: &str) -> String {
242 self.dead_letter_exchange
243 .clone()
244 .unwrap_or_else(|| format!("{}.dlx", queue_name))
245 }
246
247 pub fn get_dead_letter_queue(&self, queue_name: &str) -> String {
249 self.dead_letter_queue
250 .clone()
251 .unwrap_or_else(|| format!("{}.dlq", queue_name))
252 }
253
254 pub fn get_retry_queue_name(&self, queue_name: &str, attempt: u32) -> String {
256 format!("{}.retry.{}", queue_name, attempt + 1)
257 }
258
259 pub fn get_delay_exchange(&self, queue_name: &str) -> String {
261 format!("{}.delay", queue_name)
262 }
263}
264
265impl Default for RetryConfig {
266 fn default() -> Self {
267 Self::exponential_default()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_exponential_retry() {
277 let config = RetryConfig::exponential(5, Duration::from_secs(1), Duration::from_secs(30));
278
279 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); }
286
287 #[test]
288 fn test_exponential_retry_with_cap() {
289 let config = RetryConfig::exponential(10, Duration::from_secs(1), Duration::from_secs(5));
290
291 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))); }
297
298 #[test]
299 fn test_linear_retry() {
300 let config = RetryConfig::linear(3, Duration::from_secs(5));
301
302 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(5)));
303 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
304 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(5)));
305 assert_eq!(config.calculate_delay(3), None); }
307
308 #[test]
309 fn test_custom_retry() {
310 let delays = vec![
311 Duration::from_secs(1),
312 Duration::from_secs(5),
313 Duration::from_secs(30),
314 ];
315 let config = RetryConfig::custom(delays);
316
317 assert_eq!(config.calculate_delay(0), Some(Duration::from_secs(1)));
318 assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(5)));
319 assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(30)));
320 assert_eq!(config.calculate_delay(3), None); }
322
323 #[test]
324 fn test_no_retry() {
325 let config = RetryConfig::no_retry();
326
327 assert_eq!(config.calculate_delay(0), None); }
329
330 #[test]
331 fn test_dead_letter_names() {
332 let config = RetryConfig::exponential_default();
333
334 assert_eq!(config.get_dead_letter_exchange("orders"), "orders.dlx");
335 assert_eq!(config.get_dead_letter_queue("orders"), "orders.dlq");
336
337 let config_custom =
338 config.with_dead_letter("custom.dlx".to_string(), "custom.dlq".to_string());
339 assert_eq!(
340 config_custom.get_dead_letter_exchange("orders"),
341 "custom.dlx"
342 );
343 assert_eq!(config_custom.get_dead_letter_queue("orders"), "custom.dlq");
344 }
345
346 #[test]
347 fn test_retry_queue_names() {
348 let config = RetryConfig::exponential_default();
349
350 assert_eq!(config.get_retry_queue_name("orders", 0), "orders.retry.1");
351 assert_eq!(config.get_retry_queue_name("orders", 1), "orders.retry.2");
352 assert_eq!(config.get_retry_queue_name("orders", 4), "orders.retry.5");
353 }
354
355 #[test]
356 fn test_delay_exchange_names() {
357 let config = RetryConfig::exponential_default();
358
359 assert_eq!(config.get_delay_exchange("orders"), "orders.delay");
360 }
361
362 #[test]
363 fn test_delay_strategy_default() {
364 let config = RetryConfig::exponential_default();
365
366 assert!(matches!(config.delay_strategy, DelayStrategy::TTL));
367 }
368
369 #[test]
370 fn test_delay_strategy_configuration() {
371 let config =
372 RetryConfig::exponential_default().with_delay_strategy(DelayStrategy::DelayedExchange);
373
374 assert!(matches!(
375 config.delay_strategy,
376 DelayStrategy::DelayedExchange
377 ));
378 }
379}