Skip to main content

atomr_remote_serial/
reconnect.rs

1//! Exponential-backoff reconnect policy for [`SerialTransport`].
2//!
3//! USB devices re-enumerate on cable wiggles, gadget reboots, and host
4//! suspend/resume — much more often than a TCP socket gets RST'd. We
5//! handle that inside the transport so [`atomr_remote::endpoint_manager`]
6//! doesn't churn through `Pending → Quarantined` cycles on every flap.
7
8use std::time::Duration;
9
10/// How aggressively the transport retries a failed `open()` of the
11/// configured device path.
12#[derive(Debug, Clone)]
13pub struct ReconnectPolicy {
14    /// First retry after this delay.
15    pub initial: Duration,
16    /// Cap on the per-retry delay.
17    pub max: Duration,
18    /// Multiplier applied between retries.
19    pub multiplier: f64,
20}
21
22impl Default for ReconnectPolicy {
23    fn default() -> Self {
24        Self { initial: Duration::from_millis(50), max: Duration::from_secs(5), multiplier: 2.0 }
25    }
26}
27
28impl ReconnectPolicy {
29    /// Disable reconnect entirely. The transport will surface
30    /// `TransportError::Closed` on disconnect and never retry.
31    pub fn never() -> Self {
32        Self { initial: Duration::ZERO, max: Duration::ZERO, multiplier: 1.0 }
33    }
34
35    pub(crate) fn next_delay(&self, current: Duration) -> Duration {
36        if self.max.is_zero() {
37            return Duration::ZERO;
38        }
39        let scaled = current.mul_f64(self.multiplier);
40        scaled.min(self.max)
41    }
42
43    pub(crate) fn is_enabled(&self) -> bool {
44        !self.max.is_zero()
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn default_grows_to_cap_then_holds() {
54        let p = ReconnectPolicy::default();
55        let mut d = p.initial;
56        for _ in 0..10 {
57            d = p.next_delay(d);
58        }
59        assert_eq!(d, p.max, "exponential backoff should saturate at `max`");
60    }
61
62    #[test]
63    fn never_disables_reconnect() {
64        let p = ReconnectPolicy::never();
65        assert!(!p.is_enabled());
66        assert_eq!(p.next_delay(Duration::from_secs(1)), Duration::ZERO);
67    }
68
69    #[test]
70    fn next_delay_uses_multiplier() {
71        let p = ReconnectPolicy {
72            initial: Duration::from_millis(100),
73            max: Duration::from_secs(10),
74            multiplier: 3.0,
75        };
76        assert_eq!(p.next_delay(Duration::from_millis(100)), Duration::from_millis(300));
77        assert_eq!(p.next_delay(Duration::from_millis(300)), Duration::from_millis(900));
78    }
79}