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 `tokio::time::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}