rust_rabbit/
retry.rs

1//! Simplified and flexible retry system for rust-rabbit
2//!
3//! This module provides a simple but powerful retry mechanism with support for:
4//! - Configurable max retry count
5//! - Multiple retry mechanisms: exponential (1s->2s->4s->8s), linear, custom delays
6//! - Delay-based message retry using RabbitMQ delayed exchanges
7
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11/// Retry mechanism configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum RetryMechanism {
14    /// Exponential backoff: base_delay * 2^attempt, capped at max_delay
15    /// Example: 1s -> 2s -> 4s -> 8s -> 16s (capped at max_delay)
16    Exponential {
17        base_delay: Duration,
18        max_delay: Duration,
19    },
20
21    /// Linear delay: same delay for each retry attempt
22    /// Example: 5s -> 5s -> 5s -> 5s
23    Linear { delay: Duration },
24
25    /// Custom delays: specify exact delay for each retry attempt
26    /// Example: [1s, 5s, 30s] for 3 retries with increasing delays
27    Custom { delays: Vec<Duration> },
28}
29
30/// Simple retry configuration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RetryConfig {
33    /// Maximum number of retry attempts (0 means no retries)
34    pub max_retries: u32,
35
36    /// Retry mechanism to use
37    pub mechanism: RetryMechanism,
38
39    /// Dead letter exchange for messages that exceed max retries
40    /// If None, uses default: "{queue_name}.dlx"
41    pub dead_letter_exchange: Option<String>,
42
43    /// Dead letter queue for permanently failed messages
44    /// If None, uses default: "{queue_name}.dlq"
45    pub dead_letter_queue: Option<String>,
46}
47
48impl RetryConfig {
49    /// Create exponential retry: 1s -> 2s -> 4s -> 8s -> 16s (max 5 retries)
50    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    /// Create exponential retry with custom parameters
63    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    /// Create linear retry: same delay for each attempt
76    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    /// Create custom retry with specific delays for each attempt
86    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    /// No retries - fail immediately
97    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    /// Set custom dead letter exchange and queue names
109    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    /// Calculate delay for a specific retry attempt (0-indexed)
116    pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
117        if attempt >= self.max_retries {
118            return None; // No more retries
119        }
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; // No more custom delays
136                }
137            }
138        };
139
140        Some(delay)
141    }
142
143    /// Get dead letter exchange name (with fallback to default)
144    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    /// Get dead letter queue name (with fallback to default)
151    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    /// Generate retry queue name for delayed messages
158    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))); // 1s
178        assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(2))); // 2s
179        assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(4))); // 4s
180        assert_eq!(config.calculate_delay(3), Some(Duration::from_secs(8))); // 8s
181        assert_eq!(config.calculate_delay(4), Some(Duration::from_secs(16))); // 16s
182        assert_eq!(config.calculate_delay(5), None); // No more retries
183    }
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))); // 1s
190        assert_eq!(config.calculate_delay(1), Some(Duration::from_secs(2))); // 2s
191        assert_eq!(config.calculate_delay(2), Some(Duration::from_secs(4))); // 4s
192        assert_eq!(config.calculate_delay(3), Some(Duration::from_secs(5))); // 5s (capped)
193        assert_eq!(config.calculate_delay(4), Some(Duration::from_secs(5))); // 5s (capped)
194    }
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); // No more retries
204    }
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); // No more retries
219    }
220
221    #[test]
222    fn test_no_retry() {
223        let config = RetryConfig::no_retry();
224
225        assert_eq!(config.calculate_delay(0), None); // No retries at all
226    }
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}