Skip to main content

ocpp_client/
reconnect.rs

1//! Automatic-reconnect support for `Client`'s background read loop. `Reconnector` mirrors the
2//! `Executor`/`Timer` pattern in `src/runtime.rs` - a dyn-safe trait so `Client<E>` stays
3//! generic over one type parameter only. `connect_1_6`/`connect_2_0_1`/`connect_2_1` wire up a
4//! WebSocket-backed impl automatically; embedded users implement this trait for their own
5//! transport to get the same behavior.
6
7use crate::transport::{TransportError, TransportSink, TransportStream};
8use alloc::boxed::Box;
9use core::future::Future;
10use core::pin::Pin;
11use core::time::Duration;
12
13/// (Re-)establishes a transport connection from scratch. Called by `Client`'s background read
14/// loop after the current transport reports it closed (`TransportStream::recv` returning
15/// `Ok(None)` or `Err(_)`).
16pub trait Reconnector: Send + Sync + 'static {
17    #[allow(clippy::type_complexity)]
18    fn connect<'a>(
19        &'a self,
20    ) -> Pin<
21        Box<
22            dyn Future<
23                    Output = Result<
24                        (Box<dyn TransportSink>, Box<dyn TransportStream>),
25                        TransportError,
26                    >,
27                > + Send
28                + 'a,
29        >,
30    >;
31}
32
33/// Bounded exponential backoff between reconnect attempts. The delay doubles (by
34/// `multiplier`) after each failed attempt, capped at `max_delay` - but the number of attempts
35/// itself is unbounded: a charge point should keep trying to reach its CSMS indefinitely
36/// rather than giving up after N tries.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct ReconnectPolicy {
39    pub initial_delay: Duration,
40    pub max_delay: Duration,
41    pub multiplier: u32,
42}
43
44impl Default for ReconnectPolicy {
45    fn default() -> Self {
46        Self {
47            initial_delay: Duration::from_secs(1),
48            max_delay: Duration::from_secs(60),
49            multiplier: 2,
50        }
51    }
52}
53
54impl ReconnectPolicy {
55    /// The delay to wait before reconnect attempt number `attempt` (0-indexed: `0` is the
56    /// delay before the first retry, right after the initial disconnect).
57    pub(crate) fn delay_for(&self, attempt: u32) -> Duration {
58        let mut delay = self.initial_delay;
59        for _ in 0..attempt {
60            delay = match delay.checked_mul(self.multiplier) {
61                Some(d) if d < self.max_delay => d,
62                _ => return self.max_delay,
63            };
64        }
65        delay
66    }
67}
68
69/// Whether a `connect_*` call should reconnect automatically on disconnect. Defaults to
70/// `Enabled` with `ReconnectPolicy::default()` - production charge points are expected to keep
71/// retrying the CSMS connection, so that's the out-of-the-box behavior; set
72/// `ConnectOptions::reconnect` to `Disabled` to opt out.
73#[derive(Debug, Clone, Copy)]
74pub enum ReconnectBehavior {
75    Enabled(ReconnectPolicy),
76    Disabled,
77}
78
79impl Default for ReconnectBehavior {
80    fn default() -> Self {
81        Self::Enabled(ReconnectPolicy::default())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn delay_doubles_and_caps() {
91        let policy = ReconnectPolicy {
92            initial_delay: Duration::from_secs(1),
93            max_delay: Duration::from_secs(10),
94            multiplier: 2,
95        };
96        assert_eq!(policy.delay_for(0), Duration::from_secs(1));
97        assert_eq!(policy.delay_for(1), Duration::from_secs(2));
98        assert_eq!(policy.delay_for(2), Duration::from_secs(4));
99        assert_eq!(policy.delay_for(3), Duration::from_secs(8));
100        assert_eq!(policy.delay_for(4), Duration::from_secs(10));
101        assert_eq!(policy.delay_for(10), Duration::from_secs(10));
102    }
103}