Skip to main content

ironsbe_client/
reconnect.rs

1//! Reconnection logic for client connections.
2
3use std::time::Duration;
4
5/// Configuration for reconnection behavior.
6#[derive(Debug, Clone)]
7pub struct ReconnectConfig {
8    /// Whether reconnection is enabled.
9    pub enabled: bool,
10    /// Initial delay before first reconnect attempt.
11    pub initial_delay: Duration,
12    /// Maximum delay between reconnect attempts.
13    pub max_delay: Duration,
14    /// Multiplier for exponential backoff.
15    pub backoff_multiplier: f64,
16    /// Maximum number of reconnect attempts (0 = unlimited).
17    pub max_attempts: usize,
18}
19
20impl Default for ReconnectConfig {
21    fn default() -> Self {
22        Self {
23            enabled: true,
24            initial_delay: Duration::from_millis(100),
25            max_delay: Duration::from_secs(30),
26            backoff_multiplier: 2.0,
27            max_attempts: 10,
28        }
29    }
30}
31
32/// Tracks reconnection state and calculates delays.
33pub struct ReconnectState {
34    config: ReconnectConfig,
35    attempts: usize,
36    current_delay: Duration,
37}
38
39impl ReconnectState {
40    /// Creates a new reconnect state with the given configuration.
41    #[must_use]
42    pub fn new(config: ReconnectConfig) -> Self {
43        let initial_delay = config.initial_delay;
44        Self {
45            config,
46            attempts: 0,
47            current_delay: initial_delay,
48        }
49    }
50
51    /// Records a failed connection attempt and returns the delay before next attempt.
52    ///
53    /// Returns `None` if max attempts reached or reconnection is disabled.
54    pub fn on_failure(&mut self) -> Option<Duration> {
55        if !self.config.enabled {
56            return None;
57        }
58
59        self.attempts += 1;
60
61        if self.config.max_attempts > 0 && self.attempts >= self.config.max_attempts {
62            return None;
63        }
64
65        let delay = self.current_delay;
66
67        // Calculate next delay with exponential backoff
68        let next_delay = Duration::from_secs_f64(
69            self.current_delay.as_secs_f64() * self.config.backoff_multiplier,
70        );
71        self.current_delay = next_delay.min(self.config.max_delay);
72
73        Some(delay)
74    }
75
76    /// Resets the reconnection state after a successful connection.
77    pub fn on_success(&mut self) {
78        self.attempts = 0;
79        self.current_delay = self.config.initial_delay;
80    }
81
82    /// Returns the number of reconnection attempts made.
83    #[must_use]
84    pub fn attempts(&self) -> usize {
85        self.attempts
86    }
87
88    /// Returns true if more reconnection attempts are allowed.
89    #[must_use]
90    pub fn can_retry(&self) -> bool {
91        self.config.enabled
92            && (self.config.max_attempts == 0 || self.attempts < self.config.max_attempts)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_reconnect_backoff() {
102        let config = ReconnectConfig {
103            enabled: true,
104            initial_delay: Duration::from_millis(100),
105            max_delay: Duration::from_secs(10),
106            backoff_multiplier: 2.0,
107            max_attempts: 5,
108        };
109
110        let mut state = ReconnectState::new(config);
111
112        // First failure
113        let delay = state.on_failure().unwrap();
114        assert_eq!(delay, Duration::from_millis(100));
115
116        // Second failure
117        let delay = state.on_failure().unwrap();
118        assert_eq!(delay, Duration::from_millis(200));
119
120        // Third failure
121        let delay = state.on_failure().unwrap();
122        assert_eq!(delay, Duration::from_millis(400));
123    }
124
125    #[test]
126    fn test_reconnect_max_attempts() {
127        let config = ReconnectConfig {
128            enabled: true,
129            initial_delay: Duration::from_millis(100),
130            max_delay: Duration::from_secs(10),
131            backoff_multiplier: 2.0,
132            max_attempts: 2,
133        };
134
135        let mut state = ReconnectState::new(config);
136
137        assert!(state.on_failure().is_some());
138        assert!(state.on_failure().is_none()); // Max reached
139    }
140
141    #[test]
142    fn test_reconnect_reset() {
143        let config = ReconnectConfig::default();
144        let mut state = ReconnectState::new(config);
145
146        state.on_failure();
147        state.on_failure();
148        assert_eq!(state.attempts(), 2);
149
150        state.on_success();
151        assert_eq!(state.attempts(), 0);
152    }
153
154    #[test]
155    fn test_reconnect_disabled() {
156        let config = ReconnectConfig {
157            enabled: false,
158            ..Default::default()
159        };
160
161        let mut state = ReconnectState::new(config);
162        assert!(state.on_failure().is_none());
163    }
164}