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, 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 /// Shape each fresh connection before anything is sent on it, such as
149 /// with [`Connection::min_request_interval`] for a venue that paces its
150 /// requests. By default the connection is left as it is.
151 fn configure(&self, connection: Connection) -> Connection {
152 connection
153 }
154
155 /// Put the membership on a fresh connection, paced as the venue requires.
156 /// A failure is [`recover`](Self::recover)ed as a failed attempt.
157 fn replay(
158 &mut self,
159 connection: &mut Connection,
160 membership: &Self::Membership,
161 ) -> impl Future<Output = Result<(), Self::Error>> + Send;
162
163 /// Make one change on a live connection, and record it in the membership
164 /// as far as it took effect. The caller gets the answer; an error keeps
165 /// the connection, unless the request outlived the staleness window.
166 fn apply(
167 &mut self,
168 connection: &mut Connection,
169 membership: &mut Self::Membership,
170 change: Self::Change,
171 ) -> impl Future<Output = Result<Self::Ack, Self::Error>> + Send;
172
173 /// What becomes of a change made during an outage. By default it is held
174 /// and [`apply`](Self::apply)'d once a connection is up.
175 fn record(
176 &mut self,
177 membership: &mut Self::Membership,
178 change: Self::Change,
179 ) -> Offline<Self::Change, Self::Ack, Self::Error> {
180 let _ = membership;
181 Offline::Held(change)
182 }
183
184 /// What one frame means: Text, Binary, Ping, Pong or Close.
185 fn decode(&mut self, frame: &Message) -> Decoded<Self::Event, Self::Error>;
186
187 /// Whether this frame, decoded so, proves the connection alive and
188 /// restarts the staleness window (D5).
189 fn is_live(&self, frame: &Message, decoded: &Decoded<Self::Event, Self::Error>) -> bool;
190
191 /// Whether this frame, decoded so, makes the connection one that
192 /// delivered something, which resets the backoff when it ends (D6).
193 fn delivered(&self, frame: &Message, decoded: &Decoded<Self::Event, Self::Error>) -> bool;
194
195 /// The ping to send at a ping tick, or `None` for none (D4). It is sent
196 /// without awaiting an answer; the answer is a frame like any other.
197 fn ping(&mut self, connection: &mut Connection) -> Option<Message>;
198
199 /// Whether the membership still wants a connection. One that stops being
200 /// wanted on a healthy connection closes it politely, with no marker, and
201 /// ends the stream; during an outage it ends the outage with
202 /// [`reconnected`](Self::reconnected) and then the stream.
203 fn wanted(membership: &Self::Membership) -> bool {
204 let _ = membership;
205 true
206 }
207
208 /// Whether reconnecting can help after this error. Required, since only
209 /// the venue knows its classes; it is what [`recover`](Self::recover)
210 /// reads by default.
211 fn is_retriable(err: &Self::Error) -> bool;
212
213 /// What to do after this error: by default
214 /// [`Reconnect`](Recovery::Reconnect) exactly when
215 /// [`is_retriable`](Self::is_retriable) says so, and
216 /// [`Stop`](Recovery::Stop) otherwise.
217 fn recover(&self, err: &Self::Error) -> Recovery {
218 if Self::is_retriable(err) {
219 Recovery::Reconnect
220 } else {
221 Recovery::Stop
222 }
223 }
224
225 /// How long the server asked the client to wait after this error, such
226 /// as a refused upgrade's `Retry-After`; `None` by default.
227 ///
228 /// The reconnect that follows waits at least this, clamped to
229 /// [`Timing::max_backoff`], and never less than the backoff's own step:
230 /// a server's wait only extends the delay (DRIFT R11). A venue answers
231 /// it from its error's class, the one place that reads the header.
232 fn retry_after(&self, err: &Self::Error) -> Option<Duration> {
233 let _ = err;
234 None
235 }
236
237 /// The event announcing an outage, or `None` for a protocol that
238 /// declares none (the default). Called at most once per outage.
239 fn disconnected(&mut self, outage: Outage<Self::Error>) -> Option<Self::Event> {
240 let _ = outage;
241 None
242 }
243
244 /// The event ending an outage, yielded exactly once per outage: after a
245 /// fresh connection has replayed the membership, or when the membership
246 /// stopped being wanted during the outage.
247 fn reconnected(&mut self) -> Self::Event;
248
249 /// Forget what belongs to the previous connection, before a fresh one
250 /// replays the membership.
251 fn reset(&mut self) {}
252
253 /// The queue between the task and the consumer.
254 fn queue(&self) -> Queue;
255
256 /// The error for a membership change, or a connection attempt, that the
257 /// server left unanswered for the staleness window. By default a
258 /// [`Kind::NoAnswer`](crate::Kind::NoAnswer).
259 fn stalled(silent: Duration) -> Self::Error {
260 WsError::no_answer(silent).into()
261 }
262
263 /// The error for a membership change, or a [`Supervised::close`], once
264 /// the task has stopped. By default a
265 /// [`Kind::AlreadyClosed`](crate::Kind::AlreadyClosed).
266 fn stopped() -> Self::Error {
267 WsError::from(tungstenite::Error::AlreadyClosed).into()
268 }
269}
270
271/// Starts a supervised connection for a [`Protocol`].
272///
273/// The task it spawns holds one connection at a time and keeps these rules
274/// (AD-11):
275///
276/// - **Pings on the wall clock.** A ping goes out every
277/// [`Timing::ping_interval`] whatever the traffic, and also while the
278/// consumer's queue is full: the task then races the queue's space against
279/// the ping timer and reads nothing more, and the time it waits does not
280/// count as silence. None goes out while a membership change is in flight,
281/// which [`Timing::stale_after`] bounds.
282/// - **Staleness.** Silence for [`Timing::stale_after`], by the protocol's
283/// liveness rule, replaces the connection.
284/// - **No drops.** No decoded event is dropped; a bounded queue holds the
285/// task back instead.
286/// - **Outage markers.** At most one [`Protocol::disconnected`] per outage,
287/// and exactly one [`Protocol::reconnected`], even when the membership
288/// stops being wanted during the outage.
289/// - **Not wanted.** A membership that stops being wanted on a healthy
290/// connection closes it politely, with no marker, and ends the stream.
291/// - **Rotation.** A connection older than [`Timing::max_connection_age`] is
292/// closed politely and replaced at once.
293/// - **Membership during an outage.** A change made during an outage goes to
294/// [`Protocol::record`], and a held one is applied after the reconnect.
295/// - **The consumer gone.** Dropping [`Supervised`] ends the task promptly,
296/// and [`Supervised::close`] does not wait on a consumer that never read.
297///
298/// Reconnects wait out [`Timing::backoff`]'s schedule, with no cap on
299/// attempts, or the [`Protocol::retry_after`] of the failure before them when
300/// that is longer, up to [`Timing::max_backoff`] (DRIFT R11); each attempt is
301/// bounded by [`Timing::connect_timeout`]. The task logs under the
302/// `polyoxide_ws` target.
303///
304/// The basis is one socket crate's former supervised tier (`ws/supervised.rs`,
305/// deleted in Story 4.6), with another socket crate's former loop (replaced
306/// in Story 4.7 by a router over one supervisor per path) for rotation,
307/// outage markers, changes recorded during an outage, the start in an outage
308/// and the consumer's departure. Neither kept pinging behind a full queue.
309#[derive(Debug)]
310pub struct Supervisor<P: Protocol> {
311 protocol: P,
312 timing: Timing,
313}
314
315impl<P: Protocol> Supervisor<P> {
316 /// A supervisor for `protocol` on `timing`.
317 pub fn new(protocol: P, timing: Timing) -> Self {
318 Self { protocol, timing }
319 }
320
321 /// Connect, replay `membership`, and start supervising. Any failure of
322 /// that first attempt is returned, so a setup that is wrong is reported
323 /// rather than retried.
324 pub async fn connect(
325 mut self,
326 mut membership: P::Membership,
327 ) -> Result<Supervised<P>, P::Error> {
328 let live = task::attempt(&mut self.protocol, &self.timing, &mut membership).await?;
329 Ok(self.spawn(membership, Start::Up(Box::new(live))))
330 }
331
332 /// As [`connect`](Self::connect), except that a first failure
333 /// [`Protocol::recover`] would retry starts the stream in an outage: the
334 /// protocol's [`disconnected`](Protocol::disconnected) for it, then the
335 /// backoff and the attempts that follow. Only a failure it would
336 /// [`Stop`](Recovery::Stop) on is returned.
337 pub async fn start(mut self, mut membership: P::Membership) -> Result<Supervised<P>, P::Error> {
338 match task::attempt(&mut self.protocol, &self.timing, &mut membership).await {
339 Ok(live) => Ok(self.spawn(membership, Start::Up(Box::new(live)))),
340 Err(err) => match self.protocol.recover(&err) {
341 Recovery::Stop => Err(err),
342 Recovery::Reconnect | Recovery::Skip => {
343 Ok(self.spawn(membership, Start::Outage(err)))
344 }
345 },
346 }
347 }
348
349 fn spawn(self, membership: P::Membership, start: Start<P>) -> Supervised<P> {
350 let (events, receiver) = task::queue(self.protocol.queue());
351 let (commands, commands_rx) = mpsc::channel(COMMANDS);
352 let task = Task::new(self.protocol, self.timing, membership, events, commands_rx);
353 Supervised {
354 events: receiver,
355 commands,
356 task: tokio::spawn(task.run(start)),
357 }
358 }
359}
360
361/// A supervised connection: a `Stream` of the protocol's events, and of the
362/// errors it skips past or stops on, that survives drops, stalls and
363/// rotations by reconnecting and replaying its membership.
364///
365/// It ends after an error [`Protocol::recover`] stops on, which it yields
366/// first, or when the membership stops being wanted. Dropping it ends the
367/// task, closing the connection politely.
368pub struct Supervised<P: Protocol> {
369 events: Receiver<task::Item<P>>,
370 commands: mpsc::Sender<Command<P>>,
371 /// Whether the task stopped on an error.
372 task: tokio::task::JoinHandle<bool>,
373}
374
375impl<P: Protocol> std::fmt::Debug for Supervised<P> {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 f.debug_struct("Supervised")
378 .field("running", &!self.task.is_finished())
379 .finish_non_exhaustive()
380 }
381}
382
383impl<P: Protocol> Supervised<P> {
384 /// A handle for changing the membership while the stream runs.
385 pub fn membership(&self) -> MembershipHandle<P> {
386 MembershipHandle {
387 commands: self.commands.clone(),
388 }
389 }
390
391 /// Close the connection politely and stop the task, without waiting on
392 /// the consumer's queue. Fails with [`Protocol::stopped`] if the task had
393 /// already stopped on an error, which the stream yielded. Every
394 /// [`MembershipHandle`] answers [`Protocol::stopped`] afterwards.
395 pub async fn close(self) -> Result<(), P::Error> {
396 let Self {
397 events,
398 commands,
399 task,
400 } = self;
401 // The task sees the queue's receiver go, wherever it is waiting,
402 // a full queue included.
403 drop(events);
404 drop(commands);
405 match task.await {
406 Ok(false) => Ok(()),
407 Ok(true) | Err(_) => Err(P::stopped()),
408 }
409 }
410}
411
412impl<P: Protocol> Stream for Supervised<P> {
413 type Item = Result<P::Event, P::Error>;
414
415 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
416 self.get_mut().events.poll_recv(cx)
417 }
418}
419
420/// Changes the membership of a running [`Supervised`].
421///
422/// A call waits for the task to take the change and for its answer. The task
423/// takes changes between frames on a live connection, and during an outage's
424/// backoff, but not while the consumer's queue is full: a call made from the
425/// task that reads the stream can then wait for ever. Make changes from
426/// another task, or keep reading while one is pending.
427pub struct MembershipHandle<P: Protocol> {
428 commands: mpsc::Sender<Command<P>>,
429}
430
431impl<P: Protocol> Clone for MembershipHandle<P> {
432 fn clone(&self) -> Self {
433 Self {
434 commands: self.commands.clone(),
435 }
436 }
437}
438
439impl<P: Protocol> std::fmt::Debug for MembershipHandle<P> {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 f.debug_struct("MembershipHandle")
442 .field("running", &!self.commands.is_closed())
443 .finish()
444 }
445}
446
447impl<P: Protocol> MembershipHandle<P> {
448 /// Make one change, and wait for its answer: from
449 /// [`Protocol::apply`] on a live connection, from [`Protocol::record`]
450 /// during an outage, [`Protocol::stalled`] for a request left unanswered
451 /// for the staleness window, or [`Protocol::stopped`] once the task has
452 /// stopped.
453 pub async fn apply(&self, change: P::Change) -> Result<P::Ack, P::Error> {
454 let (reply, answer) = oneshot::channel();
455 self.commands
456 .send(Command { change, reply })
457 .await
458 .map_err(|_| P::stopped())?;
459 answer.await.map_err(|_| P::stopped())?
460 }
461}
462
463#[cfg(test)]
464mod tests;