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.base_interval * (1_u32 << self.attempt.min(10));
76
77        // Cap at max interval
78        if self.current_interval > self.max_interval {
79            self.current_interval = self.max_interval;
80        }
81
82        delay
83    }
84
85    /// Reset the reconnection state after a successful connection.
86    ///
87    /// This resets the attempt counter and interval back to the base values.
88    pub fn reset(&mut self) {
89        self.attempt = 0;
90        self.current_interval = self.base_interval;
91    }
92
93    /// Get the current attempt number.
94    #[inline]
95    #[must_use]
96    pub const fn attempt(&self) -> u32 {
97        self.attempt
98    }
99
100    /// Get the base reconnection interval.
101    #[inline]
102    #[must_use]
103    pub const fn base_interval(&self) -> Duration {
104        self.base_interval
105    }
106
107    /// Get the maximum reconnection interval.
108    #[inline]
109    #[must_use]
110    pub const fn max_interval(&self) -> Duration {
111        self.max_interval
112    }
113
114    /// Get the current reconnection interval.
115    #[inline]
116    #[must_use]
117    pub const fn current_interval(&self) -> Duration {
118        self.current_interval
119    }
120}
121
122/// Error type for reconnection operations.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum ReconnectError {
125    /// Maximum reconnection attempts reached
126    MaxAttemptsReached { attempts: u32 },
127    /// Connection failed with I/O error
128    ConnectionFailed { message: String },
129    /// Reconnection cancelled by user
130    Cancelled,
131}
132
133impl std::fmt::Display for ReconnectError {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::MaxAttemptsReached { attempts } => {
137                write!(f, "Maximum reconnection attempts reached: {attempts}")
138            }
139            Self::ConnectionFailed { message } => {
140                write!(f, "Connection failed: {message}")
141            }
142            Self::Cancelled => {
143                write!(f, "Reconnection cancelled")
144            }
145        }
146    }
147}
148
149impl std::error::Error for ReconnectError {}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_exponential_backoff() {
157        let options = SocketOptions::default()
158            .with_reconnect_ivl(Duration::from_millis(100))
159            .with_reconnect_ivl_max(Duration::from_secs(10));
160
161        let mut state = ReconnectState::new(&options);
162
163        // First attempt: base interval
164        assert_eq!(state.next_delay(), Duration::from_millis(100));
165        assert_eq!(state.attempt(), 1);
166
167        // Second attempt: doubled
168        assert_eq!(state.next_delay(), Duration::from_millis(200));
169        assert_eq!(state.attempt(), 2);
170
171        // Third attempt: doubled again
172        assert_eq!(state.next_delay(), Duration::from_millis(400));
173        assert_eq!(state.attempt(), 3);
174
175        // Fourth attempt
176        assert_eq!(state.next_delay(), Duration::from_millis(800));
177        assert_eq!(state.attempt(), 4);
178    }
179
180    #[test]
181    fn test_max_interval_cap() {
182        let options = SocketOptions::default()
183            .with_reconnect_ivl(Duration::from_millis(100))
184            .with_reconnect_ivl_max(Duration::from_millis(500));
185
186        let mut state = ReconnectState::new(&options);
187
188        assert_eq!(state.next_delay(), Duration::from_millis(100));
189        assert_eq!(state.next_delay(), Duration::from_millis(200));
190        assert_eq!(state.next_delay(), Duration::from_millis(400));
191
192        // Should be capped at max
193        assert_eq!(state.next_delay(), Duration::from_millis(500));
194        assert_eq!(state.next_delay(), Duration::from_millis(500));
195    }
196
197    #[test]
198    fn test_reset() {
199        let options = SocketOptions::default()
200            .with_reconnect_ivl(Duration::from_millis(100))
201            .with_reconnect_ivl_max(Duration::from_secs(10));
202
203        let mut state = ReconnectState::new(&options);
204
205        // Make some attempts
206        state.next_delay();
207        state.next_delay();
208        state.next_delay();
209        assert_eq!(state.attempt(), 3);
210
211        // Reset
212        state.reset();
213        assert_eq!(state.attempt(), 0);
214        assert_eq!(state.next_delay(), Duration::from_millis(100));
215    }
216
217    #[test]
218    fn test_state_accessors() {
219        let options = SocketOptions::default()
220            .with_reconnect_ivl(Duration::from_millis(250))
221            .with_reconnect_ivl_max(Duration::from_secs(5));
222
223        let state = ReconnectState::new(&options);
224
225        assert_eq!(state.base_interval(), Duration::from_millis(250));
226        assert_eq!(state.max_interval(), Duration::from_secs(5));
227        assert_eq!(state.current_interval(), Duration::from_millis(250));
228        assert_eq!(state.attempt(), 0);
229    }
230}