Skip to main content

polyoxide_ws/supervisor/
mod.rs

1//! The generic supervision loop (AD-11): one task per supervised connection,
2//! which keeps it alive, replaces it, replays its membership and reports its
3//! outages, for any venue that describes its protocol as a [`Protocol`].
4
5mod task;
6
7use std::{
8    future::Future,
9    pin::Pin,
10    task::{Context, Poll},
11    time::Duration,
12};
13
14use futures_util::Stream;
15use tokio::sync::{mpsc, oneshot};
16
17use crate::{
18    tungstenite::{self, handshake::client::Request, Message},
19    Connection, Socket, Timing, WsError,
20};
21use task::{Command, Receiver, Start, Task};
22
23/// How many membership changes may wait for the task before a caller waits
24/// to hand one over.
25const COMMANDS: usize = 16;
26
27/// What to do after a failure.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Recovery {
31    /// Replace the connection: back off, connect again and replay the
32    /// membership.
33    Reconnect,
34    /// Yield the error and keep the connection, such as for one frame that
35    /// did not decode. A failed connection attempt has no connection to keep,
36    /// so there it reads as [`Reconnect`](Self::Reconnect).
37    Skip,
38    /// Yield the error and end the stream: retrying replays the same failure.
39    Stop,
40}
41
42/// The queue between the task and the consumer.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum Queue {
46    /// At most this many items unread (at least one). While it is full the
47    /// task reads nothing more from the connection, and still pings.
48    Bounded(usize),
49    /// No limit: the task never waits for the consumer.
50    Unbounded,
51}
52
53/// What one frame decodes to.
54#[derive(Debug)]
55#[non_exhaustive]
56pub enum Decoded<E, Err> {
57    /// Nothing for the consumer: a control frame, an answer the protocol
58    /// reads itself, or a frame it skips.
59    Nothing,
60    /// One event.
61    Event(E),
62    /// Several events, in order, such as one the protocol originates ahead of
63    /// the update it concerns.
64    Events(Vec<E>),
65    /// A failure, which [`Protocol::recover`] decides.
66    Error(Err),
67}
68
69/// What becomes of a membership change made while no connection is up.
70#[derive(Debug)]
71#[non_exhaustive]
72pub enum Offline<C, A, E> {
73    /// Recorded in the membership for the replay to apply; the caller gets
74    /// this answer now.
75    Recorded(Result<A, E>),
76    /// Held, and applied with [`Protocol::apply`] once a connection is up and
77    /// its membership replayed; the caller waits for that answer.
78    Held(C),
79}
80
81/// Why a connection went down, for [`Protocol::disconnected`], which owns it,
82/// so a protocol's marker can carry the error.
83#[derive(Debug)]
84#[non_exhaustive]
85pub enum Outage<E> {
86    /// The server closed the connection: with a close frame, whose code and
87    /// reason these are, or by ending the stream with none (no code and an
88    /// empty reason).
89    Closed {
90        /// The close code, when a close frame carried one.
91        code: Option<u16>,
92        /// The close reason, empty when none was given.
93        reason: String,
94    },
95    /// A failure: a read, a ping, a decoded frame [`Protocol::recover`] sent
96    /// to a reconnect, or the first connection attempt of
97    /// [`Supervisor::start`].
98    Error(E),
99    /// Nothing that counts as liveness arrived for the staleness window, or a
100    /// membership change went unanswered for it.
101    Stale {
102        /// The silence, less any time the consumer's queue was full.
103        silent: Duration,
104    },
105    /// Replaced on purpose at [`Timing::max_connection_age`].
106    Rotation,
107}
108
109/// A venue's socket protocol, as the [`Supervisor`] drives it.
110///
111/// The supervisor owns the rules every venue shares (AD-11): pings on the
112/// wall clock, staleness, backoff, rotation, the outage markers and the
113/// consumer's queue. A protocol says what is its own, and each divergence row
114/// stays here, at the venue:
115///
116/// - **D4, the ping:** [`ping`](Self::ping), sent every
117///   [`Timing::ping_interval`] whatever the traffic.
118/// - **D5, liveness:** [`is_live`](Self::is_live), which restarts the
119///   staleness window.
120/// - **D6, a delivered connection:** [`delivered`](Self::delivered), which
121///   resets the backoff when the connection ends.
122/// - **D8, outage markers:** [`disconnected`](Self::disconnected), or none.
123/// - **D19 and the rest of recovery:** [`recover`](Self::recover).
124///
125/// The hooks that talk to the server, [`replay`](Self::replay) and
126/// [`apply`](Self::apply), borrow the [`Connection`], so they use its request
127/// correlation. A request is not cancel-safe, so the supervisor bounds each by
128/// [`Timing::stale_after`] and replaces a connection that outlives it.
129///
130/// The trait is generic only, and its async hooks return `impl Future + Send`.
131pub trait Protocol: Send + Sized + 'static {
132    /// What the stream yields.
133    type Event: Send + 'static;
134    /// What the stream and the membership calls fail with. Every socket
135    /// failure arrives as a [`WsError`] first.
136    type Error: std::error::Error + From<WsError> + Send + 'static;
137    /// What the connection carries, such as its subscriptions.
138    type Membership: Send + 'static;
139    /// One change to the membership.
140    type Change: Send + 'static;
141    /// The answer to a change.
142    type Ack: Send + 'static;
143
144    /// The handshake request for one connection attempt, built afresh for
145    /// each, so any authentication in it is recomputed.
146    fn handshake(&mut self) -> Result<Request, Self::Error>;
147
148    /// Open the connection for one attempt, within `timeout`.
149    ///
150    /// The [`Supervisor`] calls it for every attempt, the first and each
151    /// reconnect, with [`Timing::connect_timeout`]. By default it builds
152    /// [`handshake`](Self::handshake)'s request first, so a failure there is
153    /// the attempt's, and dials it with the kit's [`connect`](crate::connect).
154    /// A protocol that dials its venue a step at a time, such as a venue that
155    /// tries each resolved address in turn, overrides it: it bounds each step
156    /// by `timeout` itself, upgrades with [`connect_over`](crate::connect_over),
157    /// and may keep `handshake` for the request it upgrades with. An error is a
158    /// failed attempt, which [`recover`](Self::recover) decides on.
159    fn connect(
160        &mut self,
161        timeout: Duration,
162    ) -> impl Future<Output = Result<Socket, Self::Error>> + Send {
163        let request = self.handshake();
164        async move {
165            let socket = crate::connect(request?, timeout).await?;
166            Ok::<_, Self::Error>(socket)
167        }
168    }
169
170    /// Shape each fresh connection before anything is sent on it, such as
171    /// with [`Connection::min_request_interval`] for a venue that paces its
172    /// requests. By default the connection is left as it is.
173    fn configure(&self, connection: Connection) -> Connection {
174        connection
175    }
176
177    /// Put the membership on a fresh connection, paced as the venue requires.
178    /// A failure is [`recover`](Self::recover)ed as a failed attempt.
179    fn replay(
180        &mut self,
181        connection: &mut Connection,
182        membership: &Self::Membership,
183    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
184
185    /// Make one change on a live connection, and record it in the membership
186    /// as far as it took effect. The caller gets the answer; an error keeps
187    /// the connection, unless the request outlived the staleness window.
188    fn apply(
189        &mut self,
190        connection: &mut Connection,
191        membership: &mut Self::Membership,
192        change: Self::Change,
193    ) -> impl Future<Output = Result<Self::Ack, Self::Error>> + Send;
194
195    /// What becomes of a change made during an outage. By default it is held
196    /// and [`apply`](Self::apply)'d once a connection is up.
197    fn record(
198        &mut self,
199        membership: &mut Self::Membership,
200        change: Self::Change,
201    ) -> Offline<Self::Change, Self::Ack, Self::Error> {
202        let _ = membership;
203        Offline::Held(change)
204    }
205
206    /// What one frame means: Text, Binary, Ping, Pong or Close.
207    fn decode(&mut self, frame: &Message) -> Decoded<Self::Event, Self::Error>;
208
209    /// Whether this frame, decoded so, proves the connection alive and
210    /// restarts the staleness window (D5).
211    fn is_live(&self, frame: &Message, decoded: &Decoded<Self::Event, Self::Error>) -> bool;
212
213    /// Whether this frame, decoded so, makes the connection one that
214    /// delivered something, which resets the backoff when it ends (D6).
215    fn delivered(&self, frame: &Message, decoded: &Decoded<Self::Event, Self::Error>) -> bool;
216
217    /// The ping to send at a ping tick, or `None` for none (D4). It is sent
218    /// without awaiting an answer; the answer is a frame like any other.
219    fn ping(&mut self, connection: &mut Connection) -> Option<Message>;
220
221    /// A frame to send right after a frame was decoded, or `None` for none (the
222    /// default): how a protocol asks the server for something its last frame
223    /// showed it needs, such as the state of a stream it can no longer trust.
224    ///
225    /// The supervisor calls it once after each [`decode`](Self::decode) that
226    /// leaves the connection up: not after a decode error that reconnects or
227    /// stops, and not on a connection that has already ended, such as after
228    /// the server's close frame. What it returns is sent on the same
229    /// connection, one frame for each call, within [`Timing::stale_after`],
230    /// and the answer is not awaited: it is a frame like any other. A send
231    /// that fails, or outlives that window, ends the connection as a failed
232    /// [`ping`](Self::ping) does, and [`recover`](Self::recover) decides what
233    /// follows.
234    ///
235    /// A protocol with several frames to send returns one for each call, so
236    /// the rest go out after later frames. Every frame the connection yields
237    /// is decoded, the server's pings, pongs and unsolicited answers
238    /// included, so a held frame goes out after the next frame of any kind,
239    /// and on a connection that receives nothing it waits. What it still
240    /// holds when the connection ends stays with it, and is cleared in
241    /// [`reset`](Self::reset).
242    fn follow_up(&mut self) -> Option<Message> {
243        None
244    }
245
246    /// Whether the membership still wants a connection. One that stops being
247    /// wanted on a healthy connection closes it politely, with no marker, and
248    /// ends the stream; during an outage it ends the outage with
249    /// [`reconnected`](Self::reconnected) and then the stream.
250    fn wanted(membership: &Self::Membership) -> bool {
251        let _ = membership;
252        true
253    }
254
255    /// Whether reconnecting can help after this error. Required, since only
256    /// the venue knows its classes; it is what [`recover`](Self::recover)
257    /// reads by default.
258    fn is_retriable(err: &Self::Error) -> bool;
259
260    /// What to do after this error: by default
261    /// [`Reconnect`](Recovery::Reconnect) exactly when
262    /// [`is_retriable`](Self::is_retriable) says so, and
263    /// [`Stop`](Recovery::Stop) otherwise.
264    fn recover(&self, err: &Self::Error) -> Recovery {
265        if Self::is_retriable(err) {
266            Recovery::Reconnect
267        } else {
268            Recovery::Stop
269        }
270    }
271
272    /// How long the server asked the client to wait after this error, such
273    /// as a refused upgrade's `Retry-After`; `None` by default.
274    ///
275    /// The reconnect that follows waits at least this, clamped to
276    /// [`Timing::max_backoff`], and never less than the backoff's own step:
277    /// a server's wait only extends the delay (DRIFT R11). A venue answers
278    /// it from its error's class, the one place that reads the header.
279    fn retry_after(&self, err: &Self::Error) -> Option<Duration> {
280        let _ = err;
281        None
282    }
283
284    /// The event announcing an outage, or `None` for a protocol that
285    /// declares none (the default). Called at most once per outage.
286    fn disconnected(&mut self, outage: Outage<Self::Error>) -> Option<Self::Event> {
287        let _ = outage;
288        None
289    }
290
291    /// The event ending an outage, yielded exactly once per outage: after a
292    /// fresh connection has replayed the membership, or when the membership
293    /// stopped being wanted during the outage.
294    fn reconnected(&mut self) -> Self::Event;
295
296    /// Forget what belongs to the previous connection, before a fresh one
297    /// replays the membership.
298    fn reset(&mut self) {}
299
300    /// The queue between the task and the consumer.
301    fn queue(&self) -> Queue;
302
303    /// The error for a membership change, or a connection attempt, that the
304    /// server left unanswered for the staleness window. By default a
305    /// [`Kind::NoAnswer`](crate::Kind::NoAnswer).
306    fn stalled(silent: Duration) -> Self::Error {
307        WsError::no_answer(silent).into()
308    }
309
310    /// The error for a membership change, or a [`Supervised::close`], once
311    /// the task has stopped. By default a
312    /// [`Kind::AlreadyClosed`](crate::Kind::AlreadyClosed).
313    fn stopped() -> Self::Error {
314        WsError::from(tungstenite::Error::AlreadyClosed).into()
315    }
316}
317
318/// Starts a supervised connection for a [`Protocol`].
319///
320/// The task it spawns holds one connection at a time and keeps these rules
321/// (AD-11):
322///
323/// - **Pings on the wall clock.** A ping goes out every
324///   [`Timing::ping_interval`] whatever the traffic, and also while the
325///   consumer's queue is full: the task then races the queue's space against
326///   the ping timer and reads nothing more, and the time it waits does not
327///   count as silence. None goes out while a membership change is in flight,
328///   which [`Timing::stale_after`] bounds.
329/// - **Staleness.** Silence for [`Timing::stale_after`], by the protocol's
330///   liveness rule, replaces the connection.
331/// - **Follow-ups.** After each decoded frame that leaves the connection up,
332///   the frame [`Protocol::follow_up`] returns is sent within
333///   [`Timing::stale_after`]; a send that fails or outlives it replaces the
334///   connection, as a failed ping does.
335/// - **No drops.** No decoded event is dropped; a bounded queue holds the
336///   task back instead.
337/// - **Outage markers.** At most one [`Protocol::disconnected`] per outage,
338///   and exactly one [`Protocol::reconnected`], even when the membership
339///   stops being wanted during the outage.
340/// - **Not wanted.** A membership that stops being wanted on a healthy
341///   connection closes it politely, with no marker, and ends the stream.
342/// - **Rotation.** A connection older than [`Timing::max_connection_age`] is
343///   closed politely and replaced at once.
344/// - **Membership during an outage.** A change made during an outage goes to
345///   [`Protocol::record`], and a held one is applied after the reconnect.
346/// - **The consumer gone.** Dropping [`Supervised`] ends the task promptly,
347///   and [`Supervised::close`] does not wait on a consumer that never read.
348///
349/// Reconnects wait out [`Timing::backoff`]'s schedule, with no cap on
350/// attempts, or the [`Protocol::retry_after`] of the failure before them when
351/// that is longer, up to [`Timing::max_backoff`] (DRIFT R11); each attempt is
352/// bounded by [`Timing::connect_timeout`]. The task logs under the
353/// `polyoxide_ws` target.
354///
355/// The basis is one socket crate's former supervised tier (`ws/supervised.rs`,
356/// deleted in Story 4.6), with another socket crate's former loop (replaced
357/// in Story 4.7 by a router over one supervisor per path) for rotation,
358/// outage markers, changes recorded during an outage, the start in an outage
359/// and the consumer's departure. Neither kept pinging behind a full queue.
360#[derive(Debug)]
361pub struct Supervisor<P: Protocol> {
362    protocol: P,
363    timing: Timing,
364}
365
366impl<P: Protocol> Supervisor<P> {
367    /// A supervisor for `protocol` on `timing`.
368    pub fn new(protocol: P, timing: Timing) -> Self {
369        Self { protocol, timing }
370    }
371
372    /// Connect, replay `membership`, and start supervising. Any failure of
373    /// that first attempt is returned, so a setup that is wrong is reported
374    /// rather than retried.
375    pub async fn connect(
376        mut self,
377        mut membership: P::Membership,
378    ) -> Result<Supervised<P>, P::Error> {
379        let live = task::attempt(&mut self.protocol, &self.timing, &mut membership).await?;
380        Ok(self.spawn(membership, Start::Up(Box::new(live))))
381    }
382
383    /// As [`connect`](Self::connect), except that a first failure
384    /// [`Protocol::recover`] would retry starts the stream in an outage: the
385    /// protocol's [`disconnected`](Protocol::disconnected) for it, then the
386    /// backoff and the attempts that follow. Only a failure it would
387    /// [`Stop`](Recovery::Stop) on is returned.
388    pub async fn start(mut self, mut membership: P::Membership) -> Result<Supervised<P>, P::Error> {
389        match task::attempt(&mut self.protocol, &self.timing, &mut membership).await {
390            Ok(live) => Ok(self.spawn(membership, Start::Up(Box::new(live)))),
391            Err(err) => match self.protocol.recover(&err) {
392                Recovery::Stop => Err(err),
393                Recovery::Reconnect | Recovery::Skip => {
394                    Ok(self.spawn(membership, Start::Outage(err)))
395                }
396            },
397        }
398    }
399
400    fn spawn(self, membership: P::Membership, start: Start<P>) -> Supervised<P> {
401        let (events, receiver) = task::queue(self.protocol.queue());
402        let (commands, commands_rx) = mpsc::channel(COMMANDS);
403        let task = Task::new(self.protocol, self.timing, membership, events, commands_rx);
404        Supervised {
405            events: receiver,
406            commands,
407            task: tokio::spawn(task.run(start)),
408        }
409    }
410}
411
412/// A supervised connection: a `Stream` of the protocol's events, and of the
413/// errors it skips past or stops on, that survives drops, stalls and
414/// rotations by reconnecting and replaying its membership.
415///
416/// It ends after an error [`Protocol::recover`] stops on, which it yields
417/// first, or when the membership stops being wanted. Dropping it ends the
418/// task, closing the connection politely.
419pub struct Supervised<P: Protocol> {
420    events: Receiver<task::Item<P>>,
421    commands: mpsc::Sender<Command<P>>,
422    /// Whether the task stopped on an error.
423    task: tokio::task::JoinHandle<bool>,
424}
425
426impl<P: Protocol> std::fmt::Debug for Supervised<P> {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        f.debug_struct("Supervised")
429            .field("running", &!self.task.is_finished())
430            .finish_non_exhaustive()
431    }
432}
433
434impl<P: Protocol> Supervised<P> {
435    /// A handle for changing the membership while the stream runs.
436    pub fn membership(&self) -> MembershipHandle<P> {
437        MembershipHandle {
438            commands: self.commands.clone(),
439        }
440    }
441
442    /// Close the connection politely and stop the task, without waiting on
443    /// the consumer's queue. Fails with [`Protocol::stopped`] if the task had
444    /// already stopped on an error, which the stream yielded. Every
445    /// [`MembershipHandle`] answers [`Protocol::stopped`] afterwards.
446    pub async fn close(self) -> Result<(), P::Error> {
447        let Self {
448            events,
449            commands,
450            task,
451        } = self;
452        // The task sees the queue's receiver go, wherever it is waiting,
453        // a full queue included.
454        drop(events);
455        drop(commands);
456        match task.await {
457            Ok(false) => Ok(()),
458            Ok(true) | Err(_) => Err(P::stopped()),
459        }
460    }
461}
462
463impl<P: Protocol> Stream for Supervised<P> {
464    type Item = Result<P::Event, P::Error>;
465
466    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
467        self.get_mut().events.poll_recv(cx)
468    }
469}
470
471/// Changes the membership of a running [`Supervised`].
472///
473/// A call waits for the task to take the change and for its answer. The task
474/// takes changes between frames on a live connection, and during an outage's
475/// backoff, but not while the consumer's queue is full: a call made from the
476/// task that reads the stream can then wait for ever. Make changes from
477/// another task, or keep reading while one is pending.
478pub struct MembershipHandle<P: Protocol> {
479    commands: mpsc::Sender<Command<P>>,
480}
481
482impl<P: Protocol> Clone for MembershipHandle<P> {
483    fn clone(&self) -> Self {
484        Self {
485            commands: self.commands.clone(),
486        }
487    }
488}
489
490impl<P: Protocol> std::fmt::Debug for MembershipHandle<P> {
491    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492        f.debug_struct("MembershipHandle")
493            .field("running", &!self.commands.is_closed())
494            .finish()
495    }
496}
497
498impl<P: Protocol> MembershipHandle<P> {
499    /// Make one change, and wait for its answer: from
500    /// [`Protocol::apply`] on a live connection, from [`Protocol::record`]
501    /// during an outage, [`Protocol::stalled`] for a request left unanswered
502    /// for the staleness window, or [`Protocol::stopped`] once the task has
503    /// stopped.
504    pub async fn apply(&self, change: P::Change) -> Result<P::Ack, P::Error> {
505        let (reply, answer) = oneshot::channel();
506        self.commands
507            .send(Command { change, reply })
508            .await
509            .map_err(|_| P::stopped())?;
510        answer.await.map_err(|_| P::stopped())?
511    }
512}
513
514#[cfg(test)]
515mod tests;