Skip to main content

xmtp_api_backend/queries/
bidi.rs

1//! The native bidirectional connection actor owns both stream halves.
2//!
3//! It sends all request frames, answers server pings, and matches probe pongs.
4//! It forwards subscription events in wire order. The binding supplies frame
5//! types and splits envelope batches. The actor also checks silence, bounds
6//! outbound backlog, and drains after request half-close.
7//!
8//! When the response stream ends, the actor releases both halves. Later sends
9//! fail through the closed command channel. The lease ledger owns reconnect.
10
11use parking_lot::Mutex;
12use std::collections::{HashMap, VecDeque};
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
15use std::time::Duration;
16
17use futures::StreamExt;
18use futures::stream::BoxStream;
19use tokio::sync::{mpsc, oneshot};
20use xmtp_common::{AbortHandle, MaybeSend, MaybeSync, RetryableError, StreamHandle};
21use xmtp_proto::types::Topic;
22
23/// Wire-outbound depth. The actor is the sole writer; a transport that stops
24/// draining backs frames up here first, then in the actor's `pending` queue,
25/// until [`MAX_PENDING_FRAMES`] declares the wire wedged and the actor gives up.
26pub(crate) const WIRE_BUFFER: usize = 64;
27/// Caller→actor command depth.
28pub(crate) const COMMAND_BUFFER: usize = 64;
29/// Actor→caller event depth; large enough that a brief consumer stall doesn't
30/// stall wire reads (and thus pong liveness).
31pub(crate) const EVENT_BUFFER: usize = 1024;
32/// Default keepalive until Started supplies the server interval.
33pub(crate) const DEFAULT_KEEPALIVE_MS: u32 = 30_000;
34/// The default probe deadline multiplier
35/// is this many keepalive intervals — generous enough not to false-positive a
36/// slow-but-live link. Latency-sensitive callers (e.g. a notification handler)
37/// pass a much smaller bound to [`Connection::probe_within`].
38///
39/// It is also the actor's total silence budget: after `N - 1` intervals with no
40/// inbound frame the actor sends a watchdog ping, and after the full `N` it
41/// tears down (see [`watchdog_deadline`]).
42pub(crate) const PROBE_TIMEOUT_MULTIPLIER: u32 = 3;
43/// Nonce for the actor's own watchdog pings. Client probes mint their nonces
44/// via [`next_probe_nonce`], which skips this value, so they can never collide
45/// with it — and the watchdog doesn't correlate the pong anyway: *any* inbound
46/// frame proves the link and resets the window.
47const WATCHDOG_NONCE: u64 = u64::MAX;
48
49/// Mint a client probe nonce: counts up from 1, skipping `0` and the reserved
50/// [`WATCHDOG_NONCE`] when the counter wraps. The loop settles within three
51/// steps — there are only two reserved values.
52fn next_probe_nonce(counter: &AtomicU64) -> u64 {
53    loop {
54        let nonce = counter.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
55        if nonce != WATCHDOG_NONCE && nonce != 0 {
56            return nonce;
57        }
58    }
59}
60/// Hard cap on the outbound backlog. Past this, the wire has been wedged long
61/// enough that buffering more is pointless — the transport isn't draining the
62/// request half, so the link is effectively dead — and the actor gives up,
63/// tearing down so the consumer re-opens from cursors on a fresh stream.
64/// Reached only under a sustained stall while commands or auto-pongs keep
65/// arriving; tearing down here (rather than parking callers forever) is also
66/// what keeps a queued `Finish` from starving behind a wedged wire.
67pub(crate) const MAX_PENDING_FRAMES: usize = WIRE_BUFFER * 2;
68/// Total time the post-`finish` drain will wait for wire capacity while flushing
69/// already-accepted frames. Generous for a transient stall (a draining transport
70/// frees a slot in milliseconds) yet bounded, so a wedged transport can't hold
71/// the half-close hostage — see [`drain_after_finish`].
72const DRAIN_FLUSH_BUDGET: Duration = Duration::from_secs(1);
73
74/// Backend-specific wire vocabulary for a bidi subscription. The control core is
75/// generic over this: a binding names the wire request/response types and the
76/// per-backend `Mutate` and message types, builds outbound frames, and — via
77/// [`BidiBinding::handle`] — classifies an inbound response into an [`Inbound`]
78/// instruction.
79pub trait BidiBinding: Send + 'static {
80    /// Outbound wire frame (client → server).
81    type Request: Send + 'static;
82    /// Inbound wire frame (server → client).
83    type Response: Send + 'static;
84    /// The backend update payload.
85    type Mutate: Send;
86    /// Encrypted group envelope for the consumer.
87    type GroupMessage: MaybeSend + MaybeSync;
88    /// Encrypted welcome envelope for the consumer.
89    type WelcomeMessage: MaybeSend + MaybeSync;
90
91    /// Wrap a `Mutate` as an outbound request frame.
92    fn mutate_frame(mutate: Self::Mutate) -> Self::Request;
93    /// A client `Ping` request frame (liveness probe).
94    fn ping_frame(nonce: u64) -> Self::Request;
95    /// A `Pong` request frame answering a server `Ping`.
96    fn pong_frame(nonce: u64) -> Self::Request;
97
98    /// Classify one inbound response into an actor instruction. An associated
99    /// fn, like the frame constructors: bindings are stateless by design — any
100    /// stateful ordering or cursor tracking belongs to the consumer, not a
101    /// single connection actor.
102    fn handle(response: Self::Response) -> Inbound<Self::GroupMessage, Self::WelcomeMessage>;
103}
104
105/// What an inbound frame means to the control core, after the binding classifies
106/// it. Liveness (`Ping`/`Pong`) is handled by the actor and never surfaced;
107/// everything else becomes a consumer [`Event`].
108pub enum Inbound<G, W> {
109    /// Server ping — the actor auto-pongs this nonce.
110    Ping(u64),
111    /// Server pong — the actor resolves the matching client probe.
112    Pong(u64),
113    /// A single consumer event to surface (handshake / markers).
114    Emit(Event<G, W>),
115    /// A delivery batch — the actor emits `GroupMessages` then
116    /// `WelcomeMessages`. Empty batches produce no event.
117    Messages { group: Vec<G>, welcome: Vec<W> },
118    /// Nothing to do — unknown version or an informational/undecodable frame.
119    Skip,
120    /// The frame cannot establish an ordered delivery prefix.
121    Invalid(&'static str),
122}
123
124#[derive(Debug, thiserror::Error)]
125pub enum ConnectionFailure {
126    #[error("subscription stream failed: {0}")]
127    Wire(#[source] xmtp_proto::api::NetworkError),
128    #[error("invalid subscription frame: {0}")]
129    Protocol(&'static str),
130}
131
132impl RetryableError for ConnectionFailure {
133    fn is_retryable(&self) -> bool {
134        match self {
135            Self::Wire(error) => error.is_retryable(),
136            Self::Protocol(_) => false,
137        }
138    }
139}
140
141type FailureSlot = Arc<Mutex<Option<Arc<ConnectionFailure>>>>;
142
143/// Events surfaced to the consumer, in wire order. `Ping`/`Pong` never appear —
144/// liveness lives entirely inside the actor. Generic over the backend's group
145/// and welcome message types.
146#[derive(Debug, Clone, PartialEq)]
147pub enum Event<G, W> {
148    /// The first response supplies the server keepalive interval.
149    Started { keepalive_interval_ms: u32 },
150    /// An accepted update supplies fixed targets for newly added topics.
151    Applied { id: u64, targets: Vec<(Topic, u64)> },
152    /// Group envelopes in wire order.
153    GroupMessages { messages: Vec<G> },
154    /// Welcome envelopes in wire order.
155    WelcomeMessages { messages: Vec<W> },
156}
157
158#[derive(Debug, thiserror::Error)]
159pub enum BidiError {
160    #[error("the bidi connection is closed; re-open and resume from durable cursors")]
161    Closed,
162    #[error("liveness probe timed out; treat the link as dead, drop it, and re-open")]
163    ProbeTimedOut,
164}
165
166impl RetryableError for BidiError {
167    fn is_retryable(&self) -> bool {
168        true
169    }
170}
171
172/// Outcome of a [`Connection::try_mutate`] that could not be accepted; both
173/// variants hand the mutate back to the caller.
174#[derive(Debug)]
175pub enum TryMutateError<M> {
176    /// The command buffer is momentarily full — retry once the actor has
177    /// made progress (e.g. after the caller drains more events).
178    Full(M),
179    /// The connection is finished or its actor is gone; this update can only be
180    /// re-issued on a fresh connection.
181    Closed(M),
182}
183
184/// Submitted by the handle, performed by the actor (the sole wire writer).
185enum Command<B: BidiBinding> {
186    Mutate(B::Mutate),
187    /// A client liveness probe. The actor sends the `Ping` and fires `ack` when
188    /// the matching `Pong` returns; if the actor exits first, `ack` is dropped
189    /// and the waiting [`Connection::probe`] resolves to `Closed`.
190    Probe {
191        nonce: u64,
192        ack: oneshot::Sender<()>,
193    },
194    /// Half-close the request half. The actor drops its wire sender so the
195    /// outbound stream ends (the server sees the half-close), then drains inbound
196    /// to completion. No further outbound frames are sent after this.
197    Finish,
198}
199
200/// A handle to one open bidirectional subscription. Writing to the wire is the
201/// actor's job; this only submits commands and reads events. Generic over the
202/// backend [`BidiBinding`]. The backend module supplies the concrete alias.
203pub struct Connection<B: BidiBinding> {
204    commands: mpsc::Sender<Command<B>>,
205    events: mpsc::Receiver<Event<B::GroupMessage, B::WelcomeMessage>>,
206    probe_nonce: AtomicU64,
207    /// Latched `true` once a `finish` has *delivered* `Command::Finish` to the
208    /// actor; monotonic — set once on delivery, never cleared. Lets `mutate`/
209    /// `probe` observe `Closed` without racing the actor's teardown of the
210    /// command channel: after `finish` returns, `Command::Finish` is still in
211    /// flight and the receiver lives until the actor drains it, so a
212    /// FIFO-following `mutate` would otherwise be accepted into the buffer and
213    /// then silently dropped. Latching only on delivery (not before the send) is
214    /// what makes a cancelled `finish` harmless and concurrent `finish` calls
215    /// race-free — there is no reset to race.
216    finished: AtomicBool,
217    /// The server's advertised keepalive cadence (ms), recorded when
218    /// [`Self::next`] surfaces `Started`; `0` until then. Drives the default
219    /// probe deadline so `probe` can self-bound without the caller re-deriving
220    /// it. A probe issued before `Started` has been consumed falls back to the
221    /// 30s-derived default, which the probe docs already sanction as safe.
222    keepalive_ms: u32,
223    actor: Box<dyn AbortHandle>,
224    failure: FailureSlot,
225}
226
227impl<B: BidiBinding> Connection<B> {
228    /// Open the stream: seed `initial` as the first request frame (it names the
229    /// initial topic set with per-topic resume cursors),
230    /// then hand the outbound frame stream to `transport` to obtain the inbound
231    /// frame stream, and spawn the actor. The backend modules wrap this with an
232    /// ergonomic `open(api, initial)`.
233    pub(crate) async fn start<T, Fut, S, E>(initial: B::Mutate, transport: T) -> Result<Self, E>
234    where
235        T: FnOnce(BoxStream<'static, B::Request>) -> Fut,
236        Fut: Future<Output = Result<S, E>>,
237        S: futures::Stream<Item = Result<B::Response, E>> + Send + 'static,
238        E: RetryableError + Send + 'static,
239    {
240        let (wire_out, mut wire_out_rx) = mpsc::channel(WIRE_BUFFER);
241        let (commands_tx, commands_rx) = mpsc::channel(COMMAND_BUFFER);
242        let (event_tx, events) = mpsc::channel(EVENT_BUFFER);
243        let failure = FailureSlot::default();
244
245        // The first request frame carries the initial update. Seed it into
246        // the fresh, empty wire channel before the transport or the actor can
247        // write anything else; the receiver is still held here, so a fresh,
248        // empty, bounded channel can neither be full nor closed — `try_send`
249        // makes that "cannot block" invariant structural.
250        wire_out
251            .try_send(B::mutate_frame(initial))
252            .unwrap_or_else(|_| {
253                unreachable!("send into a fresh, empty, owned channel cannot fail")
254            });
255
256        // Wrap the receiver as a `Stream` without pulling in `tokio-stream`:
257        // `poll_recv` is exactly the poll fn a `Stream` needs.
258        let outbound = futures::stream::poll_fn(move |cx| wire_out_rx.poll_recv(cx));
259        let inbound = transport(Box::pin(outbound)).await?;
260
261        let actor = xmtp_common::spawn(
262            None,
263            // Box::pin makes the inbound stream `Unpin` for the select loop without
264            // requiring the transport's stream type to be `Unpin` itself.
265            run_actor::<B, _, _>(
266                Box::pin(inbound),
267                wire_out,
268                commands_rx,
269                event_tx,
270                failure.clone(),
271            ),
272        );
273
274        Ok(Self {
275            commands: commands_tx,
276            events,
277            probe_nonce: AtomicU64::new(0),
278            finished: AtomicBool::new(false),
279            keepalive_ms: 0,
280            actor: actor.abort_handle(),
281            failure,
282        })
283    }
284
285    /// Add/remove subscriptions in place. Awaits a free command slot
286    /// (backpressure is right for a state change); returns `Closed` once the
287    /// actor has stopped — the command receiver dies with it.
288    pub async fn mutate(&self, mutate: B::Mutate) -> Result<(), BidiError> {
289        if self.finished.load(Ordering::Acquire) {
290            return Err(BidiError::Closed);
291        }
292        self.commands
293            .send(Command::Mutate(mutate))
294            .await
295            .map_err(|_| BidiError::Closed)
296    }
297
298    /// Non-blocking [`Self::mutate`]: accept the update into the command buffer or
299    /// hand it straight back. Exists for callers that must never park on this
300    /// handle — a consumer that is also the sole drainer of [`Self::next`] and
301    /// awaits `mutate` while events back up can deadlock against the actor
302    /// (each side blocked on the channel only the other drains). Such callers
303    /// keep their own retry queue and stay free to drain.
304    ///
305    /// `Full` returns the mutate for the caller to retry; `Closed` returns it
306    /// for a post-mortem (re-open and re-subscribe from durable cursors).
307    pub fn try_mutate(&self, mutate: B::Mutate) -> Result<(), TryMutateError<B::Mutate>> {
308        if self.finished.load(Ordering::Acquire) {
309            return Err(TryMutateError::Closed(mutate));
310        }
311        self.commands
312            .try_send(Command::Mutate(mutate))
313            .map_err(|e| {
314                let (closed, cmd) = match e {
315                    mpsc::error::TrySendError::Full(cmd) => (false, cmd),
316                    mpsc::error::TrySendError::Closed(cmd) => (true, cmd),
317                };
318                let Command::Mutate(mutate) = cmd else {
319                    unreachable!("try_mutate only submits Command::Mutate")
320                };
321                if closed {
322                    TryMutateError::Closed(mutate)
323                } else {
324                    TryMutateError::Full(mutate)
325                }
326            })
327    }
328
329    /// Half-close the request half — signal that we are done sending. The
330    /// outbound stream ends, so `mutate` and `probe` thereafter return `Closed`;
331    /// any live delivery already in flight keeps arriving until the server closes
332    /// its side. Half-close ends the session. It does not wait for catch-up
333    /// targets. Use the returned targets to decide when to cancel a bounded sync.
334    pub async fn finish(&self) -> Result<(), BidiError> {
335        // Latch closed only *after* the send is delivered. A cancelled `finish`
336        // (future dropped mid-send) never delivered `Finish`, so it must not
337        // wedge the handle — leaving the latch untouched is exactly right. And
338        // because the latch is monotonic (set on delivery, never cleared), two
339        // concurrent `finish` calls can't race a reset. The same-caller contract
340        // still holds: a FIFO-following `mutate`/`probe` runs after this store.
341        self.commands
342            .send(Command::Finish)
343            .await
344            .map_err(|_| BidiError::Closed)?;
345        self.finished.store(true, Ordering::Release);
346        Ok(())
347    }
348
349    /// Probe the link (e.g. right after the process resumes) with the default
350    /// deadline: `N ×` the server's advertised keepalive interval (a 30s-derived
351    /// fallback until `Started` arrives). Resolves `Ok` on the matching `Pong`,
352    /// `Closed` if the link is already torn down, or `ProbeTimedOut` if no pong
353    /// arrives in time — the half-open case this whole mechanism exists to catch.
354    pub async fn probe(&self) -> Result<(), BidiError> {
355        self.probe_within(self.default_probe_timeout()).await
356    }
357
358    /// Probe with an explicit deadline. A latency-sensitive caller — say a push
359    /// notification handler that must decide in a couple of seconds whether to
360    /// reuse the connection or re-open — passes a bound far below the default.
361    /// A tight bound trades occasional false `ProbeTimedOut`s for speed, which is
362    /// safe: re-opening replays from durable cursors and discards duplicates.
363    ///
364    /// The timeout covers the whole probe — submitting the ping (so a stalled
365    /// wire backpressuring the command queue can't park us) and awaiting the
366    /// pong. We deliberately don't fail-fast on a full command queue: a transient
367    /// burst of `mutate`s is "busy," not "dead."
368    pub async fn probe_within(&self, timeout: Duration) -> Result<(), BidiError> {
369        match xmtp_common::time::timeout(timeout, self.probe_inner()).await {
370            Ok(result) => result,
371            Err(_elapsed) => Err(BidiError::ProbeTimedOut),
372        }
373    }
374
375    async fn probe_inner(&self) -> Result<(), BidiError> {
376        if self.finished.load(Ordering::Acquire) {
377            return Err(BidiError::Closed);
378        }
379        let nonce = next_probe_nonce(&self.probe_nonce);
380        let (ack, ack_rx) = oneshot::channel();
381        self.commands
382            .send(Command::Probe { nonce, ack })
383            .await
384            .map_err(|_| BidiError::Closed)?;
385        ack_rx.await.map_err(|_| BidiError::Closed)
386    }
387
388    pub(crate) fn default_probe_timeout(&self) -> Duration {
389        let keepalive = match self.keepalive_ms {
390            0 => DEFAULT_KEEPALIVE_MS,
391            ms => ms,
392        };
393        Duration::from_millis(u64::from(keepalive) * u64::from(PROBE_TIMEOUT_MULTIPLIER))
394    }
395
396    /// Next event, in wire order. `None` means the connection ended (server
397    /// close, network death, watchdog teardown, or reap) — resume from durable
398    /// cursors on a fresh connection.
399    ///
400    /// Only resolves on an event or end-of-stream, but a half-open link cannot
401    /// leave it pending forever: the actor's silence watchdog tears the
402    /// connection down after [`PROBE_TIMEOUT_MULTIPLIER`] keepalive intervals
403    /// with no inbound frame (one watchdog ping in between), and that teardown
404    /// surfaces here as `None`. [`Self::probe`] remains for callers that need
405    /// an answer *faster* than the watchdog budget — e.g. a notification
406    /// handler deciding within seconds whether to reuse the connection.
407    pub async fn next(&mut self) -> Option<Event<B::GroupMessage, B::WelcomeMessage>> {
408        let event = self.events.recv().await;
409        // Record the server's cadence as `Started` passes through, so a probe
410        // issued in reaction to it already gets the keepalive-derived deadline.
411        if let Some(Event::Started {
412            keepalive_interval_ms,
413            ..
414        }) = &event
415        {
416            self.keepalive_ms = *keepalive_interval_ms;
417        }
418        event
419    }
420
421    pub fn failure(&self) -> Option<Arc<ConnectionFailure>> {
422        self.failure.lock().clone()
423    }
424}
425
426impl<B: BidiBinding> Drop for Connection<B> {
427    fn drop(&mut self) {
428        // Abort the actor so it cannot keep auto-ponging — a zombie keepalive
429        // would hold the server-side subscription open forever. The abort drops
430        // the actor's wire-outbound and inbound, cancelling the underlying
431        // request and tearing the stream down in both directions.
432        self.actor.end();
433    }
434}
435
436/// When the silence watchdog next fires: after `N - 1` keepalive intervals
437/// without an inbound frame it probes, and a probed connection gets the one
438/// remaining interval — the full `N ×` budget of [`PROBE_TIMEOUT_MULTIPLIER`] —
439/// before teardown. Recomputed every `select!` iteration, so any inbound frame
440/// pushes both deadlines out.
441fn watchdog_deadline(
442    last_inbound: tokio::time::Instant,
443    keepalive: Duration,
444    probed: bool,
445) -> tokio::time::Instant {
446    let intervals = if probed {
447        PROBE_TIMEOUT_MULTIPLIER
448    } else {
449        PROBE_TIMEOUT_MULTIPLIER - 1
450    };
451    last_inbound + keepalive * intervals
452}
453
454/// Hand `frame` to the wire if it has room right now, else queue it for the
455/// reserve branch to flush. Returns `false` when the actor should give up and tear
456/// down: either the wire is already closed (this frame can't be delivered), or the
457/// backlog has grown past [`MAX_PENDING_FRAMES`] — a wedged wire we won't buffer
458/// behind forever.
459///
460/// Reaching the queue means the transport isn't draining the request half —
461/// which on a healthy link should essentially never happen — so we warn the
462/// first time we fall back to it (not on every frame: an existing backlog skips
463/// straight to the queue without re-probing the wire).
464#[must_use]
465fn enqueue<R>(wire_out: &mpsc::Sender<R>, pending: &mut VecDeque<R>, frame: R) -> bool {
466    if pending.is_empty() {
467        match wire_out.try_send(frame) {
468            Ok(()) => return true,
469            Err(mpsc::error::TrySendError::Full(frame)) => {
470                tracing::warn!(
471                    "bidi wire is backpressured (transport not draining the request half); \
472                     queuing outbound frames"
473                );
474                pending.push_back(frame);
475            }
476            // Transport gone. Signal teardown now rather than queue an undeliverable
477            // frame and lean on the reserve branch to notice on a later `select!`
478            // iteration — which could accept a few more doomed frames first, since
479            // `select!` picks ready branches at random. The frame is dropped; a wire
480            // this dead will never send it, and the consumer re-syncs from durable
481            // cursors on re-open.
482            Err(mpsc::error::TrySendError::Closed(_frame)) => return false,
483        }
484    } else {
485        pending.push_back(frame);
486    }
487    if pending.len() > MAX_PENDING_FRAMES {
488        tracing::warn!(
489            backlog = pending.len(),
490            "bidi wire wedged past the outbound backlog cap; giving up so the consumer re-opens"
491        );
492        return false;
493    }
494    true
495}
496
497/// The single owner of the wire: sole reader of inbound, sole writer of
498/// outbound, sole producer of events. Caller commands and server frames are
499/// multiplexed onto one outbound FIFO via an internal `pending` queue, drained
500/// to the wire by a `reserve()` branch — so no send ever blocks the loop and a
501/// busy wire can't delay an auto-pong. (Event sends to the *consumer* do still
502/// await: a hopelessly slow consumer backpressures here, the intended path to
503/// reap-then-resume — distinct from outbound/wire pressure, which the queue
504/// handles.) It ends when the wire ends/errors or the handle goes away; ending
505/// drops `wire_out` (the request half closes, tearing the stream down), the
506/// `commands` receiver (so `mutate`/`probe` see `Closed`), the `events` sender
507/// (so `next` ends), and any outstanding probe acks (so pending `probe`s see
508/// `Closed`). One place, every teardown. It also gives up the same way if the
509/// outbound backlog blows past [`MAX_PENDING_FRAMES`] — a wedged wire that will
510/// never drain, so we tear down and let the consumer re-open — or if the wire
511/// goes silent past the watchdog budget (see [`watchdog_deadline`]): a
512/// conformant server keeps pinging, so sustained silence, with one watchdog
513/// ping of grace, means a half-open link that will never speak again.
514async fn run_actor<B, S, E>(
515    mut inbound: S,
516    wire_out: mpsc::Sender<B::Request>,
517    mut commands: mpsc::Receiver<Command<B>>,
518    events: mpsc::Sender<Event<B::GroupMessage, B::WelcomeMessage>>,
519    failure: FailureSlot,
520) where
521    B: BidiBinding,
522    S: futures::Stream<Item = Result<B::Response, E>> + Unpin,
523    E: RetryableError + 'static,
524{
525    // Outstanding client probes awaiting their `Pong`, keyed by nonce.
526    let mut probes: HashMap<u64, oneshot::Sender<()>> = HashMap::new();
527    // Outbound frames awaiting wire capacity. We drain these through a `reserve()`
528    // branch below rather than `send().await` in a branch body — an `.await` in a
529    // `select!` arm blocks the whole loop, so a backed-up wire would stall inbound
530    // reads and delay auto-pongs (getting us reaped on an otherwise healthy link).
531    let mut pending: VecDeque<B::Request> = VecDeque::new();
532    // Set by `Command::Finish`: leave the main loop and drain inbound to close,
533    // rather than tear everything down. Distinguishes a deliberate half-close
534    // from the wire/handle-gone breaks, which fall straight through to teardown.
535    let mut finished = false;
536    // Silence-watchdog state. A conformant server pings at `keepalive` cadence,
537    // so a healthy wire always shows *some* inbound frame well inside the
538    // budget; sustained silence is a half-open link. The cadence starts at the
539    // default and updates when `Started` advertises the real one.
540    let mut keepalive = Duration::from_millis(DEFAULT_KEEPALIVE_MS.into());
541    let mut last_inbound = tokio::time::Instant::now();
542    let mut watchdog_probed = false;
543
544    loop {
545        tokio::select! {
546            // Polled in order, so teardown is truly last-resort: the watchdog
547            // arm at the bottom is only reached when no flush, command, or
548            // inbound frame is ready — a pong landing in the same poll as the
549            // deadline always wins the tie and restamps the window. (A command
550            // flood can starve the deadline check, but a silent wire under a
551            // command flood hits `enqueue`'s give-up cap and tears down there.)
552            biased;
553            // Hand one queued frame to the wire the moment it has capacity,
554            // concurrently with everything else. This is what keeps a busy wire
555            // from blocking the loop. Gated on a non-empty queue so we only
556            // contend for a permit when there's something to flush.
557            permit = wire_out.reserve(), if !pending.is_empty() => {
558                match permit {
559                    Ok(permit) => {
560                        permit.send(pending.pop_front().expect("queue is non-empty"));
561                        // Drain as much of the backlog as the wire will take right
562                        // now, so a cleared stall flushes promptly.
563                        while !pending.is_empty() {
564                            match wire_out.try_reserve() {
565                                Ok(permit) => {
566                                    permit.send(pending.pop_front().expect("queue is non-empty"));
567                                }
568                                Err(_) => break, // wire full again, or closed
569                            }
570                        }
571                    }
572                    Err(_) => break, // transport gone
573                }
574            }
575            // A command from the handle: mutate, probe, or finish. Never gated:
576            // a backlog-room gate would starve a queued `Finish` forever on a
577            // wedged wire (the give-up cap is only checked when a frame is
578            // enqueued, and a closed gate means nothing is). Backlog growth from
579            // accepted mutates/probes is bounded by `enqueue`'s
580            // [`MAX_PENDING_FRAMES`] give-up instead — a wedged wire tears down
581            // rather than parking callers forever.
582            cmd = commands.recv() => {
583                let Some(cmd) = cmd else {
584                    // Every handle dropped — nothing more will be sent.
585                    break;
586                };
587                let queued = match cmd {
588                    Command::Mutate(mutate) => {
589                        enqueue(&wire_out, &mut pending, B::mutate_frame(mutate))
590                    }
591                    Command::Probe { nonce, ack } => {
592                        // Sweep entries whose probe already timed out client-side
593                        // (their receiver is gone), so a scheduled prober against
594                        // a pong-less peer can't grow the map without bound.
595                        probes.retain(|_, ack| !ack.is_closed());
596                        probes.insert(nonce, ack);
597                        enqueue(&wire_out, &mut pending, B::ping_frame(nonce))
598                    }
599                    Command::Finish => {
600                        // Half-close: stop accepting new commands, then flush the
601                        // backlog and drain inbound to close (in `drain_after_finish`).
602                        finished = true;
603                        break;
604                    }
605                };
606                if !queued {
607                    break; // wire wedged past the backlog cap — give up
608                }
609            }
610            // A frame from the server. Always read, so liveness never stalls behind
611            // outbound pressure.
612            frame = inbound.next() => {
613                let Some(frame) = frame else {
614                    break; // inbound ended — the stream is closed
615                };
616                let response = match frame {
617                    Ok(r) => r,
618                    Err(e) => {
619                        tracing::warn!("bidi subscription stream errored: {e}");
620                        *failure.lock() = Some(Arc::new(ConnectionFailure::Wire(xmtp_proto::api::NetworkError::new(e))));
621                        break;
622                    }
623                };
624                match B::handle(response) {
625                    Inbound::Invalid(reason) => {
626                        *failure.lock() = Some(Arc::new(ConnectionFailure::Protocol(reason)));
627                        break;
628                    }
629                    // Liveness is internal: auto-pong / probe-correlate here, never
630                    // surface it to the consumer.
631                    Inbound::Ping(nonce) => {
632                        // Auto-pong: hand it to the wire (or queue it FIFO behind a
633                        // backlog). A busy wire delays but never drops the pong, and
634                        // never blocks us from reading the next frame — but if the
635                        // backlog has blown past the cap, the wire is wedged and we
636                        // give up rather than keep piling pongs into a dead stream.
637                        if !enqueue(&wire_out, &mut pending, B::pong_frame(nonce)) {
638                            break;
639                        }
640                    }
641                    Inbound::Pong(nonce) => {
642                        // Resolve the matching client probe. Unmatched pongs are
643                        // ignored — never surfaced to the consumer.
644                        if let Some(ack) = probes.remove(&nonce) {
645                            let _ = ack.send(());
646                        }
647                    }
648                    // Consumer-facing frames. The same emit path feeds the live loop
649                    // and the post-`finish` drain, so their event semantics match.
650                    instruction => {
651                        // Adopt the server's advertised cadence for the watchdog as
652                        // `Started` passes through (0 keeps the fallback).
653                        if let Inbound::Emit(Event::Started {
654                            keepalive_interval_ms,
655                            ..
656                        }) = &instruction
657                            && *keepalive_interval_ms > 0
658                        {
659                            keepalive = Duration::from_millis((*keepalive_interval_ms).into());
660                        }
661                        if emit_instruction(&events, instruction).await {
662                            break;
663                        }
664                    }
665                }
666                // Stamp *after* the frame is fully handled, not on receipt: the
667                // emit above can park on consumer backpressure for a long time,
668                // and that stall is the consumer's, not the wire's — inbound
669                // frames may be sitting unread behind it. Restarting the window
670                // when we resume listening keeps a slow consumer from reading
671                // as wire silence and tearing down a healthy link.
672                last_inbound = tokio::time::Instant::now();
673                watchdog_probed = false;
674            }
675            // The silence watchdog. Fires only while the arms above are idle
676            // (enforced by `biased`) — exactly the "nothing inbound" condition
677            // it exists to detect. One
678            // unanswered ping stands between silence and teardown, so a quiet
679            // but live link (a server between keepalives) is never reaped: any
680            // inbound frame — the pong included — resets the window above.
681            _ = tokio::time::sleep_until(watchdog_deadline(last_inbound, keepalive, watchdog_probed)) => {
682                if watchdog_probed {
683                    tracing::warn!(
684                        silent_for_ms = last_inbound.elapsed().as_millis() as u64,
685                        "bidi wire silent past the watchdog budget (probe unanswered); \
686                         tearing down so the consumer re-opens from cursors"
687                    );
688                    break;
689                }
690                watchdog_probed = true;
691                if !enqueue(&wire_out, &mut pending, B::ping_frame(WATCHDOG_NONCE)) {
692                    break;
693                }
694            }
695        }
696    }
697    // Resolve any in-flight probes now: dropping the probe-ack senders makes each
698    // waiting `probe()` see `Closed`. Must happen *before* `drain_after_finish`,
699    // which can block on `inbound` for a while on a half-open link — a probe must
700    // never hang on a connection that has already left the live loop.
701    drop(probes);
702    // A deliberate half-close drains inbound to close rather than tearing down;
703    // every other exit reason falls straight through to teardown.
704    if finished {
705        drain_after_finish::<B, _, _>(inbound, wire_out, pending, commands, &events).await;
706    }
707    // One exit log for every reason (wire ended/errored, handle gone, or finished
708    // draining). The error a caller sees is just `Closed`; the "why" lives here.
709    tracing::debug!("bidi subscription actor stopped");
710    // `wire_out`, `commands`, `events`, and the `pending` queue drop here — that
711    // *is* the teardown in both directions.
712}
713
714/// Surface the consumer events for one classified inbound frame, returning true
715/// if the consumer is gone (the actor should stop). `Ping`/`Pong` must already
716/// have been handled by the caller — passing them here is a no-op. Shared by the
717/// live loop and the post-`finish` drain so both deliver identical events.
718async fn emit_instruction<G, W>(
719    events: &mpsc::Sender<Event<G, W>>,
720    instruction: Inbound<G, W>,
721) -> bool {
722    match instruction {
723        Inbound::Emit(event) => emit(events, event).await,
724        Inbound::Messages { group, welcome } => {
725            if !group.is_empty() && emit(events, Event::GroupMessages { messages: group }).await {
726                return true;
727            }
728            if !welcome.is_empty()
729                && emit(events, Event::WelcomeMessages { messages: welcome }).await
730            {
731                return true;
732            }
733            false
734        }
735        // Ping/Pong are the caller's job; Skip is a no-op.
736        Inbound::Ping(_) | Inbound::Pong(_) | Inbound::Skip => false,
737        Inbound::Invalid(_) => true,
738    }
739}
740
741/// Drain inbound to completion after a half-close. First flush any frames the
742/// caller already had accepted (a `mutate` it got `Ok` for, an auto-pong, a
743/// probe ping queued under backpressure) so half-close doesn't silently discard
744/// them. Then dropping `wire_out` ends the outbound stream (the server sees the
745/// half-close, finishes the update, and closes its side); dropping `commands`
746/// makes any late `mutate`/`probe` see `Closed`. We keep surfacing events until
747/// the server closes inbound (`next` -> `None`) or the consumer goes away.
748/// Liveness frames can't be answered (the wire is gone), so they are ignored;
749/// outstanding probes drop with the actor and resolve to `Closed`.
750async fn drain_after_finish<B, S, E>(
751    mut inbound: S,
752    wire_out: mpsc::Sender<B::Request>,
753    mut pending: VecDeque<B::Request>,
754    commands: mpsc::Receiver<Command<B>>,
755    events: &mpsc::Sender<Event<B::GroupMessage, B::WelcomeMessage>>,
756) where
757    B: BidiBinding,
758    S: futures::Stream<Item = Result<B::Response, E>> + Unpin,
759    E: std::fmt::Display,
760{
761    // Flush the accepted-but-unsent backlog before closing the request half,
762    // waiting for wire capacity under one shared budget. Bounded on purpose: an
763    // unbounded `reserve().await` on a stalled transport (wire full, server no
764    // longer reading the request stream) would block forever, so `drop(wire_out)`
765    // would never run and the half-close would never reach the server — but a
766    // purely non-blocking flush drops accepted frames on a *transient* stall a
767    // healthy transport clears in milliseconds. Frames still unplaced when the
768    // budget runs out are lost; the consumer re-syncs from durable cursors on
769    // re-open, and a wedged actor is the strictly worse outcome.
770    let flush_deadline = tokio::time::Instant::now() + DRAIN_FLUSH_BUDGET;
771    while let Some(frame) = pending.pop_front() {
772        match tokio::time::timeout_at(flush_deadline, wire_out.reserve()).await {
773            Ok(Ok(permit)) => permit.send(frame),
774            Ok(Err(_)) => break, // wire gone — close rather than block
775            Err(_elapsed) => {
776                tracing::warn!(
777                    dropped = pending.len() + 1,
778                    "bidi half-close flush budget exhausted on a stalled wire; \
779                     dropping the remaining backlog (re-open re-syncs from cursors)"
780                );
781                break;
782            }
783        }
784    }
785    drop(wire_out);
786    drop(commands);
787    while let Some(frame) = inbound.next().await {
788        let response = match frame {
789            Ok(r) => r,
790            Err(e) => {
791                tracing::warn!("bidi stream errored during finish drain: {e}");
792                break;
793            }
794        };
795        if emit_instruction(events, B::handle(response)).await {
796            break; // consumer gone
797        }
798    }
799}
800
801/// Send one event to the consumer, awaiting a free slot (the backpressure that
802/// stalls wire reads once [`EVENT_BUFFER`] fills). Returns true when the
803/// consumer is gone and the actor should shut down. (The server's keepalive
804/// cadence is recorded handle-side, when [`Connection::next`] surfaces
805/// `Started` — every event reaches the consumer through there.)
806async fn emit<G, W>(events: &mpsc::Sender<Event<G, W>>, event: Event<G, W>) -> bool {
807    events.send(event).await.is_err()
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    /// A trivial binding: wire frames are bare `u64`s and nothing is ever
815    /// surfaced. Enough to exercise the backend-agnostic control logic directly.
816    struct TestBinding;
817
818    impl BidiBinding for TestBinding {
819        type Request = u64;
820        type Response = u64;
821        type Mutate = u64;
822        type GroupMessage = ();
823        type WelcomeMessage = ();
824
825        fn mutate_frame(mutate: u64) -> u64 {
826            mutate
827        }
828        fn ping_frame(nonce: u64) -> u64 {
829            nonce
830        }
831        fn pong_frame(nonce: u64) -> u64 {
832            nonce
833        }
834        fn handle(_response: u64) -> Inbound<(), ()> {
835            Inbound::Skip
836        }
837    }
838
839    /// Probe nonces skip the reserved values across the wrap: the counter
840    /// steps over [`WATCHDOG_NONCE`] and `0`, then resumes counting from 1.
841    #[xmtp_common::test]
842    fn probe_nonces_never_mint_the_watchdog_nonce() {
843        let counter = AtomicU64::new(u64::MAX - 2);
844        let minted: Vec<u64> = (0..4).map(|_| next_probe_nonce(&counter)).collect();
845        assert_eq!(minted, vec![u64::MAX - 1, 1, 2, 3]);
846    }
847
848    /// `finish` is a half-close, not an abort: frames the caller already had
849    /// accepted under wire backpressure (parked in the actor's `pending` queue,
850    /// not yet on the wire) must be flushed before the request half closes. Drive
851    /// `drain_after_finish` directly with a pre-seeded `pending` and an
852    /// already-closed inbound, so the flush is the only behaviour under test.
853    #[xmtp_common::test(unwrap_try = true)]
854    async fn drain_after_finish_flushes_pending_before_closing() {
855        let (wire_out, mut wire_rx) = mpsc::channel::<u64>(WIRE_BUFFER);
856        let (_commands_tx, commands_rx) = mpsc::channel::<Command<TestBinding>>(COMMAND_BUFFER);
857        let (events, _events_rx) = mpsc::channel::<Event<(), ()>>(EVENT_BUFFER);
858
859        // A distinctively-tagged frame queued as if wire backpressure had parked
860        // it before the caller half-closed.
861        let mut pending = VecDeque::new();
862        pending.push_back(7_777_u64);
863
864        // Inbound is already closed, so the drain has nothing to read and returns
865        // as soon as the pending flush completes.
866        let inbound = futures::stream::empty::<Result<u64, BidiError>>();
867        drain_after_finish::<TestBinding, _, _>(inbound, wire_out, pending, commands_rx, &events)
868            .await;
869
870        // The queued frame reached the wire instead of being dropped on close,
871        // and the wire then closes (the actor dropped its sender).
872        assert_eq!(
873            wire_rx.recv().await,
874            Some(7_777),
875            "the pending frame must be flushed to the wire"
876        );
877        assert_eq!(
878            wire_rx.recv().await,
879            None,
880            "the wire closes once the flush is done"
881        );
882    }
883
884    /// A *wedged* wire must not hold the half-close hostage: the flush waits out
885    /// [`DRAIN_FLUSH_BUDGET`] for capacity, then drops the backlog and still
886    /// closes the request half so the server sees the half-close.
887    #[xmtp_common::test(unwrap_try = true)]
888    async fn drain_after_finish_bounds_the_flush_on_a_wedged_wire() {
889        // Capacity 1, pre-filled, receiver never polled: reserve() can never
890        // succeed, so only the budget can end the flush.
891        let (wire_out, mut wire_rx) = mpsc::channel::<u64>(1);
892        let (_commands_tx, commands_rx) = mpsc::channel::<Command<TestBinding>>(COMMAND_BUFFER);
893        let (events, _events_rx) = mpsc::channel::<Event<(), ()>>(EVENT_BUFFER);
894        wire_out.try_send(1_u64).unwrap();
895
896        let mut pending = VecDeque::new();
897        pending.push_back(7_777_u64);
898
899        let inbound = futures::stream::empty::<Result<u64, BidiError>>();
900        drain_after_finish::<TestBinding, _, _>(inbound, wire_out, pending, commands_rx, &events)
901            .await;
902
903        // The pre-wedged frame is still there; the un-flushable backlog was
904        // dropped; and crucially the request half closed anyway.
905        assert_eq!(wire_rx.recv().await, Some(1));
906        assert_eq!(
907            wire_rx.recv().await,
908            None,
909            "the request half must close even when the flush budget expires"
910        );
911    }
912
913    /// Binding for the watchdog tests. A frame under `100_000` is a `Started`
914    /// advertising that many milliseconds of keepalive — so each test picks a
915    /// cadence fast enough to run in real time. [`MSG_FRAME`] delivers one
916    /// group message (to park the actor on consumer backpressure); anything
917    /// else is a no-op frame that still counts as inbound activity.
918    struct WatchdogBinding;
919    const MSG_FRAME: u64 = 1_000_000;
920    const ACTIVITY_FRAME: u64 = 2_000_000;
921    /// Generous ceiling for CI scheduling noise; every wait in these tests
922    /// resolves in well under a second on a healthy run.
923    const WAIT: Duration = Duration::from_secs(5);
924
925    impl BidiBinding for WatchdogBinding {
926        type Request = u64;
927        type Response = u64;
928        type Mutate = u64;
929        type GroupMessage = ();
930        type WelcomeMessage = ();
931
932        fn mutate_frame(mutate: u64) -> u64 {
933            mutate
934        }
935        fn ping_frame(nonce: u64) -> u64 {
936            nonce
937        }
938        fn pong_frame(nonce: u64) -> u64 {
939            nonce
940        }
941        fn handle(response: u64) -> Inbound<(), ()> {
942            match response {
943                ms if ms < 100_000 => Inbound::Emit(Event::Started {
944                    keepalive_interval_ms: ms as u32,
945                }),
946                MSG_FRAME => Inbound::Messages {
947                    group: vec![()],
948                    welcome: vec![],
949                },
950                _ => Inbound::Skip,
951            }
952        }
953    }
954
955    struct ActorHarness {
956        inbound: mpsc::Sender<Result<u64, BidiError>>,
957        wire: mpsc::Receiver<u64>,
958        events: mpsc::Receiver<Event<(), ()>>,
959        _commands: mpsc::Sender<Command<WatchdogBinding>>,
960    }
961
962    /// Spawn `run_actor` over harness-controlled channels, mirroring
963    /// [`Connection::start`]'s wiring (`poll_fn` stream + `Box::pin`).
964    fn spawn_actor() -> ActorHarness {
965        let (inbound_tx, mut inbound_rx) = mpsc::channel::<Result<u64, BidiError>>(2048);
966        let (wire_out, wire_rx) = mpsc::channel::<u64>(WIRE_BUFFER);
967        let (commands_tx, commands_rx) = mpsc::channel::<Command<WatchdogBinding>>(COMMAND_BUFFER);
968        let (events_tx, events_rx) = mpsc::channel::<Event<(), ()>>(EVENT_BUFFER);
969        let inbound = futures::stream::poll_fn(move |cx| inbound_rx.poll_recv(cx));
970        xmtp_common::spawn(
971            None,
972            run_actor::<WatchdogBinding, _, _>(
973                Box::pin(inbound),
974                wire_out,
975                commands_rx,
976                events_tx,
977                FailureSlot::default(),
978            ),
979        );
980        ActorHarness {
981            inbound: inbound_tx,
982            wire: wire_rx,
983            events: events_rx,
984            _commands: commands_tx,
985        }
986    }
987
988    /// Total silence earns exactly one watchdog ping, then teardown: the
989    /// half-open link surfaces as end-of-events instead of hanging forever.
990    #[xmtp_common::test(unwrap_try = true)]
991    async fn watchdog_probes_then_tears_down_a_silent_wire() {
992        let mut actor = spawn_actor();
993        actor.inbound.send(Ok(150)).await?; // Started: 150ms keepalive
994        assert!(matches!(
995            xmtp_common::time::timeout(WAIT, actor.events.recv()).await?,
996            Some(Event::Started { .. })
997        ));
998
999        let ping = xmtp_common::time::timeout(WAIT, actor.wire.recv()).await?;
1000        assert_eq!(
1001            ping,
1002            Some(WATCHDOG_NONCE),
1003            "the watchdog must probe before giving up"
1004        );
1005        assert_eq!(
1006            xmtp_common::time::timeout(WAIT, actor.events.recv()).await?,
1007            None,
1008            "an unanswered watchdog ping must tear the connection down"
1009        );
1010    }
1011
1012    /// A quiet-but-live link is never reaped: any inbound frame — a pong or
1013    /// otherwise — answers the watchdog ping and resets the window.
1014    #[xmtp_common::test(unwrap_try = true)]
1015    async fn an_answered_watchdog_probe_keeps_the_wire_alive() {
1016        let mut actor = spawn_actor();
1017        actor.inbound.send(Ok(150)).await?;
1018        xmtp_common::time::timeout(WAIT, actor.events.recv()).await?;
1019
1020        // Ride out three whole silence windows, answering each probe.
1021        for _ in 0..3 {
1022            let ping = xmtp_common::time::timeout(WAIT, actor.wire.recv()).await?;
1023            assert_eq!(ping, Some(WATCHDOG_NONCE));
1024            actor.inbound.send(Ok(ACTIVITY_FRAME)).await?;
1025        }
1026        assert!(
1027            matches!(
1028                actor.events.try_recv(),
1029                Err(mpsc::error::TryRecvError::Empty)
1030            ),
1031            "an answered probe must keep the connection alive"
1032        );
1033
1034        // Stop answering: the next unanswered ping tears it down.
1035        assert_eq!(
1036            xmtp_common::time::timeout(WAIT, actor.events.recv()).await?,
1037            None
1038        );
1039    }
1040
1041    /// A steady trickle of frames well inside the probe threshold never
1042    /// triggers a probe at all.
1043    #[xmtp_common::test(unwrap_try = true)]
1044    async fn inbound_activity_resets_the_watchdog() {
1045        let mut actor = spawn_actor();
1046        actor.inbound.send(Ok(200)).await?; // probe would fire at 400ms silent
1047        xmtp_common::time::timeout(WAIT, actor.events.recv()).await?;
1048
1049        for _ in 0..20 {
1050            actor.inbound.send(Ok(ACTIVITY_FRAME)).await?;
1051            xmtp_common::time::sleep(Duration::from_millis(50)).await;
1052        }
1053        assert!(
1054            matches!(actor.wire.try_recv(), Err(mpsc::error::TryRecvError::Empty)),
1055            "no watchdog ping may fire while frames keep arriving"
1056        );
1057        assert!(matches!(
1058            actor.events.try_recv(),
1059            Err(mpsc::error::TryRecvError::Empty)
1060        ));
1061    }
1062
1063    /// Consumer backpressure must not read as wire silence: while the actor is
1064    /// parked emitting into a full event channel, inbound frames sit unread
1065    /// behind it — the stall is the consumer's, not the wire's. The silence
1066    /// window restarts when the actor resumes listening, so a slow consumer
1067    /// never gets a healthy link torn down.
1068    #[xmtp_common::test(unwrap_try = true)]
1069    async fn consumer_backpressure_is_not_wire_silence() {
1070        let mut actor = spawn_actor();
1071        actor.inbound.send(Ok(200)).await?; // probe at 400ms, teardown at 600ms
1072        xmtp_common::time::timeout(WAIT, actor.events.recv()).await?;
1073
1074        // Fill the event buffer plus one: the actor parks mid-emit.
1075        for _ in 0..(EVENT_BUFFER + 1) {
1076            actor.inbound.send(Ok(MSG_FRAME)).await?;
1077        }
1078        // Hold the park well past the full teardown budget, draining nothing.
1079        xmtp_common::time::sleep(Duration::from_millis(800)).await;
1080
1081        // Drain; the actor unparks with a fresh silence window. If the park
1082        // had counted as silence, both watchdog deadlines are long past and
1083        // teardown would be immediate.
1084        let mut delivered = 0;
1085        while delivered < EVENT_BUFFER + 1 {
1086            match xmtp_common::time::timeout(WAIT, actor.events.recv()).await? {
1087                Some(Event::GroupMessages { .. }) => delivered += 1,
1088                other => panic!("unexpected event while draining: {other:?}"),
1089            }
1090        }
1091        xmtp_common::time::sleep(Duration::from_millis(150)).await;
1092        assert!(
1093            matches!(
1094                actor.events.try_recv(),
1095                Err(mpsc::error::TryRecvError::Empty)
1096            ),
1097            "a consumer stall must not be read as wire silence"
1098        );
1099    }
1100}