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