Skip to main content

velo_ext/
transport.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Active-message transport extension trait.
5//!
6//! This is the primary contract that out-of-tree transport authors implement.
7//! Concrete impls (TCP, HTTP, NATS, gRPC, ZMQ) ship in the `velo` runtime
8//! crate; external implementors `impl Transport for MyTransport` against this
9//! crate without depending on the runtime.
10
11use bytes::Bytes;
12use futures::future::BoxFuture;
13
14use crate::admission::SendOutcome;
15use crate::id::{InstanceId, PeerInfo, TransportKey, WorkerAddress};
16use crate::observability::TransportObservability;
17
18use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
19use std::{sync::Arc, time::Duration};
20use tokio::sync::Notify;
21use tokio_util::sync::CancellationToken;
22
23/// Errors returned by individual [`Transport`] implementations.
24#[derive(thiserror::Error, Debug)]
25pub enum TransportError {
26    /// The peer's [`WorkerAddress`] does not contain an entry for this transport.
27    #[error("No endpoint found for transport")]
28    NoEndpoint,
29
30    /// The endpoint string could not be parsed (malformed URL, invalid address).
31    #[error("Invalid endpoint format")]
32    InvalidEndpoint,
33
34    /// The target peer was never registered with this transport.
35    #[error("Peer not registered: {0}")]
36    PeerNotRegistered(InstanceId),
37
38    /// The transport has not been started yet (no runtime handle).
39    #[error("Transport not started")]
40    NotStarted,
41
42    /// No responders available for the peer (e.g. NATS request with no subscriber).
43    #[error("No responders for peer")]
44    NoResponders,
45}
46
47/// Error type specific to health check operations
48#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
49pub enum HealthCheckError {
50    /// The peer was never registered with this transport.
51    #[error("Peer not registered with transport")]
52    PeerNotRegistered,
53
54    /// The transport has not been started yet.
55    #[error("Transport not started")]
56    TransportNotStarted,
57
58    /// The peer is registered but no connection has ever been established.
59    #[error("Connection never established to peer")]
60    NeverConnected,
61
62    /// An existing connection is unhealthy or the peer is unreachable.
63    #[error("Connection failed or peer unreachable")]
64    ConnectionFailed,
65
66    /// The health check exceeded the specified timeout.
67    #[error("Health check timed out")]
68    Timeout,
69}
70
71/// Shared shutdown coordinator for graceful multi-phase shutdown.
72///
73/// **Phases**:
74/// 1. **Gate** — `begin_drain()` flips the draining flag; transports reject new inbound requests.
75/// 2. **Drain** — `wait_for_drain()` blocks until all in-flight guards are dropped.
76/// 3. **Teardown** — `teardown_token().cancel()` kills listeners and writer tasks.
77///
78/// Hot-path cost: one `SeqCst` `fetch_add` plus one `SeqCst` load per inbound
79/// [`MessageType::Message`], both inside [`TransportAdapter::admit_message`].
80/// Every other frame type is ungated and touches none of these atomics.
81/// `is_draining()` is a `Relaxed` load, but it is a reporting hook, not
82/// something the frame path calls — see below.
83///
84/// # Admission is not `is_draining()`
85///
86/// Deciding whether to accept an inbound [`MessageType::Message`] must go
87/// through [`TransportAdapter::admit_message`], never through a bare
88/// `is_draining()` check. `is_draining()` is a best-effort observer: a
89/// check-then-enqueue sequence built on it is a plain interleaving race —
90/// [`begin_drain`](Self::begin_drain), [`wait_for_drain`](Self::wait_for_drain)
91/// and teardown can all complete inside the producer's check-to-enqueue gap,
92/// and no memory ordering can close that. `admit_message` acquires the
93/// in-flight guard *first* and then re-reads the flag with `SeqCst`, which
94/// turns the pair into a store-buffer litmus that at least one side must win.
95#[derive(Clone)]
96pub struct ShutdownState {
97    inner: Arc<ShutdownStateInner>,
98}
99
100struct ShutdownStateInner {
101    draining: AtomicBool,
102    in_flight: AtomicUsize,
103    drain_complete: Notify,
104    teardown_token: CancellationToken,
105}
106
107impl ShutdownState {
108    /// Create a new shutdown state. Not draining, zero in-flight.
109    pub fn new() -> Self {
110        Self {
111            inner: Arc::new(ShutdownStateInner {
112                draining: AtomicBool::new(false),
113                in_flight: AtomicUsize::new(0),
114                drain_complete: Notify::new(),
115                teardown_token: CancellationToken::new(),
116            }),
117        }
118    }
119
120    /// Returns `true` if drain has been initiated (Phase 1).
121    ///
122    /// **Best-effort observer, not an admission decision.** Uses `Relaxed`
123    /// ordering: cheap enough for a per-frame hot-path peek, and sound for
124    /// *reporting* because the flag is monotonic (false → true, never reset),
125    /// so a `true` is always authoritative. A `false` is not — it may be
126    /// stale, and acting on it to enqueue inbound work reopens the
127    /// check-then-act race described on [`ShutdownState`]. Admit inbound
128    /// [`MessageType::Message`] frames with
129    /// [`TransportAdapter::admit_message`] instead.
130    #[inline]
131    pub fn is_draining(&self) -> bool {
132        self.inner.draining.load(Ordering::Relaxed)
133    }
134
135    /// Admission-strength read of the draining flag.
136    ///
137    /// `SeqCst` so that it participates in the single total order that also
138    /// contains [`begin_drain`](Self::begin_drain)'s store, the `fetch_add` in
139    /// [`acquire`](Self::acquire), and [`wait_for_drain`](Self::wait_for_drain)'s
140    /// load. Private on purpose: the only correct use is *after* acquiring a
141    /// guard, which is what [`TransportAdapter::admit_message`] does.
142    #[inline]
143    fn is_draining_for_admission(&self) -> bool {
144        self.inner.draining.load(Ordering::SeqCst)
145    }
146
147    /// Begin Phase 1: flip the draining flag. Idempotent.
148    ///
149    /// `SeqCst`: this store and [`wait_for_drain`](Self::wait_for_drain)'s
150    /// load are one half of the admission litmus (the other half being
151    /// [`acquire`](Self::acquire)'s `fetch_add` and the flag re-read in
152    /// [`TransportAdapter::admit_message`]). Under acquire/release neither
153    /// side's load would be ordered against the other side's store and both
154    /// could read stale — shutdown seeing zero in-flight while a producer
155    /// sees "not draining" — which is exactly the message that would slip
156    /// past the gate *and* past the drain wait.
157    pub fn begin_drain(&self) {
158        self.inner.draining.store(true, Ordering::SeqCst);
159    }
160
161    /// Acquire an in-flight guard. The guard increments the counter on creation
162    /// and decrements it on drop. Use this to track requests that are being processed.
163    ///
164    /// Guards are still acquirable after `begin_drain()` — this is intentional
165    /// so that already-accepted work can be tracked.
166    ///
167    /// `SeqCst` on the increment: see [`begin_drain`](Self::begin_drain).
168    pub fn acquire(&self) -> InFlightGuard {
169        self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
170        InFlightGuard {
171            inner: self.inner.clone(),
172        }
173    }
174
175    /// Current number of in-flight requests. Primarily for testing/debugging.
176    pub fn in_flight_count(&self) -> usize {
177        self.inner.in_flight.load(Ordering::Acquire)
178    }
179
180    /// Wait until in-flight count reaches zero. Returns immediately if already zero.
181    ///
182    /// Registers interest *before* re-checking the counter: `notify_waiters()`
183    /// stores no permit, so a guard dropping between the check and the
184    /// registration would strand this waiter forever under
185    /// [`ShutdownPolicy::WaitForever`]. Creating the `Notified` future is
186    /// enough — tokio wakes futures that exist at `notify_waiters()` time even
187    /// if they have not been polled yet.
188    ///
189    /// The load is `SeqCst` so it is ordered against every producer's
190    /// `fetch_add` in [`acquire`](Self::acquire) — see
191    /// [`begin_drain`](Self::begin_drain). Note that a guard acquired and
192    /// released purely to *reject* a message (the `Draining` arm of
193    /// [`TransportAdapter::admit_message`]) makes the count transiently
194    /// non-zero; the register-before-check loop above turns that into one
195    /// extra iteration, not a missed wakeup.
196    pub async fn wait_for_drain(&self) {
197        loop {
198            let notified = self.inner.drain_complete.notified();
199            if self.inner.in_flight.load(Ordering::SeqCst) == 0 {
200                return;
201            }
202            notified.await;
203        }
204    }
205
206    /// Get the Phase 3 teardown token. Cancel this to kill listeners/writers.
207    pub fn teardown_token(&self) -> &CancellationToken {
208        &self.inner.teardown_token
209    }
210}
211
212impl Default for ShutdownState {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218/// RAII guard that decrements the in-flight counter on drop.
219pub struct InFlightGuard {
220    inner: Arc<ShutdownStateInner>,
221}
222
223impl InFlightGuard {
224    /// Explicitly complete this guard (equivalent to dropping it).
225    pub fn complete(self) {
226        // Drop impl handles the decrement
227    }
228}
229
230impl Drop for InFlightGuard {
231    fn drop(&mut self) {
232        let prev = self.inner.in_flight.fetch_sub(1, Ordering::AcqRel);
233        // If we just decremented to 0, notify waiters
234        if prev == 1 {
235            self.inner.drain_complete.notify_waiters();
236        }
237    }
238}
239
240/// Policy for how long to wait during the drain phase.
241#[derive(Debug, Clone)]
242pub enum ShutdownPolicy {
243    /// Wait indefinitely for all in-flight requests to complete.
244    WaitForever,
245    /// Wait up to the given duration, then force teardown.
246    Timeout(Duration),
247}
248
249/// Abstraction over a single message transport (TCP, HTTP, NATS, gRPC, …).
250///
251/// Implementations handle peer registration, message sending, listener
252/// lifecycle, health checking, and graceful shutdown. The trait is object-safe
253/// so transports can be stored as `Arc<dyn Transport>`.
254///
255/// Out-of-tree implementors should `impl Transport for MyTransport`. The
256/// default [`set_observability`](Transport::set_observability) hook is a no-op
257/// — implementors only need to override it if they want to integrate with
258/// the runtime's metrics handle (which they recover via
259/// [`ObservabilityHook::downcast`]).
260pub trait Transport: Send + Sync {
261    /// Unique key identifying this transport (e.g. `"tcp"`, `"grpc"`).
262    fn key(&self) -> TransportKey;
263    /// The [`WorkerAddress`] fragment advertised by this transport.
264    fn address(&self) -> WorkerAddress;
265    /// Register a remote peer, extracting its endpoint from [`PeerInfo`].
266    fn register(&self, peer_info: PeerInfo) -> Result<(), TransportError>;
267
268    /// Send an active message to the remote instance.
269    ///
270    /// The frame is taken unconditionally: implementations must not hand it
271    /// back, and the caller has no way to retract it. What the return value
272    /// reports is *when* the frame reached the per-target send channel.
273    ///
274    /// - [`SendOutcome::Admitted`] — it is on the channel already. This is also
275    ///   what a hard pre-wire failure returns, once `on_error` has been called
276    ///   for it (peer unregistered, transport not started, oversized frame):
277    ///   there is nothing left for the caller to wait on either way.
278    /// - [`SendOutcome::Pending`] — the channel was saturated, so the frame is
279    ///   queued in the target's [`AdmissionGate`](crate::admission::AdmissionGate)
280    ///   behind its predecessors. The returned
281    ///   [`SendAdmission`](crate::admission::SendAdmission) resolves `Ok(())` when the frame is
282    ///   enqueued and `Err` when it never will be (the connection epoch died,
283    ///   the channel closed). Delivery does **not** depend on the caller
284    ///   polling it — dropping it is a legitimate fire-and-forget pattern.
285    ///
286    /// Implementations must route every send through one gate per target and
287    /// keep no `try_send` path around it: an admission that can be overtaken by
288    /// a later fast-path send is the reordering hazard the gate exists to
289    /// remove (see the [`admission`](crate::admission) module docs).
290    ///
291    /// Failures *after* admission — the write itself — continue to flow
292    /// through `on_error`.
293    fn send_message(
294        &self,
295        instance_id: InstanceId,
296        header: Bytes,
297        payload: Bytes,
298        message_type: MessageType,
299        on_error: Arc<dyn TransportErrorHandler>,
300    ) -> SendOutcome;
301
302    /// Largest single message this transport will carry to `target`, in bytes.
303    ///
304    /// The number bounds `header.len() + payload.len()` for one
305    /// [`send_message`](Transport::send_message) — the *combined* frame
306    /// content, not the payload alone. A caller that prepends its own envelope
307    /// to the payload subtracts that envelope from this number; there is no
308    /// second allowance hiding behind it.
309    ///
310    /// `None` means this transport does not know its capacity: nothing was
311    /// negotiated, no limit is configured, or it has not started yet. It is
312    /// **not** a claim of unlimited capacity — a caller that reads `None` must
313    /// fall back to a conservative budget of its own choosing.
314    ///
315    /// The answer is per-target because it can be genuinely per-connection: a
316    /// NATS client learns `max_payload` from the server it happens to be
317    /// connected to, so two peers reached through two clients can differ.
318    /// Transports with a single static limit ignore the argument.
319    ///
320    /// Nothing about this method changes what `send_message` does with an
321    /// oversized frame — that still fails pre-wire through the send's
322    /// `on_error` handler. It exists so callers can size sends to avoid
323    /// meeting that failure at all.
324    fn max_message_size(&self, _target: InstanceId) -> Option<usize> {
325        None
326    }
327
328    /// Start the transport (bind listener, spawn tasks) for the given instance.
329    fn start(
330        &self,
331        instance_id: InstanceId,
332        channels: TransportAdapter,
333        rt: tokio::runtime::Handle,
334    ) -> BoxFuture<'_, anyhow::Result<()>>;
335
336    /// Tear down the transport, cancelling all tasks and closing connections.
337    ///
338    /// This is phase 3 of the runtime's graceful shutdown and the runtime calls
339    /// it only after [`ShutdownState::begin_drain`] and the drain wait. Several
340    /// in-tree transports (TCP, UDS, gRPC, UCX) also cancel the *shared*
341    /// [`ShutdownState::teardown_token`] here, which is instance-wide: it stops
342    /// every transport's listeners **and** the runtime's inbound message
343    /// consumer, which then abandons whatever is still queued. Calling
344    /// `shutdown()` directly on one such transport of a live instance therefore
345    /// kills inbound dispatch for all of them, with no drain and no
346    /// [`MessageType::ShuttingDown`] correlation for the senders. Reach for
347    /// the runtime's graceful shutdown instead unless that is precisely what
348    /// you want.
349    fn shutdown(&self);
350
351    /// Install a transport-scoped observability handle.
352    ///
353    /// The runtime calls this once per transport during startup with a handle
354    /// pre-bound to this transport's `key`. Implementations typically store it
355    /// in an [`OnceLock`](std::sync::OnceLock) and call its methods on the
356    /// hot path; transports that do not emit metrics can leave the default
357    /// no-op.
358    fn set_observability(&self, _observability: std::sync::Arc<dyn TransportObservability>) {}
359
360    /// Notification hook for Phase 1 (Gate) of graceful shutdown.
361    ///
362    /// The runtime calls this *after* flipping the shared [`ShutdownState`]'s
363    /// drain flag — the flag the runtime handed this transport inside the
364    /// [`TransportAdapter`] at [`start`](Transport::start). Flipping that flag
365    /// is the runtime's job, not this method's: per-frame gating is
366    /// implemented by routing every inbound [`MessageType::Message`] through
367    /// [`TransportAdapter::admit_message`], which returns
368    /// [`AdmitOutcome::Draining`] with the frame handed back so this transport
369    /// can answer it with a [`MessageType::ShuttingDown`] correlation reply.
370    /// Do not gate on [`ShutdownState::is_draining`] directly — that is a
371    /// best-effort observer and a check-then-enqueue built on it can let a
372    /// message slip past both the gate and the drain wait.
373    ///
374    /// Override only for drain work the shared flag cannot express — e.g.,
375    /// unsubscribing from broker subjects so new requests stop arriving at
376    /// all, or pausing an accept loop. The default no-op is correct for
377    /// transports whose listeners gate per-frame off the shared flag.
378    ///
379    /// Must be idempotent. Do not flip the shared `ShutdownState` here: it is
380    /// instance-wide, shared by every transport of the instance, so flipping
381    /// it from one transport would silently drain them all.
382    fn begin_drain(&self) {}
383
384    /// Check if a registered peer is reachable and healthy.
385    ///
386    /// Returns `Ok(())` if the peer responds within the timeout. Different
387    /// transports implement this differently:
388    /// - NATS: request/reply to health subject
389    /// - TCP: check existing connection or attempt new connection
390    /// - HTTP: HEAD request to health endpoint
391    fn check_health(
392        &self,
393        instance_id: InstanceId,
394        timeout: Duration,
395    ) -> std::pin::Pin<
396        Box<dyn std::future::Future<Output = Result<(), HealthCheckError>> + Send + '_>,
397    >;
398}
399
400/// Callback trait invoked when a transport fails to deliver a message.
401///
402/// The original `header` and `payload` are returned so higher layers can
403/// retry or log the failure.
404pub trait TransportErrorHandler: Send + Sync {
405    /// Called when message delivery fails. Receives the original data and error description.
406    fn on_error(&self, header: Bytes, payload: Bytes, error: String);
407}
408
409/// Message type discriminator for routing frames to appropriate streams
410#[repr(u8)]
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub enum MessageType {
413    #[allow(missing_docs)]
414    Message = 0,
415    #[allow(missing_docs)]
416    Response = 1,
417    #[allow(missing_docs)]
418    Ack = 2,
419    #[allow(missing_docs)]
420    Event = 3,
421    /// Sent back to a peer when we are draining and cannot accept new messages.
422    /// The original request header is echoed back for correlation.
423    ShuttingDown = 4,
424}
425
426impl MessageType {
427    /// Try to convert a u8 to a MessageType
428    pub fn from_u8(value: u8) -> Option<Self> {
429        match value {
430            0 => Some(MessageType::Message),
431            1 => Some(MessageType::Response),
432            2 => Some(MessageType::Ack),
433            3 => Some(MessageType::Event),
434            4 => Some(MessageType::ShuttingDown),
435            _ => None,
436        }
437    }
438
439    /// Convert MessageType to u8
440    pub fn as_u8(self) -> u8 {
441        self as u8
442    }
443}
444
445/// An inbound [`MessageType::Message`] frame together with the in-flight
446/// guard that makes it visible to [`ShutdownState::wait_for_drain`].
447///
448/// The guard is **mandatory**, which is the whole point of the type: a message
449/// cannot sit on the inbound queue without being counted work. Producers never
450/// build one directly — [`TransportAdapter::admit_message`] acquires the guard
451/// and constructs it, and the message channel's sender is private so there is
452/// no other way in. Whatever happens to the message afterwards — dispatched,
453/// dropped on a decode error, handed back undelivered when the receiver has
454/// gone, discarded by a consumer that is abandoning its backlog at teardown —
455/// the guard rides along and its `Drop` releases the count. "Queued implies
456/// counted" is therefore an invariant of the type system, not a convention
457/// producers have to honour.
458///
459/// The one thing that does *not* release a guard is walking away from the
460/// channel: flume frees buffered items only once the last endpoint is gone, so
461/// a consumer that stops receiving while transports still hold sender clones
462/// must drain and drop what it is abandoning, not merely drop its receiver.
463///
464/// `#[non_exhaustive]`: construct with [`InboundMessage::new`], match with a
465/// trailing `..`.
466#[non_exhaustive]
467pub struct InboundMessage {
468    /// The frame's header bytes.
469    pub header: Bytes,
470    /// The frame's payload bytes.
471    pub payload: Bytes,
472    /// Keeps the instance's in-flight count non-zero for as long as this
473    /// message exists, queued or in a handler.
474    pub guard: InFlightGuard,
475}
476
477impl InboundMessage {
478    /// Bind a frame to an already-acquired in-flight guard.
479    ///
480    /// Public for consumer-side fabrication — a test that wants a realistic
481    /// item to feed a receiver, or a harness that stands in for the runtime.
482    /// It is not a way onto the inbound queue: the channel's sender is
483    /// private, so producers still go through
484    /// [`TransportAdapter::admit_message`].
485    pub fn new(header: Bytes, payload: Bytes, guard: InFlightGuard) -> Self {
486        Self {
487            header,
488            payload,
489            guard,
490        }
491    }
492}
493
494/// What [`TransportAdapter::admit_message`] did with an inbound frame.
495///
496/// The rejecting variants hand the frame back, because the reply a transport
497/// owes its peer is transport-specific: a TCP listener writes a
498/// [`MessageType::ShuttingDown`] frame onto the same socket, NATS publishes to
499/// the reply inbox, gRPC pushes onto the RPC's server-to-client stream, and a
500/// reader with no reply path at all just drops it and records a rejection.
501///
502/// Deliberately *not* `#[non_exhaustive]`: each variant hands back a frame that
503/// demands a different mandatory action, so a transport that grew a `_ => {}`
504/// arm would silently swallow frames. A future outcome should be a compile
505/// error at every producer — worth the coordinated bump it would cost.
506#[derive(Debug)]
507#[must_use = "the rejecting variants hand back a frame the caller still owes its peer a reply for"]
508pub enum AdmitOutcome {
509    /// The frame is queued and counted; nothing left for the caller to do.
510    Admitted,
511    /// This instance is draining and will not accept new requests. Reply
512    /// [`MessageType::ShuttingDown`] with `header` echoed verbatim so the
513    /// sender fails fast instead of waiting out its own timeout.
514    Draining {
515        /// The rejected request's header.
516        header: Bytes,
517        /// The rejected request's payload.
518        payload: Bytes,
519    },
520    /// The inbound queue's receiver is gone — the runtime has torn down.
521    /// Route the frame to the transport's error handler.
522    Disconnected {
523        /// The undelivered frame's header.
524        header: Bytes,
525        /// The undelivered frame's payload.
526        payload: Bytes,
527    },
528}
529
530/// Sender-side handle given to transports for routing inbound frames.
531///
532/// Each transport receives a clone of this adapter during [`Transport::start`]
533/// and uses it to forward decoded `(header, payload)` pairs to the appropriate
534/// stream based on [`MessageType`]. Inbound [`MessageType::Message`] frames are
535/// the exception: they go through [`admit_message`](Self::admit_message), which
536/// owns both the drain gate and the enqueue.
537#[derive(Clone)]
538pub struct TransportAdapter {
539    /// Channel for inbound [`MessageType::Message`] frames.
540    ///
541    /// Private: every item must carry an [`InFlightGuard`], so the only way to
542    /// enqueue is [`admit_message`](Self::admit_message).
543    message_stream: flume::Sender<InboundMessage>,
544    /// Channel for inbound [`MessageType::Response`] frames.
545    pub response_stream: flume::Sender<(Bytes, Bytes)>,
546    /// Channel for inbound [`MessageType::Ack`] and [`MessageType::Event`] frames.
547    pub event_stream: flume::Sender<(Bytes, Bytes)>,
548    /// Channel for inbound [`MessageType::ShuttingDown`] frames — drain
549    /// rejections from a peer.
550    ///
551    /// Each carries the rejected *request's* header, echoed back verbatim so
552    /// the sender can correlate it, and an empty payload. The header is in
553    /// the request format, not the response format — which is why these
554    /// frames have their own lane instead of sharing `response_stream`.
555    pub shutdown_stream: flume::Sender<(Bytes, Bytes)>,
556    /// Shared shutdown coordinator for drain-aware routing.
557    pub shutdown_state: ShutdownState,
558}
559
560impl TransportAdapter {
561    /// Offer an inbound [`MessageType::Message`] frame to the runtime.
562    ///
563    /// This is the *only* way to enqueue inbound request work, and it is the
564    /// drain gate: transports must not pre-filter on
565    /// [`ShutdownState::is_draining`].
566    ///
567    /// # Why acquire before checking
568    ///
569    /// The order below is load-bearing: acquire the guard, *then* read the
570    /// draining flag, and only then send.
571    ///
572    /// Checking first and enqueueing second is a check-then-act race that no
573    /// memory ordering can fix — the whole of
574    /// [`begin_drain`](ShutdownState::begin_drain) →
575    /// [`wait_for_drain`](ShutdownState::wait_for_drain) → teardown can run
576    /// inside the gap between a producer's check and its send, and the message
577    /// then lands on a queue nobody will ever drain.
578    ///
579    /// Acquiring first makes the two sides a store-buffer litmus instead. This
580    /// side does `fetch_add(in_flight)` then `load(draining)`; the shutdown
581    /// side does `store(draining)` then `load(in_flight)`. All four accesses
582    /// are `SeqCst`, so they share one total order and at least one side must
583    /// observe the other: either the shutdown wait sees the increment and
584    /// parks until this message is done, or this call sees the flag and
585    /// rejects. Both-stale — admitted *and* invisible to the drain — cannot
586    /// happen.
587    ///
588    /// A rejected message therefore blips the in-flight count up and back
589    /// down. That is harmless: `wait_for_drain` registers its wakeup before
590    /// re-checking, so the blip costs it one extra loop iteration.
591    ///
592    /// Synchronous by design — the inbound channel is unbounded, so the send
593    /// never blocks, and callers on non-async threads (a ZMQ listener thread,
594    /// a UCX active-message callback) can use it unchanged.
595    pub fn admit_message(&self, header: Bytes, payload: Bytes) -> AdmitOutcome {
596        let guard = self.shutdown_state.acquire();
597
598        if self.shutdown_state.is_draining_for_admission() {
599            drop(guard);
600            return AdmitOutcome::Draining { header, payload };
601        }
602
603        match self
604            .message_stream
605            .send(InboundMessage::new(header, payload, guard))
606        {
607            Ok(()) => AdmitOutcome::Admitted,
608            Err(flume::SendError(InboundMessage {
609                header,
610                payload,
611                guard,
612                ..
613            })) => {
614                // Explicit: the guard must be released here, or an
615                // undeliverable frame would keep `wait_for_drain` parked
616                // forever under `ShutdownPolicy::WaitForever`.
617                drop(guard);
618                AdmitOutcome::Disconnected { header, payload }
619            }
620        }
621    }
622}
623
624/// Receiver-side handle for consuming inbound frames from all transports.
625///
626/// Returned by [`make_channels`] alongside the corresponding [`TransportAdapter`].
627/// Higher layers pull [`InboundMessage`]s off the message lane and
628/// `(header, payload)` pairs off the other three.
629pub struct DataStreams {
630    /// Receiver for inbound message frames.
631    ///
632    /// Every item is an [`InboundMessage`] carrying its own [`InFlightGuard`],
633    /// so a plain receive is already drain-tracked and the consumer must *not*
634    /// acquire a guard of its own. (This is why there is no
635    /// `recv_message_tracked`: it acquired after the dequeue, which left the
636    /// queued-but-unconsumed window invisible to the drain.)
637    ///
638    /// A consumer that stops receiving while messages are still queued must
639    /// drain and drop them, not just drop this receiver: flume keeps the
640    /// buffer alive until the *last* endpoint goes, and transports hold sender
641    /// clones for the instance's lifetime, so guards abandoned in the buffer
642    /// pin [`ShutdownState::wait_for_drain`] above zero for good.
643    pub message_stream: flume::Receiver<InboundMessage>,
644    /// Receiver for inbound response frames.
645    pub response_stream: flume::Receiver<(Bytes, Bytes)>,
646    /// Receiver for inbound ack and event frames.
647    pub event_stream: flume::Receiver<(Bytes, Bytes)>,
648    /// Receiver for inbound shutting-down frames (drain rejections): the
649    /// rejected request's header, echoed verbatim, with an empty payload.
650    pub shutdown_stream: flume::Receiver<(Bytes, Bytes)>,
651    /// Shared shutdown coordinator.
652    pub shutdown_state: ShutdownState,
653}
654
655type DataStreamTuple = (
656    flume::Receiver<InboundMessage>,
657    flume::Receiver<(Bytes, Bytes)>,
658    flume::Receiver<(Bytes, Bytes)>,
659    flume::Receiver<(Bytes, Bytes)>,
660);
661
662impl DataStreams {
663    /// Destructure into the four raw receivers
664    /// `(message, response, event, shutdown)`.
665    pub fn into_parts(self) -> DataStreamTuple {
666        (
667            self.message_stream,
668            self.response_stream,
669            self.event_stream,
670            self.shutdown_stream,
671        )
672    }
673}
674
675/// Create a matched pair of [`TransportAdapter`] (sender) and [`DataStreams`] (receiver).
676///
677/// Both sides share the same [`ShutdownState`] so drain coordination is automatic.
678pub fn make_channels() -> (TransportAdapter, DataStreams) {
679    let shutdown_state = ShutdownState::new();
680    let (message_tx, message_rx) = flume::unbounded();
681    let (response_tx, response_rx) = flume::unbounded();
682    let (event_tx, event_rx) = flume::unbounded();
683    let (shutdown_tx, shutdown_rx) = flume::unbounded();
684    (
685        TransportAdapter {
686            message_stream: message_tx,
687            response_stream: response_tx,
688            event_stream: event_tx,
689            shutdown_stream: shutdown_tx,
690            shutdown_state: shutdown_state.clone(),
691        },
692        DataStreams {
693            message_stream: message_rx,
694            response_stream: response_rx,
695            event_stream: event_rx,
696            shutdown_stream: shutdown_rx,
697            shutdown_state,
698        },
699    )
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use tokio::time::{sleep, timeout};
706
707    #[test]
708    fn test_shutdown_state_initial() {
709        let state = ShutdownState::new();
710        assert!(!state.is_draining());
711        assert_eq!(state.in_flight_count(), 0);
712    }
713
714    #[test]
715    fn test_begin_drain_flips_flag() {
716        let state = ShutdownState::new();
717        state.begin_drain();
718        assert!(state.is_draining());
719    }
720
721    #[test]
722    fn test_acquire_increments_inflight() {
723        let state = ShutdownState::new();
724        let _g1 = state.acquire();
725        assert_eq!(state.in_flight_count(), 1);
726    }
727
728    #[test]
729    fn test_guard_drop_decrements_inflight() {
730        let state = ShutdownState::new();
731        let g = state.acquire();
732        assert_eq!(state.in_flight_count(), 1);
733        drop(g);
734        assert_eq!(state.in_flight_count(), 0);
735    }
736
737    #[tokio::test]
738    async fn test_wait_for_drain_blocks_then_completes() {
739        let state = ShutdownState::new();
740        let guard = state.acquire();
741
742        let state_clone = state.clone();
743        let handle = tokio::spawn(async move {
744            state_clone.wait_for_drain().await;
745        });
746
747        sleep(Duration::from_millis(50)).await;
748        assert!(!handle.is_finished());
749
750        drop(guard);
751        timeout(Duration::from_millis(100), handle)
752            .await
753            .expect("should complete after guard drop")
754            .unwrap();
755    }
756
757    #[test]
758    fn test_message_type_roundtrip() {
759        for v in 0..=4 {
760            let mt = MessageType::from_u8(v).unwrap();
761            assert_eq!(mt.as_u8(), v);
762        }
763        assert_eq!(MessageType::from_u8(5), None);
764    }
765
766    #[test]
767    fn test_make_channels_includes_shutdown_state() {
768        let (adapter, streams) = make_channels();
769        assert!(!adapter.shutdown_state.is_draining());
770        adapter.shutdown_state.begin_drain();
771        assert!(streams.shutdown_state.is_draining());
772    }
773
774    // ---------------------------------------------------------------------
775    // Admission / drain-visibility invariants.
776    //
777    // The property under test is "queued implies counted": a frame that
778    // `admit_message` put on the inbound queue keeps `wait_for_drain` parked
779    // until somebody takes ownership of it and drops it. Before the guard rode
780    // inside the queued item, the consumer acquired it only *after* the
781    // dequeue, so a queued-but-unconsumed frame was invisible to the drain and
782    // `wait_for_drain` returned while it was still sitting there.
783    // ---------------------------------------------------------------------
784
785    /// A frame sitting on the inbound queue is counted work: `wait_for_drain`
786    /// must park until it is received *and dropped*, not until it is enqueued.
787    #[tokio::test]
788    async fn queued_message_holds_drain() {
789        let (adapter, streams) = make_channels();
790
791        let outcome = adapter.admit_message(
792            Bytes::from_static(b"queued-header"),
793            Bytes::from_static(b"queued-payload"),
794        );
795        assert!(matches!(outcome, AdmitOutcome::Admitted));
796        assert_eq!(
797            adapter.shutdown_state.in_flight_count(),
798            1,
799            "a queued message must be counted work the moment it is admitted"
800        );
801
802        adapter.shutdown_state.begin_drain();
803
804        let waiter_state = adapter.shutdown_state.clone();
805        let waiter = tokio::spawn(async move { waiter_state.wait_for_drain().await });
806
807        sleep(Duration::from_millis(50)).await;
808        assert!(
809            !waiter.is_finished(),
810            "wait_for_drain completed while a message was still queued and undispatched"
811        );
812
813        let queued = timeout(
814            Duration::from_millis(500),
815            streams.message_stream.recv_async(),
816        )
817        .await
818        .expect("the admitted message must still be on the queue")
819        .expect("recv");
820        assert_eq!(&queued.header[..], b"queued-header");
821        assert_eq!(&queued.payload[..], b"queued-payload");
822
823        // Dropping the message drops its guard — this is what a consumer that
824        // finished dispatching does.
825        drop(queued);
826
827        timeout(Duration::from_millis(500), waiter)
828            .await
829            .expect("wait_for_drain must complete once the queued message is released")
830            .expect("waiter task panicked");
831        assert_eq!(streams.shutdown_state.in_flight_count(), 0);
832    }
833
834    /// Admission during drain rejects and hands the frame back, and the guard
835    /// it acquired to close the check-then-act race is released again.
836    #[tokio::test]
837    async fn admit_message_rejects_during_drain() {
838        let (adapter, streams) = make_channels();
839        adapter.shutdown_state.begin_drain();
840
841        match adapter.admit_message(
842            Bytes::from_static(b"reject-header"),
843            Bytes::from_static(b"reject-payload"),
844        ) {
845            AdmitOutcome::Draining { header, payload } => {
846                assert_eq!(&header[..], b"reject-header");
847                assert_eq!(&payload[..], b"reject-payload");
848            }
849            AdmitOutcome::Admitted => panic!("a draining instance must not admit a Message"),
850            AdmitOutcome::Disconnected { .. } => panic!("the receiver is still alive"),
851        }
852
853        assert!(
854            streams.message_stream.is_empty(),
855            "a rejected message must not reach the queue"
856        );
857        assert_eq!(
858            adapter.shutdown_state.in_flight_count(),
859            0,
860            "the acquire-then-check probe guard must not outlive the rejection"
861        );
862
863        // The blip must not have left a waiter stranded either.
864        timeout(
865            Duration::from_millis(500),
866            adapter.shutdown_state.wait_for_drain(),
867        )
868        .await
869        .expect("wait_for_drain must complete after a rejected admission");
870    }
871
872    /// Discarding the inbound channel with work still on it releases every
873    /// guard that work was holding — nobody has to compensate by hand.
874    ///
875    /// This is the RAII property the mandatory guard buys. A design where
876    /// producers bump a fungible counter and the consumer "adopts" it on
877    /// dequeue leaks the entire backlog here, and every later
878    /// `wait_for_drain` hangs on a count that can no longer reach zero.
879    ///
880    /// Note *when* the release happens: flume keeps the queue alive until the
881    /// last endpoint goes, so dropping only the receiver is not enough — the
882    /// guards go with the buffer, once both ends are gone. Held deliberately
883    /// as one assertion at the end rather than after each drop, so this test
884    /// does not turn flume's current ordering into a requirement.
885    #[test]
886    fn dropped_channel_releases_queued_guards() {
887        let (adapter, streams) = make_channels();
888        let state = adapter.shutdown_state.clone();
889
890        for i in 0..8u8 {
891            let outcome = adapter.admit_message(
892                Bytes::from(vec![b'h', i]),
893                Bytes::from_static(b"queued-payload"),
894            );
895            assert!(matches!(outcome, AdmitOutcome::Admitted));
896        }
897        assert_eq!(state.in_flight_count(), 8);
898
899        // Teardown: the consumer's receiver goes, then the transports holding
900        // the adapter clones go.
901        drop(streams);
902        drop(adapter);
903
904        assert_eq!(
905            state.in_flight_count(),
906            0,
907            "discarding the inbound queue must release every guard it was holding"
908        );
909    }
910
911    /// An undeliverable frame comes back to the caller so it can route it to
912    /// the transport's error handler — and its guard is released, or the
913    /// frame nobody can deliver would keep `wait_for_drain` parked forever.
914    #[tokio::test]
915    async fn admit_message_disconnected_returns_frames() {
916        let (adapter, streams) = make_channels();
917        drop(streams);
918
919        match adapter.admit_message(
920            Bytes::from_static(b"orphan-header"),
921            Bytes::from_static(b"orphan-payload"),
922        ) {
923            AdmitOutcome::Disconnected { header, payload } => {
924                assert_eq!(&header[..], b"orphan-header");
925                assert_eq!(&payload[..], b"orphan-payload");
926            }
927            AdmitOutcome::Admitted => panic!("there is no receiver left to admit to"),
928            AdmitOutcome::Draining { .. } => panic!("the instance is not draining"),
929        }
930
931        assert_eq!(
932            adapter.shutdown_state.in_flight_count(),
933            0,
934            "an undeliverable frame must not strand the drain"
935        );
936        timeout(
937            Duration::from_millis(500),
938            adapter.shutdown_state.wait_for_drain(),
939        )
940        .await
941        .expect("wait_for_drain must complete after an undeliverable admission");
942    }
943
944    /// `wait_for_drain` must not lose the wakeup when the last guard drops
945    /// while it is between reading the counter and parking.
946    ///
947    /// `notify_waiters()` stores no permit, so a `Notified` future created
948    /// *after* the drop never hears it. The fix creates the future before
949    /// reading the counter; tokio records the `notify_waiters` generation at
950    /// construction, so a future that merely exists when the drop happens
951    /// completes on its first poll.
952    ///
953    /// The window is a handful of instructions inside a single task, so this
954    /// scans it rather than hitting one interleaving. Both sides start from
955    /// the same reference point — the waiter arming `armed` — and each then
956    /// burns a busy-wait: the waiter a fixed `WAITER_LEAD`, the dropper a
957    /// `spins` that sweeps `0..SPIN_SWEEP` across iterations. The fixed lead
958    /// pays for the dropper's cache-coherence latency in seeing `armed`, which
959    /// otherwise makes it land systematically *after* the window; the sweep
960    /// then walks the drop across it. A lost wakeup is permanent, so the
961    /// per-iteration bound is short and the first hit fails the test.
962    ///
963    /// Probabilistic by nature, but the constants are measured, not guessed:
964    /// against the pre-fix `while load { notified().await }` the first hit
965    /// landed between iteration 819 and 6703 across runs on two machines, so
966    /// `ITERATIONS` leaves roughly 5x headroom over the worst observed.
967    ///
968    /// The per-iteration bound is a *detector*, not a deadline: a runner that
969    /// fails to schedule the freshly spawned dropper thread inside it looks
970    /// identical to a lost wakeup from here. So a timeout joins the dropper —
971    /// making the guard drop a fact rather than an assumption — and re-awaits
972    /// under a generous grace window. A genuinely lost wakeup is permanent
973    /// (nothing else ever calls `notify_waiters`), so the grace window costs
974    /// no detection power and turns a scheduler stall back into a pass.
975    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
976    async fn wait_for_drain_survives_guard_dropped_at_the_check() {
977        const ITERATIONS: usize = 32768;
978        const SPIN_SWEEP: usize = 1024;
979        const WAITER_LEAD: usize = 600;
980        /// Second chance after the detector window, for a stalled runner.
981        const GRACE: Duration = Duration::from_secs(2);
982
983        // `black_box` inside each busy-wait keeps LLVM from folding the sum
984        // into a closed form and deleting the delay the scan depends on.
985        fn burn(rounds: usize) {
986            let mut sink = 0usize;
987            for k in 0..rounds {
988                sink = std::hint::black_box(sink.wrapping_add(k));
989            }
990        }
991
992        for iteration in 0..ITERATIONS {
993            let state = ShutdownState::new();
994            let guard = state.acquire();
995
996            let armed = Arc::new(AtomicBool::new(false));
997            let spins = iteration % SPIN_SWEEP;
998
999            let dropper_armed = armed.clone();
1000            let dropper = std::thread::spawn(move || {
1001                while !dropper_armed.load(Ordering::Acquire) {
1002                    std::hint::spin_loop();
1003                }
1004                burn(spins);
1005                drop(guard);
1006            });
1007
1008            let waiter_state = state.clone();
1009            let mut waiter = tokio::spawn(async move {
1010                armed.store(true, Ordering::Release);
1011                burn(WAITER_LEAD);
1012                waiter_state.wait_for_drain().await;
1013            });
1014
1015            let finished = timeout(Duration::from_millis(200), &mut waiter).await;
1016            dropper.join().expect("dropper thread panicked");
1017            let joined = match finished {
1018                Ok(joined) => joined,
1019                // The drop has definitely happened now (the thread is joined),
1020                // so anything still parked is either a lost wakeup or a stall
1021                // that outlived the detector window.
1022                Err(_) => timeout(GRACE, &mut waiter).await.unwrap_or_else(|_| {
1023                    panic!(
1024                        "wait_for_drain lost the drain wakeup (iteration {iteration}, spins {spins})"
1025                    )
1026                }),
1027            };
1028            joined.expect("waiter task panicked");
1029        }
1030    }
1031}