monocoque_core/
reconnect.rs1use crate::options::SocketOptions;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
39pub struct ReconnectState {
40 base_interval: Duration,
42 max_interval: Duration,
44 attempt: u32,
46 current_interval: Duration,
48}
49
50impl ReconnectState {
51 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 pub fn next_delay(&mut self) -> Duration {
71 let delay = self.current_interval;
72
73 self.attempt += 1;
74
75 if self.max_interval.is_zero() || self.max_interval <= self.base_interval {
80 self.current_interval = self.base_interval;
81 } else {
82 self.current_interval = self
83 .base_interval
84 .checked_mul(1_u32 << self.attempt.min(10))
85 .unwrap_or(self.max_interval)
86 .min(self.max_interval);
87 }
88
89 delay
90 }
91
92 pub fn reset(&mut self) {
96 self.attempt = 0;
97 self.current_interval = self.base_interval;
98 }
99
100 #[inline]
102 #[must_use]
103 pub const fn attempt(&self) -> u32 {
104 self.attempt
105 }
106
107 #[inline]
109 #[must_use]
110 pub const fn base_interval(&self) -> Duration {
111 self.base_interval
112 }
113
114 #[inline]
116 #[must_use]
117 pub const fn max_interval(&self) -> Duration {
118 self.max_interval
119 }
120
121 #[inline]
123 #[must_use]
124 pub const fn current_interval(&self) -> Duration {
125 self.current_interval
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum ReconnectError {
132 MaxAttemptsReached { attempts: u32 },
134 ConnectionFailed { message: String },
136 Cancelled,
138}
139
140impl std::fmt::Display for ReconnectError {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 match self {
143 Self::MaxAttemptsReached { attempts } => {
144 write!(f, "Maximum reconnection attempts reached: {attempts}")
145 }
146 Self::ConnectionFailed { message } => {
147 write!(f, "Connection failed: {message}")
148 }
149 Self::Cancelled => {
150 write!(f, "Reconnection cancelled")
151 }
152 }
153 }
154}
155
156impl std::error::Error for ReconnectError {}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_exponential_backoff() {
164 let options = SocketOptions::default()
165 .with_reconnect_ivl(Duration::from_millis(100))
166 .with_reconnect_ivl_max(Duration::from_secs(10));
167
168 let mut state = ReconnectState::new(&options);
169
170 assert_eq!(state.next_delay(), Duration::from_millis(100));
172 assert_eq!(state.attempt(), 1);
173
174 assert_eq!(state.next_delay(), Duration::from_millis(200));
176 assert_eq!(state.attempt(), 2);
177
178 assert_eq!(state.next_delay(), Duration::from_millis(400));
180 assert_eq!(state.attempt(), 3);
181
182 assert_eq!(state.next_delay(), Duration::from_millis(800));
184 assert_eq!(state.attempt(), 4);
185 }
186
187 #[test]
188 fn test_max_interval_cap() {
189 let options = SocketOptions::default()
190 .with_reconnect_ivl(Duration::from_millis(100))
191 .with_reconnect_ivl_max(Duration::from_millis(500));
192
193 let mut state = ReconnectState::new(&options);
194
195 assert_eq!(state.next_delay(), Duration::from_millis(100));
196 assert_eq!(state.next_delay(), Duration::from_millis(200));
197 assert_eq!(state.next_delay(), Duration::from_millis(400));
198
199 assert_eq!(state.next_delay(), Duration::from_millis(500));
201 assert_eq!(state.next_delay(), Duration::from_millis(500));
202 }
203
204 #[test]
205 fn test_next_delay_caps_before_duration_overflow() {
206 let options = SocketOptions::default()
207 .with_reconnect_ivl(Duration::MAX)
208 .with_reconnect_ivl_max(Duration::from_secs(1));
209
210 let mut state = ReconnectState::new(&options);
211 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
212 state.next_delay();
213 }));
214
215 assert!(result.is_ok());
216 }
217
218 #[test]
219 fn test_max_zero_holds_at_base_no_zero_delay_loop() {
220 let options = SocketOptions::default()
225 .with_reconnect_ivl(Duration::from_millis(100))
226 .with_reconnect_ivl_max(Duration::ZERO);
227
228 let mut state = ReconnectState::new(&options);
229
230 for _ in 0..10 {
231 assert_eq!(
232 state.next_delay(),
233 Duration::from_millis(100),
234 "max=0 must hold the delay at the base interval, never 0"
235 );
236 }
237 }
238
239 #[test]
240 fn test_max_below_base_holds_at_base() {
241 let options = SocketOptions::default()
244 .with_reconnect_ivl(Duration::from_millis(100))
245 .with_reconnect_ivl_max(Duration::from_millis(50));
246
247 let mut state = ReconnectState::new(&options);
248
249 for _ in 0..5 {
250 assert_eq!(state.next_delay(), Duration::from_millis(100));
251 }
252 }
253
254 #[test]
255 fn test_reset() {
256 let options = SocketOptions::default()
257 .with_reconnect_ivl(Duration::from_millis(100))
258 .with_reconnect_ivl_max(Duration::from_secs(10));
259
260 let mut state = ReconnectState::new(&options);
261
262 state.next_delay();
264 state.next_delay();
265 state.next_delay();
266 assert_eq!(state.attempt(), 3);
267
268 state.reset();
270 assert_eq!(state.attempt(), 0);
271 assert_eq!(state.next_delay(), Duration::from_millis(100));
272 }
273
274 #[test]
275 fn test_state_accessors() {
276 let options = SocketOptions::default()
277 .with_reconnect_ivl(Duration::from_millis(250))
278 .with_reconnect_ivl_max(Duration::from_secs(5));
279
280 let state = ReconnectState::new(&options);
281
282 assert_eq!(state.base_interval(), Duration::from_millis(250));
283 assert_eq!(state.max_interval(), Duration::from_secs(5));
284 assert_eq!(state.current_interval(), Duration::from_millis(250));
285 assert_eq!(state.attempt(), 0);
286 }
287}