Skip to main content

monocoque_core/
reconnect.rs

1//! Reconnection utilities with exponential backoff support.
2//!
3//! This module provides utilities for managing socket reconnection with
4//! exponential backoff, following libzmq patterns.
5
6use crate::options::SocketOptions;
7use std::time::Duration;
8
9/// Reconnection state tracker for managing connection attempts and backoff.
10///
11/// This helper tracks the number of reconnection attempts and calculates
12/// the appropriate backoff delay using exponential backoff.
13///
14/// # Example
15///
16/// ```rust
17/// use monocoque_core::reconnect::ReconnectState;
18/// use monocoque_core::options::SocketOptions;
19/// use std::time::Duration;
20///
21/// let options = SocketOptions::default()
22///     .with_reconnect_ivl(Duration::from_millis(100))
23///     .with_reconnect_ivl_max(Duration::from_secs(10));
24///
25/// let mut reconnect = ReconnectState::new(&options);
26///
27/// // First attempt uses base interval
28/// assert_eq!(reconnect.next_delay(), Duration::from_millis(100));
29///
30/// // Subsequent attempts use exponential backoff
31/// assert_eq!(reconnect.next_delay(), Duration::from_millis(200));
32/// assert_eq!(reconnect.next_delay(), Duration::from_millis(400));
33///
34/// // Reset on successful connection
35/// reconnect.reset();
36/// assert_eq!(reconnect.next_delay(), Duration::from_millis(100));
37/// ```
38#[derive(Debug, Clone)]
39pub struct ReconnectState {
40    /// Base reconnection interval
41    base_interval: Duration,
42    /// Maximum reconnection interval
43    max_interval: Duration,
44    /// Current reconnection attempt (0 = first attempt)
45    attempt: u32,
46    /// Current backoff interval
47    current_interval: Duration,
48}
49
50impl ReconnectState {
51    /// Create a new reconnection state tracker from socket options.
52    pub const fn new(options: &SocketOptions) -> Self {
53        Self {
54            base_interval: options.reconnect_ivl,
55            max_interval: options.reconnect_ivl_max,
56            attempt: 0,
57            current_interval: options.reconnect_ivl,
58        }
59    }
60
61    /// Get the delay for the next reconnection attempt.
62    ///
63    /// This calculates the exponential backoff delay based on the number
64    /// of previous attempts. The delay doubles with each attempt until
65    /// it reaches `reconnect_ivl_max`.
66    ///
67    /// # Returns
68    ///
69    /// The duration to wait before the next reconnection attempt.
70    pub fn next_delay(&mut self) -> Duration {
71        let delay = self.current_interval;
72
73        // Calculate next interval with exponential backoff
74        self.attempt += 1;
75        self.current_interval = self
76            .base_interval
77            .checked_mul(1_u32 << self.attempt.min(10))
78            .unwrap_or(self.max_interval);
79
80        // Cap at max interval
81        if self.current_interval > self.max_interval {
82            self.current_interval = self.max_interval;
83        }
84
85        delay
86    }
87
88    /// Reset the reconnection state after a successful connection.
89    ///
90    /// This resets the attempt counter and interval back to the base values.
91    pub fn reset(&mut self) {
92        self.attempt = 0;
93        self.current_interval = self.base_interval;
94    }
95
96    /// Get the current attempt number.
97    #[inline]
98    #[must_use]
99    pub const fn attempt(&self) -> u32 {
100        self.attempt
101    }
102
103    /// Get the base reconnection interval.
104    #[inline]
105    #[must_use]
106    pub const fn base_interval(&self) -> Duration {
107        self.base_interval
108    }
109
110    /// Get the maximum reconnection interval.
111    #[inline]
112    #[must_use]
113    pub const fn max_interval(&self) -> Duration {
114        self.max_interval
115    }
116
117    /// Get the current reconnection interval.
118    #[inline]
119    #[must_use]
120    pub const fn current_interval(&self) -> Duration {
121        self.current_interval
122    }
123}
124
125/// Error type for reconnection operations.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum ReconnectError {
128    /// Maximum reconnection attempts reached
129    MaxAttemptsReached { attempts: u32 },
130    /// Connection failed with I/O error
131    ConnectionFailed { message: String },
132    /// Reconnection cancelled by user
133    Cancelled,
134}
135
136impl std::fmt::Display for ReconnectError {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            Self::MaxAttemptsReached { attempts } => {
140                write!(f, "Maximum reconnection attempts reached: {attempts}")
141            }
142            Self::ConnectionFailed { message } => {
143                write!(f, "Connection failed: {message}")
144            }
145            Self::Cancelled => {
146                write!(f, "Reconnection cancelled")
147            }
148        }
149    }
150}
151
152impl std::error::Error for ReconnectError {}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_exponential_backoff() {
160        let options = SocketOptions::default()
161            .with_reconnect_ivl(Duration::from_millis(100))
162            .with_reconnect_ivl_max(Duration::from_secs(10));
163
164        let mut state = ReconnectState::new(&options);
165
166        // First attempt: base interval
167        assert_eq!(state.next_delay(), Duration::from_millis(100));
168        assert_eq!(state.attempt(), 1);
169
170        // Second attempt: doubled
171        assert_eq!(state.next_delay(), Duration::from_millis(200));
172        assert_eq!(state.attempt(), 2);
173
174        // Third attempt: doubled again
175        assert_eq!(state.next_delay(), Duration::from_millis(400));
176        assert_eq!(state.attempt(), 3);
177
178        // Fourth attempt
179        assert_eq!(state.next_delay(), Duration::from_millis(800));
180        assert_eq!(state.attempt(), 4);
181    }
182
183    #[test]
184    fn test_max_interval_cap() {
185        let options = SocketOptions::default()
186            .with_reconnect_ivl(Duration::from_millis(100))
187            .with_reconnect_ivl_max(Duration::from_millis(500));
188
189        let mut state = ReconnectState::new(&options);
190
191        assert_eq!(state.next_delay(), Duration::from_millis(100));
192        assert_eq!(state.next_delay(), Duration::from_millis(200));
193        assert_eq!(state.next_delay(), Duration::from_millis(400));
194
195        // Should be capped at max
196        assert_eq!(state.next_delay(), Duration::from_millis(500));
197        assert_eq!(state.next_delay(), Duration::from_millis(500));
198    }
199
200    #[test]
201    fn test_next_delay_caps_before_duration_overflow() {
202        let options = SocketOptions::default()
203            .with_reconnect_ivl(Duration::MAX)
204            .with_reconnect_ivl_max(Duration::from_secs(1));
205
206        let mut state = ReconnectState::new(&options);
207        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
208            state.next_delay();
209        }));
210
211        assert!(result.is_ok());
212    }
213
214    #[test]
215    fn test_reset() {
216        let options = SocketOptions::default()
217            .with_reconnect_ivl(Duration::from_millis(100))
218            .with_reconnect_ivl_max(Duration::from_secs(10));
219
220        let mut state = ReconnectState::new(&options);
221
222        // Make some attempts
223        state.next_delay();
224        state.next_delay();
225        state.next_delay();
226        assert_eq!(state.attempt(), 3);
227
228        // Reset
229        state.reset();
230        assert_eq!(state.attempt(), 0);
231        assert_eq!(state.next_delay(), Duration::from_millis(100));
232    }
233
234    #[test]
235    fn test_state_accessors() {
236        let options = SocketOptions::default()
237            .with_reconnect_ivl(Duration::from_millis(250))
238            .with_reconnect_ivl_max(Duration::from_secs(5));
239
240        let state = ReconnectState::new(&options);
241
242        assert_eq!(state.base_interval(), Duration::from_millis(250));
243        assert_eq!(state.max_interval(), Duration::from_secs(5));
244        assert_eq!(state.current_interval(), Duration::from_millis(250));
245        assert_eq!(state.attempt(), 0);
246    }
247}