Skip to main content

polyoxide_ws/
timing.rs

1//! The supervision timings, their builder setters (W4), and the wait until
2//! the next deadline (W5).
3
4use std::time::Duration;
5
6use crate::{Backoff, DEFAULT_CONNECT_TIMEOUT};
7
8/// The timings a supervised connection runs on.
9///
10/// Every field is public, so a venue reads them directly and sets them through
11/// [`timing_setters!`](crate::timing_setters). The defaults are the kit's: a
12/// ping every 20 s, stale after 30 s of silence, reconnect backoff from 500 ms
13/// doubling to 60 s, a 10 s connect timeout, and no rotation. Each venue sets
14/// its own: one pings after a 5 s quiet tick, one sends no pings and goes
15/// stale after 45 s, and one rotates at 23 h 50 min.
16///
17/// The basis is the setters of four socket crates' supervised builders,
18/// which stored the same five values. Two of those builders hold a `Timing`
19/// since Stories 4.6 and 4.7; the other two keep their own fields (D7).
20/// What the values mean stays with each venue:
21///
22/// - **D4, the ping schedule.** One venue pings only after a quiet tick, two
23///   on the wall clock whatever the traffic, and one never
24///   (`ping_interval: None`); a fifth pings every 10 s. Each venue sets its
25///   own default, and never copies the quiet-tick shape to a host that
26///   requires pings.
27/// - **D5, liveness.** What restarts the [`stale_after`](Self::stale_after)
28///   window, each the rule of one venue: any frame, a decoded event, any text
29///   frame (a pong included), or pings included.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct Timing {
33    /// How often to ping, or `None` for never.
34    pub ping_interval: Option<Duration>,
35    /// Silence after which a connection is presumed dead.
36    pub stale_after: Duration,
37    /// The first reconnect delay.
38    pub initial_backoff: Duration,
39    /// The reconnect delay's ceiling.
40    pub max_backoff: Duration,
41    /// How long one connection attempt may take.
42    pub connect_timeout: Duration,
43    /// How long a connection may live before it is replaced, or `None` for
44    /// as long as it lasts.
45    pub max_connection_age: Option<Duration>,
46}
47
48impl Default for Timing {
49    fn default() -> Self {
50        Self {
51            ping_interval: Some(Duration::from_secs(20)),
52            stale_after: Duration::from_secs(30),
53            initial_backoff: Duration::from_millis(500),
54            max_backoff: Duration::from_secs(60),
55            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
56            max_connection_age: None,
57        }
58    }
59}
60
61impl Timing {
62    /// A fresh reconnect schedule from [`initial_backoff`](Self::initial_backoff)
63    /// to [`max_backoff`](Self::max_backoff).
64    pub fn backoff(&self) -> Backoff {
65        Backoff::new(self.initial_backoff, self.max_backoff)
66    }
67
68    /// The wait until the first of this connection's deadlines: the next ping,
69    /// the staleness limit and the rotation, given the time since the last
70    /// ping, the silence so far and the connection's age. See [`next_wait`].
71    pub fn next_wait(&self, since_ping: Duration, silent: Duration, age: Duration) -> Duration {
72        next_wait([
73            (self.ping_interval, since_ping),
74            (Some(self.stale_after), silent),
75            (self.max_connection_age, age),
76        ])
77    }
78}
79
80/// The wait until the first of several deadlines falls due.
81///
82/// Each deadline is a limit and the time elapsed against it; a limit of `None`
83/// never falls due. The arithmetic saturates: a deadline already past waits
84/// zero, a limit of `Duration::MAX` waits about that long, and no deadline at
85/// all, or none that is set, gives `Duration::MAX`, which a timeout
86/// treats as never. It is a wait, not an `Instant`, because adding a limit
87/// such as `Duration::MAX` to an `Instant` overflows.
88///
89/// The basis is one socket crate's former pump (deleted in Story 4.7), the
90/// minimum of its ping, staleness and rotation waits; another socket crate's
91/// former pump (deleted in Story 4.6) took the first two. Which deadlines a
92/// venue has follows D4: a venue that sends no pings passes no ping deadline.
93///
94/// ```
95/// use std::time::Duration;
96/// use polyoxide_ws::next_wait;
97///
98/// let secs = Duration::from_secs;
99/// // A ping due in 15 s, staleness in 20 s, and no rotation.
100/// assert_eq!(next_wait([(Some(secs(20)), secs(5)), (Some(secs(30)), secs(10)), (None, secs(99))]), secs(15));
101/// assert_eq!(next_wait([(None, secs(1))]), Duration::MAX);
102/// ```
103pub fn next_wait(deadlines: impl IntoIterator<Item = (Option<Duration>, Duration)>) -> Duration {
104    deadlines
105        .into_iter()
106        .filter_map(|(limit, elapsed)| limit.map(|limit| limit.saturating_sub(elapsed)))
107        .min()
108        .unwrap_or(Duration::MAX)
109}
110
111/// Generates the timing setters a supervised builder offers, each writing the
112/// builder's `timing` field, a [`Timing`].
113///
114/// List the setters the builder wants, each with its own doc comment, since
115/// what a ping or the staleness window means differs between venues (D4, D5).
116/// The five are `ping_interval(Duration)`, `stale_after(Duration)`,
117/// `backoff(initial, max)`, `connect_timeout(Duration)` and
118/// `max_connection_age(Duration)`; `ping_interval` and `max_connection_age`
119/// store `Some` of the value.
120///
121/// ```
122/// use std::time::Duration;
123/// use polyoxide_ws::Timing;
124///
125/// #[derive(Default)]
126/// pub struct FeedBuilder {
127///     timing: Timing,
128/// }
129///
130/// impl FeedBuilder {
131///     polyoxide_ws::timing_setters! {
132///         /// How long the feed may be silent, pings included.
133///         stale_after;
134///         /// Reconnect delay bounds.
135///         backoff;
136///     }
137/// }
138///
139/// let builder = FeedBuilder::default().stale_after(Duration::from_secs(45));
140/// assert_eq!(builder.timing.stale_after, Duration::from_secs(45));
141/// ```
142#[macro_export]
143macro_rules! timing_setters {
144    () => {};
145    ($(#[$meta:meta])* ping_interval; $($rest:tt)*) => {
146        $(#[$meta])*
147        pub fn ping_interval(mut self, interval: ::core::time::Duration) -> Self {
148            self.timing.ping_interval = ::core::option::Option::Some(interval);
149            self
150        }
151        $crate::timing_setters! { $($rest)* }
152    };
153    ($(#[$meta:meta])* stale_after; $($rest:tt)*) => {
154        $(#[$meta])*
155        pub fn stale_after(mut self, stale_after: ::core::time::Duration) -> Self {
156            self.timing.stale_after = stale_after;
157            self
158        }
159        $crate::timing_setters! { $($rest)* }
160    };
161    ($(#[$meta:meta])* backoff; $($rest:tt)*) => {
162        $(#[$meta])*
163        pub fn backoff(
164            mut self,
165            initial: ::core::time::Duration,
166            max: ::core::time::Duration,
167        ) -> Self {
168            self.timing.initial_backoff = initial;
169            self.timing.max_backoff = max;
170            self
171        }
172        $crate::timing_setters! { $($rest)* }
173    };
174    ($(#[$meta:meta])* connect_timeout; $($rest:tt)*) => {
175        $(#[$meta])*
176        pub fn connect_timeout(mut self, timeout: ::core::time::Duration) -> Self {
177            self.timing.connect_timeout = timeout;
178            self
179        }
180        $crate::timing_setters! { $($rest)* }
181    };
182    ($(#[$meta:meta])* max_connection_age; $($rest:tt)*) => {
183        $(#[$meta])*
184        pub fn max_connection_age(mut self, age: ::core::time::Duration) -> Self {
185            self.timing.max_connection_age = ::core::option::Option::Some(age);
186            self
187        }
188        $crate::timing_setters! { $($rest)* }
189    };
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    /// A builder that takes all five setters, as a venue's would.
197    #[derive(Debug, Default)]
198    struct Builder {
199        timing: Timing,
200    }
201
202    impl Builder {
203        crate::timing_setters! {
204            /// How often to ping.
205            ping_interval;
206            /// How long the connection may be silent.
207            stale_after;
208            /// Reconnect delay bounds.
209            backoff;
210            /// How long one connection attempt may take.
211            connect_timeout;
212            /// How long a connection may live.
213            max_connection_age;
214        }
215    }
216
217    fn secs(n: u64) -> Duration {
218        Duration::from_secs(n)
219    }
220
221    #[test]
222    fn the_defaults_are_the_shared_cadences() {
223        let timing = Timing::default();
224        assert_eq!(timing.ping_interval, Some(secs(20)));
225        assert_eq!(timing.stale_after, secs(30));
226        assert_eq!(timing.initial_backoff, Duration::from_millis(500));
227        assert_eq!(timing.max_backoff, secs(60));
228        assert_eq!(timing.connect_timeout, secs(10));
229        assert_eq!(timing.max_connection_age, None);
230    }
231
232    #[test]
233    fn each_setter_writes_its_own_field() {
234        let builder = Builder::default()
235            .ping_interval(secs(1))
236            .stale_after(secs(2))
237            .backoff(Duration::from_millis(3), Duration::from_millis(4))
238            .connect_timeout(secs(5))
239            .max_connection_age(secs(6));
240        assert_eq!(
241            builder.timing,
242            Timing {
243                ping_interval: Some(secs(1)),
244                stale_after: secs(2),
245                initial_backoff: Duration::from_millis(3),
246                max_backoff: Duration::from_millis(4),
247                connect_timeout: secs(5),
248                max_connection_age: Some(secs(6)),
249            }
250        );
251        assert_eq!(builder.timing.backoff().take(), Duration::from_millis(3));
252    }
253
254    #[test]
255    fn with_no_ping_and_no_age_the_wait_is_the_staleness_window_alone() {
256        let timing = Timing {
257            ping_interval: None,
258            max_connection_age: None,
259            ..Timing::default()
260        };
261        assert_eq!(timing.next_wait(secs(999), secs(12), secs(999)), secs(18));
262    }
263
264    #[test]
265    fn the_nearest_deadline_wins() {
266        let timing = Timing {
267            max_connection_age: Some(secs(100)),
268            ..Timing::default()
269        };
270        // Ping in 5 s, staleness in 10 s, rotation in 1 s.
271        assert_eq!(timing.next_wait(secs(15), secs(20), secs(99)), secs(1));
272        assert_eq!(timing.next_wait(secs(15), secs(20), secs(0)), secs(5));
273        assert_eq!(timing.next_wait(secs(0), secs(25), secs(0)), secs(5));
274    }
275
276    #[test]
277    fn no_deadline_at_all_waits_for_ever() {
278        assert_eq!(next_wait([]), Duration::MAX);
279        assert_eq!(
280            next_wait([(None, secs(1)), (None, secs(2)), (None, Duration::MAX)]),
281            Duration::MAX
282        );
283    }
284
285    #[test]
286    fn a_deadline_already_past_waits_zero() {
287        assert_eq!(next_wait([(Some(secs(30)), secs(31))]), Duration::ZERO);
288        assert_eq!(
289            next_wait([(Some(secs(30)), Duration::MAX), (None, secs(0))]),
290            Duration::ZERO
291        );
292    }
293
294    #[test]
295    fn a_limit_of_duration_max_saturates_rather_than_overflowing() {
296        // The limit two venues' suites pass to mean never.
297        assert_eq!(
298            next_wait([(Some(Duration::MAX), secs(1))]),
299            Duration::MAX - secs(1)
300        );
301        let never = Timing {
302            ping_interval: Some(Duration::MAX),
303            stale_after: Duration::MAX,
304            ..Timing::default()
305        };
306        assert_eq!(
307            never.next_wait(secs(1), secs(1), secs(1)),
308            Duration::MAX - secs(1)
309        );
310    }
311}