titan_client/tcp/
reconnection.rs

1use rand;
2use std::time::Duration;
3use tracing::info;
4
5use super::{tcp_client, tcp_client_blocking};
6
7/// Configuration settings for connection retry and backoff strategy
8#[derive(Debug, Clone)]
9pub struct ReconnectionConfig {
10    /// Base duration for reconnect interval (used with exponential backoff)
11    pub base_interval: Duration,
12    /// Maximum reconnect interval (cap for exponential backoff)
13    pub max_interval: Duration,
14    /// Maximum number of reconnection attempts.
15    /// Use `None` for unlimited attempts.
16    pub max_attempts: Option<u32>,
17    /// Whether to add jitter to reconnection times (prevents thundering herd problem)
18    pub use_jitter: bool,
19}
20
21impl Default for ReconnectionConfig {
22    fn default() -> Self {
23        Self {
24            base_interval: Duration::from_secs(1),
25            max_interval: Duration::from_secs(60),
26            max_attempts: None,
27            use_jitter: true,
28        }
29    }
30}
31
32/// Helper for managing reconnection attempts and backoff strategy
33#[derive(Debug, Clone)]
34pub struct ReconnectionManager {
35    config: ReconnectionConfig,
36    current_attempt: u32,
37}
38
39impl ReconnectionManager {
40    /// Create a new reconnection manager with the given configuration
41    pub fn new(config: ReconnectionConfig) -> Self {
42        Self {
43            config,
44            current_attempt: 0,
45        }
46    }
47
48    /// Create a new reconnection manager with default configuration
49    pub fn new_default() -> Self {
50        Self::new(ReconnectionConfig::default())
51    }
52
53    /// Get the current attempt number (zero-based)
54    pub fn current_attempt(&self) -> u32 {
55        self.current_attempt
56    }
57
58    /// Reset the attempt counter, typically called after a successful connection
59    pub fn reset(&mut self) {
60        self.current_attempt = 0;
61    }
62
63    /// Check if we've reached the maximum number of attempts
64    ///
65    /// Returns true if we've reached the maximum attempts, false if we can try again
66    pub fn is_max_attempts_reached(&self) -> bool {
67        if let Some(max) = self.config.max_attempts {
68            self.current_attempt > max
69        } else {
70            false
71        }
72    }
73
74    /// Increment the attempt counter and calculate the next retry delay with exponential backoff
75    ///
76    /// Returns None if max attempts reached, otherwise returns the Duration to wait
77    pub fn next_delay(&mut self) -> Option<Duration> {
78        // Increment the attempt counter first
79        self.current_attempt += 1;
80
81        // Check if we've now reached or exceeded the maximum number of attempts
82        if let Some(max) = self.config.max_attempts {
83            if self.current_attempt > max {
84                return None;
85            }
86        }
87
88        // Calculate exponential backoff with clamping to max_interval
89        let exponent = std::cmp::min(self.current_attempt, 10); // Prevent potential overflow
90        let backoff_secs = std::cmp::min(
91            self.config.base_interval.as_secs() * (1 << exponent),
92            self.config.max_interval.as_secs(),
93        );
94
95        // Add jitter if configured (to prevent thundering herd problem)
96        let final_secs = if self.config.use_jitter {
97            let jitter = rand::random::<u64>() % (backoff_secs / 4 + 1);
98            backoff_secs + jitter
99        } else {
100            backoff_secs
101        };
102
103        let wait_time = Duration::from_secs(final_secs);
104
105        info!(
106            "Reconnection attempt {}/{:?} scheduled in {:?}",
107            self.current_attempt, self.config.max_attempts, wait_time
108        );
109
110        Some(wait_time)
111    }
112
113    /// Get the configuration
114    pub fn config(&self) -> &ReconnectionConfig {
115        &self.config
116    }
117
118    /// Update the configuration
119    pub fn set_config(&mut self, config: ReconnectionConfig) {
120        self.config = config;
121    }
122}
123
124/// Convert from the sync client's TcpClientConfig to ReconnectionConfig
125pub fn from_tcp_client_config(config: &tcp_client_blocking::TcpClientConfig) -> ReconnectionConfig {
126    ReconnectionConfig {
127        base_interval: config.base_reconnect_interval,
128        max_interval: config.max_reconnect_interval,
129        max_attempts: config.max_reconnect_attempts,
130        use_jitter: true,
131    }
132}
133
134/// Convert from the async client's ReconnectSettings to ReconnectionConfig
135pub fn from_async_reconnect_settings(settings: &tcp_client::Config) -> ReconnectionConfig {
136    ReconnectionConfig {
137        base_interval: settings.retry_delay,
138        max_interval: settings.retry_delay, // No max interval in async client, use same as base
139        max_attempts: settings.max_retries,
140        use_jitter: false, // Async client doesn't use jitter
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_exponential_backoff() {
150        let config = ReconnectionConfig {
151            base_interval: Duration::from_secs(1),
152            max_interval: Duration::from_secs(60),
153            max_attempts: Some(10),
154            use_jitter: false, // Disable jitter for predictable test results
155        };
156
157        let mut manager = ReconnectionManager::new(config);
158
159        // First attempt should be 1s * 2^1 = 2s
160        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(2));
161
162        // Second attempt should be 1s * 2^2 = 4s
163        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(4));
164
165        // Third attempt should be 1s * 2^3 = 8s
166        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(8));
167
168        // Reset should set attempt back to 0
169        manager.reset();
170        assert_eq!(manager.current_attempt, 0);
171
172        // After reset, next attempt should be 1s * 2^1 = 2s again
173        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(2));
174    }
175
176    #[test]
177    fn test_max_attempts() {
178        let config = ReconnectionConfig {
179            base_interval: Duration::from_secs(1),
180            max_interval: Duration::from_secs(60),
181            max_attempts: Some(3),
182            use_jitter: false,
183        };
184
185        let mut manager = ReconnectionManager::new(config);
186
187        // We should get delays for 3 attempts
188        assert!(manager.next_delay().is_some()); // Attempt 1
189        assert!(manager.next_delay().is_some()); // Attempt 2
190        assert!(manager.next_delay().is_some()); // Attempt 3
191
192        // Then we should get None
193        assert!(manager.next_delay().is_none());
194    }
195
196    #[test]
197    fn test_unlimited_attempts() {
198        let config = ReconnectionConfig {
199            base_interval: Duration::from_secs(1),
200            max_interval: Duration::from_secs(60),
201            max_attempts: None,
202            use_jitter: false,
203        };
204
205        let mut manager = ReconnectionManager::new(config);
206
207        // We should be able to get many delays without hitting a limit
208        for _ in 0..100 {
209            assert!(manager.next_delay().is_some());
210        }
211    }
212
213    #[test]
214    fn test_max_interval() {
215        let config = ReconnectionConfig {
216            base_interval: Duration::from_secs(1),
217            max_interval: Duration::from_secs(8),
218            max_attempts: None,
219            use_jitter: false,
220        };
221
222        let mut manager = ReconnectionManager::new(config);
223
224        // 1st attempt: 1s * 2^1 = 2s
225        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(2));
226
227        // 2nd attempt: 1s * 2^2 = 4s
228        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(4));
229
230        // 3rd attempt: 1s * 2^3 = 8s
231        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(8));
232
233        // 4th attempt: should be capped at 8s
234        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(8));
235
236        // 5th attempt: should still be capped at 8s
237        assert_eq!(manager.next_delay().unwrap(), Duration::from_secs(8));
238    }
239}