Skip to main content

melin_server_runtime/
reader.rs

1//! io_uring-based multiplexed reader with multishot RECV.
2//!
3//! Uses `IORING_OP_RECV` with `IORING_RECV_MULTISHOT` — a single SQE per
4//! connection produces multiple CQEs as data arrives, eliminating the
5//! resubmission overhead of standard RECV. Combined with a ring-mapped
6//! provided-buffer ring (`IOSQE_BUFFER_SELECT` + `buf_ring`), the kernel
7//! selects a buffer from a shared pool for each recv, and consumed
8//! buffers are recycled with a shared-memory store — no per-recv
9//! ProvideBuffers SQE/CQE round trip. Requires kernel ≥ 5.19.
10//!
11//! Uses a single reader thread — io_uring is efficient enough for hundreds
12//! of connections. New connections are registered via eventfd wakeup.
13//!
14//! Connection state is stored in a slab (index-stable Vec) so that io_uring
15//! user_data carries a slab index, not an fd. This avoids fd-reuse races
16//! where a recycled fd number could match a stale CQE.
17
18use std::collections::HashMap;
19use std::net::SocketAddr;
20use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::mpsc;
24use std::thread::JoinHandle;
25use std::time::{Duration, Instant};
26
27use io_uring::{IoUring, opcode, types};
28use tracing::{debug, error};
29
30use crate::ControlEvent;
31use crate::buf_ring::BufRing;
32use crate::early_reply::EarlyReplySender;
33use crate::halt::HaltGate;
34use crate::policy_control::{AckPolicyPublisher, BootedClaim};
35use melin_app::Application;
36use melin_app::auth::{ClientRole, RoleId};
37use melin_app::decoder::ErasedDecoder;
38
39/// Decoder type alias: the application's request decoder, bound to its
40/// `Event` type, with its role type erased (see [`ErasedDecoder`]).
41/// Mirrors [`crate::response::ResponseEncoderArc`]; hides the
42/// `dyn ErasedDecoder<…>` spelling at call sites that thread the decoder
43/// through several functions.
44pub type RequestDecoderArc<A> = Arc<dyn ErasedDecoder<<A as Application>::Event>>;
45use melin_app::unix_epoch_nanos;
46use melin_pipeline::ring;
47use melin_transport_core::health::IngressMetrics;
48use melin_transport_core::pipeline::InputSlot;
49use melin_transport_core::tick::TickSchedule;
50
51/// Size of each provided buffer. 4 KiB accommodates multiple frames per
52/// recv (frames are typically <100 bytes).
53///
54/// The number of buffers in the shared pool, and the ring's depth, are
55/// not constants: both are derived from `--max-connections` at startup —
56/// see [`RingSizing`]. The pool is a power of two (buf_ring ABI) with two
57/// buffers per connection. On exhaustion the kernel completes the
58/// multishot with `ENOBUFS` (no data consumed) and the loop re-arms it
59/// after the drain's recycles refill the ring. The ring holds a drain's
60/// worth of SQEs: two per connection (a multishot re-arm and a teardown
61/// cancel) plus housekeeping; buffer recycling goes through the buf_ring,
62/// not the SQ.
63const BUF_SIZE: usize = 4096;
64
65/// Buffer group ID for the provided recv buffer pool.
66const BUF_GROUP_ID: u16 = 0;
67
68use crate::client_frames::{ClientIngress, FrameAction, LongParkReport, MAX_FRAME_SIZE};
69use crate::connection_limit::{RingSizing, await_startup, ring_setup_error};
70
71/// User data sentinel for the eventfd read SQE.
72const EVENTFD_TOKEN: u64 = u64::MAX;
73
74/// User data sentinel for legacy ProvideBuffers CQEs (fallback recycle
75/// path only). Best-effort re-provisions: we log errors but don't act
76/// on success.
77const PROVIDE_BUFS_TOKEN: u64 = u64::MAX - 1;
78
79/// User data sentinel for AsyncCancel SQEs (connection teardown and the
80/// shutdown quiesce). The completion carries no actionable information:
81/// `ENOENT`/`EALREADY` just mean the target op already finished.
82const CANCEL_TOKEN: u64 = u64::MAX - 3;
83
84/// User data sentinel for the tick timeout SQE. The reader arms a single
85/// `IORING_OP_TIMEOUT` per cadence so `submit_and_wait` returns at the tick
86/// deadline even when no client traffic is flowing. The CQE itself carries
87/// no information; the loop body checks `Instant::now()` against the next
88/// deadline and emits the actual `JournalEvent::Tick`.
89const TICK_TIMEOUT_TOKEN: u64 = u64::MAX - 2;
90
91/// User data sentinel for the park-retry timeout SQE: armed while any
92/// connection is parked, so the reader comes back round to retry them
93/// even when nothing else wakes it (see [`PARK_RETRY`]). Like the tick's,
94/// its CQE carries no information.
95const PARK_RETRY_TOKEN: u64 = u64::MAX - 4;
96
97/// The longest a parked connection waits for its retry once the
98/// early-reply queue has room again.
99///
100/// Nothing tells the reader that the response stage drained the queue,
101/// so while any connection is parked its wait in the kernel is bounded by
102/// a timeout of this length. A wake from the response stage (the eventfd)
103/// would resume sooner, but needs a fenced park/drain handshake against a
104/// lost wake, and puts a `write` on the response stage's drain, to shorten
105/// a path that is degraded by definition: a park happens only while that
106/// stage is not draining. Any other wake retries parked connections too.
107const PARK_RETRY: Duration = Duration::from_millis(1);
108
109/// The longest a park's `AsyncCancel` may go unanswered before the reader
110/// stops waiting on it and closes the connection.
111///
112/// An honoured cancel's terminal CQE lands within the same submit or the
113/// next, so a cancel still in flight after this long was not honoured:
114/// some hosts filter io_uring opcodes (see the shutdown quiesce), and a
115/// filtered cancel completes with `EINVAL`, leaving the multishot armed.
116/// The connection's bytes would then keep landing while parked, growing
117/// its buffer with nothing pushing back on the client. Closing it is the
118/// behaviour before parking existed; teardown's `shutdown(2)` ends the
119/// multishot whatever the host filters.
120const PARK_CANCEL_DEADLINE: Duration = Duration::from_secs(1);
121
122/// CQE flag: buffer ID is valid in upper 16 bits of flags.
123const IORING_CQE_F_BUFFER: u32 = 1 << 0;
124
125/// CQE flag: more completions coming from this multishot operation.
126const IORING_CQE_F_MORE: u32 = 1 << 1;
127
128/// Bit shift to extract buffer ID from CQE flags.
129const IORING_CQE_BUFFER_SHIFT: u32 = 16;
130
131use melin_wire_protocol::control::ConnectionId;
132
133/// Command sent from the accept loop to a reader thread.
134pub struct ReaderRegistration<R> {
135    pub connection_id: ConnectionId,
136    pub reader: R,
137    pub addr: SocketAddr,
138    /// The client's role, established during the auth handshake.
139    pub role: ClientRole<RoleId>,
140    /// FxHash of the client's Ed25519 public key. Stored per-connection
141    /// and copied into every InputSlot as the submitting key's identity.
142    pub key_hash: u64,
143}
144
145/// Handle for the accept loop to register connections with the io_uring reader.
146pub struct UringReaderHandle<R> {
147    tx: mpsc::Sender<ReaderRegistration<R>>,
148    /// Wakeup eventfd, shared with the reader thread.
149    ///
150    /// `Arc<OwnedFd>` rather than a `RawFd` so the descriptor cannot be
151    /// closed while the other side still holds it: the close happens in
152    /// `OwnedFd`'s `Drop`, which runs when the last holder goes away, and
153    /// no code here is able to call `close` early because none of it owns
154    /// a raw descriptor to close.
155    ///
156    /// The reader thread used to close this itself while this handle was
157    /// still writing wakeups to it — a use-after-close whose real danger
158    /// was descriptor reuse, not `EBADF`: once the number is free, a
159    /// journal segment or an accepted connection can be assigned it, and
160    /// the next wakeup writes eight bytes into that instead. Found by
161    /// ThreadSanitizer.
162    event_fd: Arc<OwnedFd>,
163    join_handle: Option<JoinHandle<()>>,
164    shutdown: Arc<AtomicBool>,
165}
166
167impl<R> UringReaderHandle<R> {
168    /// Register a new connection with the reader thread.
169    ///
170    /// If the reader thread's channel is dead (thread panicked), logs an
171    /// error and signals shutdown so the server can restart cleanly.
172    pub fn register(&mut self, registration: ReaderRegistration<R>) {
173        if self.tx.send(registration).is_ok() {
174            wake_reader(&self.event_fd);
175        } else {
176            error!("reader thread dead, cannot register connection");
177            self.shutdown.store(true, Ordering::Relaxed);
178        }
179    }
180
181    /// Signal the reader thread to shut down and wake it from io_uring_enter.
182    pub fn shutdown(&self) {
183        self.shutdown.store(true, Ordering::Relaxed);
184        wake_reader(&self.event_fd);
185    }
186
187    /// Join the reader thread. Call after `shutdown()`.
188    pub fn join(mut self) {
189        if let Some(handle) = self.join_handle.take()
190            && let Err(panic) = handle.join()
191        {
192            let msg = panic
193                .downcast_ref::<&str>()
194                .copied()
195                .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
196                .unwrap_or("<non-string panic>");
197            error!(message = msg, "reader thread panicked");
198        }
199    }
200}
201
202/// Wake the reader from `io_uring_enter` through its eventfd: the loop
203/// comes back round, and sees whatever it was woken for (a registration,
204/// shutdown, an ack-policy request).
205///
206/// A failed write is not reported: on a nonblocking eventfd it can only be
207/// `EAGAIN`, the counter at its maximum, which means a wakeup is already
208/// pending.
209pub(crate) fn wake_reader(event_fd: &OwnedFd) {
210    let val: u64 = 1;
211    // SAFETY: `event_fd` is a live descriptor (borrowed from its owner), and
212    // the buffer is a live `u64`, of the length passed.
213    unsafe {
214        libc::write(
215            event_fd.as_raw_fd(),
216            &val as *const u64 as *const libc::c_void,
217            8,
218        );
219    }
220}
221
222/// Spawn the io_uring reader thread. Returns a handle for registering
223/// connections.
224///
225/// One reader thread serves every TCP connection on the server. io_uring
226/// with multishot RECV multiplexes thousands of sockets efficiently and the
227/// matching stage is the throughput limit, so adding more reader threads
228/// would not raise throughput — it would only re-introduce contention on
229/// the input ring's multi-producer cursor.
230///
231/// `tick_cadence: Some(d)` makes the reader the clock-tick generator: it
232/// arms an `IORING_OP_TIMEOUT` so `submit_and_wait` returns at the tick
233/// deadline even when no client traffic is flowing, then publishes a
234/// `JournalEvent::Tick { now_ns }` onto the same input ring it uses for
235/// client requests. Pass `None` to disable the tick (useful for benchmarks
236/// that don't exercise time-driven features).
237///
238/// Every request it does not publish — one the application's decoder
239/// filters or refuses, a write refused while `halt` refuses writes, one
240/// shed on a full input ring — the reader answers through
241/// `early_replies` — see [`crate::halt`]. It counts those it sheds,
242/// filters and refuses into `metrics`, with the connections it parks, for
243/// the health endpoint.
244///
245/// The reader also journals the admin `ACK-POLICY` command's requests: it
246/// attaches `ack_policy` with its wakeup eventfd, so a request posted while
247/// it is parked brings it back round its loop, which publishes the entry
248/// (see [`crate::policy_control`]).
249///
250/// `sizing` sets the ring's depth and the provided-buffer pool (see
251/// [`RingSizing`]). Returns once the reader holds its io_uring instance
252/// and buffer pool, or with the error that kept it from getting them —
253/// a startup error, not a dead thread: the kernel charges ring memory
254/// against the locked-memory limit, and running out of it is an
255/// operator-fixable condition the node has to report.
256#[allow(clippy::too_many_arguments)]
257pub fn spawn_reader<A: Application, R: AsRawFd + Send + 'static>(
258    mut producer: ring::Producer<InputSlot<A::Event>>,
259    decoder: RequestDecoderArc<A>,
260    halt: HaltGate,
261    early_replies: EarlyReplySender<A::Report>,
262    metrics: Arc<IngressMetrics>,
263    ack_policy: BootedClaim,
264    control_tx: mpsc::Sender<ControlEvent>,
265    core: usize,
266    connection_timeout: Option<Duration>,
267    tick_cadence: Option<Duration>,
268    sizing: RingSizing,
269    shutdown: Arc<AtomicBool>,
270) -> std::io::Result<UringReaderHandle<R>>
271where
272    A::Event: Send + Sync + 'static,
273    A::Report: Send + 'static,
274{
275    let (tx, rx) = mpsc::channel();
276
277    let raw_event_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
278    if raw_event_fd < 0 {
279        let err = std::io::Error::last_os_error();
280        return Err(std::io::Error::new(
281            err.kind(),
282            format!("uring-reader: cannot create its wakeup eventfd: {err}"),
283        ));
284    }
285
286    // SAFETY: `eventfd` returned a fresh descriptor (checked non-negative
287    // above) that nothing else owns, so transferring ownership to `OwnedFd`
288    // is sound. From here the raw number is never closed by hand — the
289    // descriptor lives exactly as long as the last `Arc` holder.
290    let event_fd = Arc::new(unsafe { OwnedFd::from_raw_fd(raw_event_fd) });
291    let wakeup_fd = Arc::clone(&event_fd);
292    let shutdown_clone = Arc::clone(&shutdown);
293    // Attached here, on this thread, which still owns the producer alone
294    // (a policy staged during bring-up is journaled now), with the
295    // eventfd: if the thread never starts, the publisher is dropped with
296    // its closure and detaches.
297    let ack_policy = ack_policy.attach(&mut producer, &halt, Some(Arc::clone(&event_fd)));
298
299    // The thread's startup report. The ring has to be created on the
300    // thread that submits to it (it is SINGLE_ISSUER: the kernel binds
301    // it to its creator, and a ring moved across threads fails every
302    // submit), so the spawner cannot create it itself and learns the
303    // outcome here instead. Capacity one so the report never blocks the
304    // worker on the spawner reaching `recv`.
305    let (ready_tx, ready_rx) = mpsc::sync_channel::<std::io::Result<()>>(1);
306
307    // `core == 0` is the unpinned sentinel, which puts the thread on the
308    // process's home CPU mask — see the `melin_app::affinity` module docs.
309    let handle = melin_app::affinity::spawn_placed("uring-reader", core, move || {
310        reader_loop::<A, R>(
311            rx,
312            wakeup_fd,
313            producer,
314            &*decoder,
315            &halt,
316            early_replies,
317            &metrics,
318            ack_policy,
319            &control_tx,
320            connection_timeout,
321            tick_cadence,
322            sizing,
323            ready_tx,
324            &shutdown_clone,
325        );
326    })
327    .map_err(|e| {
328        let e = std::io::Error::from(e);
329        std::io::Error::new(
330            e.kind(),
331            format!("uring-reader: cannot start its thread: {e}"),
332        )
333    })?;
334
335    if let Err(e) = await_startup("uring-reader", &ready_rx) {
336        // The thread has returned (or panicked) by now; a panic was
337        // already reported by the panic hook, and the startup error is
338        // what the caller needs, so the join result adds nothing.
339        let _ = handle.join();
340        return Err(e);
341    }
342
343    Ok(UringReaderHandle {
344        tx,
345        event_fd,
346        join_handle: Some(handle),
347        shutdown,
348    })
349}
350
351// ---------------------------------------------------------------------------
352// Slab-based connection storage
353// ---------------------------------------------------------------------------
354
355/// Per-connection state for multishot io_uring recv + incremental frame parsing.
356struct ConnectionEntry<R> {
357    connection_id: u64,
358    addr: SocketAddr,
359    /// The client's role, from the auth handshake. Checked per-request
360    /// by the decoder on the reader thread (cold path), zero cost on the
361    /// matching stage.
362    role: ClientRole<RoleId>,
363    /// FxHash of the client's Ed25519 public key. Copied into every
364    /// InputSlot as the submitting key's identity.
365    key_hash: u64,
366    /// Owned reader — keeps the fd alive. Dropping closes the fd.
367    _reader: R,
368    fd: RawFd,
369    /// Bytes received and not yet processed, and what is left of a shed
370    /// tail: the input state the DPDK poll loop keeps too.
371    ingress: ClientIngress,
372    /// Where the connection's receive stands with the kernel: armed,
373    /// between two arms, parked, or dying.
374    recv: RecvState,
375    /// Last time any data was received on this connection. Used for
376    /// idle timeout detection.
377    last_activity: Instant,
378}
379
380/// Where a connection's receive stands with the kernel.
381///
382/// One enum rather than flags, so a parked connection cannot be re-armed
383/// or freed by the paths written for the others: every change goes
384/// through a method here, each an exhaustive `match`, and the reader acts
385/// on the step it returns.
386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387enum RecvState {
388    /// A multishot receive is armed: the kernel posts CQEs for it until
389    /// one without `IORING_CQE_F_MORE`.
390    Armed,
391    /// No receive armed, none owed: the multishot ended (pool exhaustion,
392    /// or the kernel's own decision) and is re-armed in the same drain.
393    Idle,
394    /// Parked (see `client_frames`): the early-reply queue had no room for
395    /// an answer, so the connection is not read, and its buffer is retried
396    /// on every wake of the reader. `cancelling`: the multishot's
397    /// `AsyncCancel` is in flight, so the kernel still owes its terminal
398    /// CQE, and data CQEs that land before it are kept. `since`: when it
399    /// parked, for the long-park report.
400    Parked { cancelling: bool, since: Instant },
401    /// A retry came back with the buffer processed while the park's cancel
402    /// was still in flight: the receive is re-armed on the cancelled one's
403    /// terminal CQE, never before, so two multishots never feed one buffer.
404    Resuming,
405    /// Torn down while the kernel still owes a terminal CQE. The slab index
406    /// must stay allocated until it arrives: freed early, the LIFO free
407    /// list hands the index to a new registration while the kernel can
408    /// still post CQEs carrying it, and the old peer's bytes would be
409    /// parsed under the new connection's identity and permissions (audit
410    /// review F1). Its CQEs recycle their buffers and are otherwise
411    /// discarded.
412    Dying,
413}
414
415/// What a receive CQE carries, as far as the receive state cares.
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417enum RecvCompletion {
418    /// Bytes, in a provided buffer.
419    Data,
420    /// Nothing, and not the end of the connection: the pool ran dry at
421    /// buffer selection (`ENOBUFS`, nothing read: the bytes wait in the
422    /// socket), the park's cancel landed (`ECANCELED`), or a data CQE
423    /// without a buffer.
424    Empty,
425    /// The peer closed the connection (0), or the receive failed.
426    Closed,
427}
428
429/// What the reader does with a receive CQE ([`RecvState::on_cqe`]). Its
430/// buffer, if it carries one, is recycled whatever the step, once any
431/// bytes are copied out.
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
433enum CqeStep {
434    /// A torn-down connection's CQE: discarded, and the slab index freed
435    /// when it is the terminal one.
436    Discard { free: bool },
437    /// The connection is over: free it and tell the response stage.
438    Remove,
439    /// Append the bytes and process the buffer, then act on
440    /// [`RecvState::after_frames`].
441    Process,
442    /// Append the bytes and nothing more: the connection is parked, and the
443    /// retry processes them.
444    Keep,
445    /// Arm a new multishot receive (the state is `Armed` already).
446    Arm,
447    /// Nothing to do.
448    Nothing,
449}
450
451/// What the reader does once a connection's buffer has been processed
452/// ([`RecvState::after_frames`]).
453#[derive(Clone, Copy, Debug, PartialEq, Eq)]
454enum FramesStep {
455    /// Nothing to do.
456    Nothing,
457    /// Arm a new multishot receive (the state is `Armed` already).
458    Arm,
459    /// Cancel the armed multishot: the connection has parked.
460    Cancel,
461    /// Tear the connection down ([`begin_teardown`]).
462    Teardown,
463}
464
465/// What tearing a connection down takes ([`RecvState::on_teardown`]).
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467enum TeardownStep {
468    /// Already dying: nothing more, no second `Disconnected`.
469    AlreadyDying,
470    /// Nothing can post a CQE for it: free the slab index now.
471    FreeNow,
472    /// Cancel the armed multishot, and free on its terminal CQE.
473    Cancel,
474    /// A cancel is already in flight (the park's): free on its terminal
475    /// CQE, with no second `AsyncCancel`.
476    AwaitCancel,
477}
478
479impl RecvState {
480    /// The state after a receive CQE, and what the reader does with it.
481    /// `terminal`: the CQE ends the armed multishot (no `F_MORE`).
482    fn on_cqe(self, completion: RecvCompletion, terminal: bool) -> (RecvState, CqeStep) {
483        if self == RecvState::Dying {
484            return (self, CqeStep::Discard { free: terminal });
485        }
486        let after = if terminal {
487            self.after_terminal()
488        } else {
489            self
490        };
491        match completion {
492            // Terminal for the armed op, so the index is free to reuse.
493            RecvCompletion::Closed => (after, CqeStep::Remove),
494            RecvCompletion::Empty => match after {
495                RecvState::Idle => (RecvState::Armed, CqeStep::Arm),
496                _ => (after, CqeStep::Nothing),
497            },
498            RecvCompletion::Data => match after {
499                RecvState::Parked { .. } => (after, CqeStep::Keep),
500                _ => (after, CqeStep::Process),
501            },
502        }
503    }
504
505    /// The state once the armed multishot has posted its last CQE.
506    fn after_terminal(self) -> RecvState {
507        match self {
508            RecvState::Armed | RecvState::Resuming => RecvState::Idle,
509            RecvState::Parked { since, .. } => RecvState::Parked {
510                cancelling: false,
511                since,
512            },
513            // Nothing was armed, or the caller frees it.
514            RecvState::Idle | RecvState::Dying => self,
515        }
516    }
517
518    /// The state after the connection's buffer was processed with
519    /// `action` at `now`, and what the reader does next. A parked
520    /// connection that parks again keeps its `since`.
521    fn after_frames(self, action: FrameAction, now: Instant) -> (RecvState, FramesStep) {
522        match action {
523            FrameAction::Disconnect => (self, FramesStep::Teardown),
524            FrameAction::Continue => match self {
525                RecvState::Armed | RecvState::Resuming | RecvState::Dying => {
526                    (self, FramesStep::Nothing)
527                }
528                RecvState::Parked {
529                    cancelling: true, ..
530                } => (RecvState::Resuming, FramesStep::Nothing),
531                RecvState::Idle
532                | RecvState::Parked {
533                    cancelling: false, ..
534                } => (RecvState::Armed, FramesStep::Arm),
535            },
536            FrameAction::Park => match self {
537                RecvState::Armed => (
538                    RecvState::Parked {
539                        cancelling: true,
540                        since: now,
541                    },
542                    FramesStep::Cancel,
543                ),
544                RecvState::Resuming => (
545                    RecvState::Parked {
546                        cancelling: true,
547                        since: now,
548                    },
549                    FramesStep::Nothing,
550                ),
551                RecvState::Idle => (
552                    RecvState::Parked {
553                        cancelling: false,
554                        since: now,
555                    },
556                    FramesStep::Nothing,
557                ),
558                RecvState::Parked { .. } | RecvState::Dying => (self, FramesStep::Nothing),
559            },
560        }
561    }
562
563    /// The state once teardown has begun, and what it takes.
564    fn on_teardown(self) -> (RecvState, TeardownStep) {
565        match self {
566            RecvState::Dying => (self, TeardownStep::AlreadyDying),
567            RecvState::Idle
568            | RecvState::Parked {
569                cancelling: false, ..
570            } => (self, TeardownStep::FreeNow),
571            RecvState::Armed => (RecvState::Dying, TeardownStep::Cancel),
572            RecvState::Resuming
573            | RecvState::Parked {
574                cancelling: true, ..
575            } => (RecvState::Dying, TeardownStep::AwaitCancel),
576        }
577    }
578
579    /// Whether, at `now`, a park's cancel has been in flight for longer
580    /// than [`PARK_CANCEL_DEADLINE`]: the host did not honour it.
581    fn park_cancel_overdue(self, now: Instant) -> bool {
582        match self {
583            RecvState::Parked {
584                cancelling: true,
585                since,
586            } => now.duration_since(since) > PARK_CANCEL_DEADLINE,
587            RecvState::Armed
588            | RecvState::Idle
589            | RecvState::Resuming
590            | RecvState::Dying
591            | RecvState::Parked {
592                cancelling: false, ..
593            } => false,
594        }
595    }
596
597    /// Whether the kernel still owes this connection a CQE: what the
598    /// shutdown quiesce waits out.
599    fn kernel_owes_cqe(self) -> bool {
600        match self {
601            RecvState::Armed
602            | RecvState::Resuming
603            | RecvState::Dying
604            | RecvState::Parked {
605                cancelling: true, ..
606            } => true,
607            RecvState::Idle
608            | RecvState::Parked {
609                cancelling: false, ..
610            } => false,
611        }
612    }
613}
614
615/// Index-stable allocator for connection state. Slab indices are used as
616/// io_uring user_data, avoiding fd-reuse races.
617struct ConnectionSlab<R> {
618    entries: Vec<Option<ConnectionEntry<R>>>,
619    /// Recycled indices for O(1) allocation.
620    free: Vec<usize>,
621}
622
623impl<R> ConnectionSlab<R> {
624    fn new() -> Self {
625        Self {
626            entries: Vec::with_capacity(256),
627            free: Vec::new(),
628        }
629    }
630
631    /// Insert a connection, returning its stable slab index.
632    fn insert(&mut self, entry: ConnectionEntry<R>) -> usize {
633        if let Some(idx) = self.free.pop() {
634            self.entries[idx] = Some(entry);
635            idx
636        } else {
637            let idx = self.entries.len();
638            self.entries.push(Some(entry));
639            idx
640        }
641    }
642
643    fn get_mut(&mut self, idx: usize) -> Option<&mut ConnectionEntry<R>> {
644        self.entries.get_mut(idx).and_then(|e| e.as_mut())
645    }
646
647    /// Remove and return a connection entry, recycling its index.
648    fn remove(&mut self, idx: usize) -> Option<ConnectionEntry<R>> {
649        if let Some(slot) = self.entries.get_mut(idx) {
650            let removed = slot.take();
651            if removed.is_some() {
652                self.free.push(idx);
653            }
654            removed
655        } else {
656            None
657        }
658    }
659}
660
661// ---------------------------------------------------------------------------
662// Main loop
663// ---------------------------------------------------------------------------
664
665/// How the reader should approach the kernel before draining the CQ.
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667enum RingEntry {
668    /// Completions are visible and there is nothing to hand over — drain
669    /// from the mmap'd ring without a syscall.
670    Skip,
671    /// SQEs to submit, but completions are already visible: hand them
672    /// over and come straight back rather than asking to wait.
673    Submit,
674    /// Nothing to process yet — submit whatever is queued and park.
675    SubmitAndWait,
676}
677
678/// The reader's enter policy. Two rules: never park while completions
679/// are already visible, and never skip the syscall while SQEs are
680/// waiting to be handed over.
681#[inline]
682fn ring_entry(sq_pending: usize, cq_ready: bool) -> RingEntry {
683    match (sq_pending > 0, cq_ready) {
684        (false, true) => RingEntry::Skip,
685        (true, true) => RingEntry::Submit,
686        (_, false) => RingEntry::SubmitAndWait,
687    }
688}
689
690/// Main io_uring reader loop. Runs until channel disconnection.
691///
692/// When `tick_cadence` is `Some`, the loop also generates the application's
693/// clock ticks — see [`spawn_reader`] for the rationale.
694#[allow(clippy::too_many_arguments)]
695fn reader_loop<A: Application, R: AsRawFd>(
696    command_rx: mpsc::Receiver<ReaderRegistration<R>>,
697    // Shared with `UringReaderHandle`. Taken by value so this thread keeps
698    // the descriptor alive for as long as it is armed in the ring, and
699    // releases it by dropping the `Arc` rather than by closing the fd.
700    wakeup_fd: Arc<OwnedFd>,
701    mut producer: ring::Producer<InputSlot<A::Event>>,
702    decoder: &dyn ErasedDecoder<A::Event>,
703    halt: &HaltGate,
704    mut early_replies: EarlyReplySender<A::Report>,
705    metrics: &IngressMetrics,
706    mut ack_policy: AckPolicyPublisher,
707    control_tx: &mpsc::Sender<ControlEvent>,
708    connection_timeout: Option<Duration>,
709    tick_cadence: Option<Duration>,
710    sizing: RingSizing,
711    // Startup report to `spawn_reader`: `Ok` once the ring and the buffer
712    // pool are in place, the error otherwise (and the loop never starts).
713    ready: mpsc::SyncSender<std::io::Result<()>>,
714    shutdown: &AtomicBool,
715) {
716    let num_buffers = sizing.provided_buffers();
717    let ring_entries = sizing.ring_entries();
718
719    // Kernel-referenced memory is declared BEFORE the io_uring so it
720    // drops AFTER the ring on every exit path, including panic unwind
721    // (locals drop in reverse declaration order). The kernel holds live
722    // references into all three for as long as the ring fd is open:
723    // armed multishot RECVs select entries from the buf_ring and write
724    // into the pool, and the armed eventfd READ writes its buffer.
725
726    // Eventfd read buffer — boxed for pointer stability across SQE lifetimes.
727    let mut eventfd_buf: Box<[u8; 8]> = Box::new([0u8; 8]);
728
729    // Shared buffer pool for provided buffers. Contiguous allocation of
730    // `num_buffers × BUF_SIZE` bytes. The kernel selects a buffer from
731    // this pool for each recv completion, identified by buffer ID in the
732    // CQE.
733    let mut buffer_pool = vec![0u8; num_buffers as usize * BUF_SIZE].into_boxed_slice();
734
735    // Ring-mapped provided-buffer ring: recycling a consumed buffer is a
736    // shared-memory store, not a ProvideBuffers SQE — see `buf_ring`.
737    // Allocated unconditionally (16 bytes per buffer, page-aligned) so
738    // its declaration precedes the io_uring's even when the legacy
739    // fallback below ends up in use.
740    let mut buf_ring = BufRing::new(num_buffers, buffer_pool.as_mut_ptr(), BUF_SIZE);
741
742    // SINGLE_ISSUER: this thread creates the ring and is the only one
743    // that ever submits — lets the kernel skip SQ locking, and turns any
744    // future cross-thread submission bug into an immediate EEXIST
745    // instead of a silent race. Same rationale as the journal and
746    // replication rings. (COOP_TASKRUN/DEFER_TASKRUN deliberately not
747    // set — see the journal ring's measured rationale in
748    // melin-transport-core::pipeline.)
749    //
750    // A failure here is reported to `spawn_reader` as a startup error
751    // rather than panicking the thread: the usual cause is the
752    // locked-memory limit, which the operator can raise.
753    let mut ring: IoUring = match IoUring::builder().setup_single_issuer().build(ring_entries) {
754        Ok(ring) => ring,
755        Err(e) => {
756            // Dropped send: a spawner that stopped waiting is unwinding,
757            // and the thread returns either way.
758            let _ = ready.send(Err(ring_setup_error("uring-reader", ring_entries, e)));
759            return;
760        }
761    };
762
763    // Prefer the buf_ring; fall back to legacy ProvideBuffers SQEs if
764    // the kernel rejects the registration. Not theoretical: PBUF_RING
765    // needs kernel ≥ 5.19, and some virtualized hosts filter newer
766    // io_uring register opcodes (EINVAL) while reporting a modern
767    // uname. The fallback costs one SQE + one CQE per received chunk —
768    // degraded but fully functional, hence warn (see log conventions).
769    let use_buf_ring = match buf_ring.register(&ring, BUF_GROUP_ID) {
770        Ok(()) => true,
771        Err(e) => {
772            tracing::warn!(
773                error = %e,
774                "buf_ring registration rejected — falling back to legacy \
775                 ProvideBuffers recycling (kernel < 5.19, or a hypervisor \
776                 filtering io_uring register opcodes)"
777            );
778            if let Err(e) = register_buffer_pool(&mut ring, buffer_pool.as_mut_ptr(), num_buffers) {
779                // Dropped send: as above.
780                let _ = ready.send(Err(std::io::Error::new(
781                    e.kind(),
782                    format!("uring-reader: cannot provide its receive buffers: {e}"),
783                )));
784                return;
785            }
786            false
787        }
788    };
789
790    // Started. A failed send means `spawn_reader` is no longer waiting —
791    // its thread is unwinding — so nothing would ever register a
792    // connection or stop this loop: return instead of running headless.
793    if ready.send(Ok(())).is_err() {
794        return;
795    }
796
797    let mut slab = ConnectionSlab::<R>::new();
798    // Reverse map for cleanup when a connection's fd needs removal.
799    // HashMap for O(1) lookup by fd. Sized for typical connection counts.
800    let mut fd_to_slab: HashMap<RawFd, usize> = HashMap::with_capacity(256);
801
802    // Pre-allocated CQE collection buffer. We must collect CQEs before
803    // processing because the CQ borrow must end before pushing new SQEs.
804    // Stores (user_data, result, flags) — flags needed for buffer ID and
805    // multishot continuation. Sized to the CQ depth (2× the SQ) so even
806    // a maximal drain never reallocates mid-loop.
807    let mut cqes: Vec<(u64, i32, u32)> = Vec::with_capacity(ring_entries as usize * 2);
808
809    // Submit the initial eventfd read so we wake on first connection.
810    // `eventfd_armed` mirrors whether that READ is pushed/armed — the
811    // kernel owes its CQE and writes `eventfd_buf` when it lands — and
812    // is maintained at every push/consume site so the teardown quiesce
813    // below can prove when the buffer is free of kernel references.
814    push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
815    let mut eventfd_armed = true;
816
817    // Stage histograms via the global registry. `publish` is the
818    // narrow ring-publish call cost (lightweight, gated on
819    // `latency-trace`); `ingest` is the full per-frame reader cost
820    // and feeds the bench's tick-to-trade decomposition (heavier,
821    // gated on `tick-to-trade`).
822    #[cfg(feature = "latency-trace")]
823    let mut publish_rec =
824        melin_transport_core::trace::register_stage("reader: publish (decode → disruptor publish)");
825    #[cfg(feature = "tick-to-trade")]
826    let mut ingest_rec =
827        melin_transport_core::trace::register_stage("reader: ingest (recv_ts → publish complete)");
828    // Paces the recorder flush at the tail of each loop iteration. This
829    // thread parks in `submit_and_wait` when there is no traffic, so
830    // without an explicit flush its samples never reach the registry.
831    #[cfg(feature = "latency-trace")]
832    let mut last_stats_flush = Instant::now();
833
834    // Coarse gate for timeout scanning — avoids scanning on every
835    // submit_and_wait return during high throughput.
836    let mut last_timeout_scan = Instant::now();
837    // Pre-allocated buffer for stale connection indices to avoid
838    // heap allocation inside the hot loop.
839    let mut stale: Vec<usize> = Vec::new();
840
841    // Parking state. `any_parked` is set whenever a connection parks and
842    // recomputed by each retry pass, so it may overstate (a parked
843    // connection removed since) but never understate: an overstatement
844    // costs one pass and one timer. `park_retry_ts` is the timer's length;
845    // like `tick_ts` it must outlive the submit that reads it, so it lives
846    // as long as the loop.
847    let mut any_parked = false;
848    let mut park_retry_armed = false;
849    let park_retry_ts = types::Timespec::new()
850        .sec(PARK_RETRY.as_secs())
851        .nsec(PARK_RETRY.subsec_nanos());
852    let mut long_park_report = LongParkReport::default();
853
854    // Tick generator state. `tick_schedule` holds the monotonic instant the
855    // next `JournalEvent::Tick` should fire and the floor that keeps the
856    // published wall-clock timestamps strictly monotonic (NTP can step the
857    // wall clock backwards) — shared with the DPDK poll thread, see
858    // `TickSchedule`. `tick_armed` tracks whether
859    // an `IORING_OP_TIMEOUT` SQE is currently pending; we keep at most one.
860    //
861    // `tick_ts` lives across loop iterations because the kernel reads its
862    // bytes via the SQE's addr field at submit time, not at push time. If
863    // we declared it inside the `if !tick_armed` arm-timeout block, the
864    // value would be dropped before the `submit_and_wait` below — the
865    // kernel would then read freed stack memory. (See `md-gateway` for the
866    // same pattern: it stores Timespec as a long-lived struct field.)
867    let tick_enabled = tick_cadence.is_some();
868    let cadence = tick_cadence.unwrap_or(Duration::ZERO);
869    let mut tick_schedule = TickSchedule::new(cadence, Instant::now());
870    let mut tick_armed = false;
871    // Arm the very first timeout here, before entering the loop. This both
872    // (a) makes the initial `tick_ts` value actually read by the kernel
873    // (silencing the unused-assignment lint, since rustc cannot see kernel
874    // pointer reads) and (b) ensures the first `submit_and_wait` returns at
875    // the cadence even if no client traffic ever arrives.
876    let mut tick_ts = types::Timespec::new()
877        .sec(cadence.as_secs())
878        .nsec(cadence.subsec_nanos());
879    if tick_enabled {
880        let sqe = opcode::Timeout::new(&tick_ts)
881            .build()
882            .user_data(TICK_TIMEOUT_TOKEN);
883        unsafe {
884            ring.submission()
885                .push(&sqe)
886                .expect("io_uring SQ full while arming initial tick timeout");
887        }
888        tick_armed = true;
889        tracing::info!(
890            cadence_ms = cadence.as_millis() as u64,
891            "tick generator integrated into reader thread"
892        );
893    }
894
895    loop {
896        if shutdown.load(Ordering::Relaxed) {
897            break;
898        }
899
900        // An `ACK-POLICY` request: journaled here, by the ring's one
901        // producer, before parking again. One load when none waits; the
902        // requester's eventfd write is what brought a parked loop here.
903        ack_policy.poll(&mut producer, halt);
904
905        // Tick generator: emit any due tick before sleeping again. Done before
906        // the timeout-arm so that a freshly-emitted tick re-arms a timeout for
907        // the *new* deadline.
908        if tick_enabled {
909            if tick_schedule.publish_if_due(Instant::now(), unix_epoch_nanos, &mut producer) {
910                // The previous timeout (if any) is now stale; let it fire and
911                // be ignored, then arm a new one below.
912                tick_armed = false;
913            }
914
915            if !tick_armed {
916                let remaining = tick_schedule
917                    .next_deadline()
918                    .saturating_duration_since(Instant::now());
919                // Update the loop-scoped Timespec in place. The kernel reads
920                // it via the SQE's addr pointer on submit_and_wait below, so
921                // the binding must outlive that call (it does — outer scope).
922                tick_ts = types::Timespec::new()
923                    .sec(remaining.as_secs())
924                    .nsec(remaining.subsec_nanos());
925                let sqe = opcode::Timeout::new(&tick_ts)
926                    .build()
927                    .user_data(TICK_TIMEOUT_TOKEN);
928                // SAFETY: `tick_ts` is loop-scoped and outlives the
929                // submit that reads it (see its declaration).
930                unsafe { push_sqe(&mut ring, &sqe, "the tick TIMEOUT") };
931                tick_armed = true;
932            }
933        }
934
935        // While a connection is parked, nothing but this timer is sure to
936        // bring the loop back round to retry it.
937        if any_parked && !park_retry_armed {
938            let sqe = opcode::Timeout::new(&park_retry_ts)
939                .build()
940                .user_data(PARK_RETRY_TOKEN);
941            // SAFETY: `park_retry_ts` lives as long as the loop, past the
942            // submit that reads it.
943            unsafe { push_sqe(&mut ring, &sqe, "the park-retry TIMEOUT") };
944            park_retry_armed = true;
945        }
946
947        // Enter the kernel only when there is a reason to.
948        //
949        // Under load the CQ usually already holds completions by the
950        // time we get here: in the ring's default task-work mode the
951        // kernel posts them while this thread is in userspace parsing.
952        // Reading that is pure userspace — the CQ is mmap'd and
953        // `completion()` loads the tail with `Acquire` — so when there
954        // is also nothing to hand over, the `io_uring_enter` is a
955        // ~200 ns mode switch that buys nothing. The replication sender
956        // measured the same skip at ~6 % of its thread's cycles
957        // (`tcp_sender.rs`); this loop makes one enter per drain.
958        //
959        // When there ARE SQEs to submit we still enter, but we do not
960        // ask the kernel to *wait* if completions are already visible.
961        // Only a genuinely empty CQ parks the thread.
962        //
963        // Note this is the half of the io_uring-audit item that pairs
964        // *against* `DEFER_TASKRUN`: deferred task work only runs on an
965        // enter with GETEVENTS, so under that flag the CQ would be empty
966        // here and the skip would simply never fire.
967        let sq_pending = ring.submission().len();
968        let cq_ready = !ring.completion().is_empty();
969        let entered = match ring_entry(sq_pending, cq_ready) {
970            RingEntry::Skip => Ok(0),
971            RingEntry::Submit => ring.submit(),
972            RingEntry::SubmitAndWait => ring.submit_and_wait(1),
973        };
974        match entered {
975            Ok(_) => {}
976            Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
977            Err(e) => {
978                error!(error = %e, "io_uring submit/wait error");
979                break;
980            }
981        }
982
983        // Drain all available CQEs into the pre-allocated buffer.
984        // Must collect before processing because the CQ borrow must end
985        // before we can push new SQEs to the SQ.
986        cqes.clear();
987        cqes.extend(
988            ring.completion()
989                .map(|cqe| (cqe.user_data(), cqe.result(), cqe.flags())),
990        );
991
992        let batch_now = Instant::now();
993        // One wall-clock read per CQE batch instead of per request: at
994        // peak request rates a per-request `unix_epoch_nanos()` (vDSO
995        // `clock_gettime(CLOCK_REALTIME)`) showed up in the primary's
996        // profile. All requests in the same batch share the timestamp —
997        // precision loss is bounded by the CQE-drain cadence. The timestamp
998        // drives the application's clock but orders nothing (the pipeline
999        // orders by sequence), so that resolution is enough.
1000        let batch_wall_ns = unix_epoch_nanos();
1001
1002        for &(token, result, flags) in &cqes {
1003            // ── Tick timeout ──
1004            // The CQE is just a wakeup signal — the actual tick emission
1005            // happens at the top of the next loop iteration via the
1006            // deadline check, so the time the tick is stamped with reflects
1007            // unix_epoch_nanos at fire time, not at submit time.
1008            if token == TICK_TIMEOUT_TOKEN {
1009                tick_armed = false;
1010                continue;
1011            }
1012
1013            // ── Park-retry timeout ──
1014            // A wakeup and nothing more: the retry pass after the drain
1015            // runs on every wake.
1016            if token == PARK_RETRY_TOKEN {
1017                park_retry_armed = false;
1018                continue;
1019            }
1020
1021            // ── Legacy ProvideBuffers completion (fallback mode only) ──
1022            if token == PROVIDE_BUFS_TOKEN {
1023                if result < 0 {
1024                    error!(error = result, "ProvideBuffers failed");
1025                }
1026                continue;
1027            }
1028
1029            // ── AsyncCancel completion ──
1030            if token == CANCEL_TOKEN {
1031                // Nothing to do: the cancelled op's own terminal CQE
1032                // drives the state machine; ENOENT/EALREADY just mean
1033                // it already finished on its own. A filtered cancel
1034                // (EINVAL) leaves a parked connection's receive armed;
1035                // the retry pass closes it (`PARK_CANCEL_DEADLINE`).
1036                continue;
1037            }
1038
1039            // ── Eventfd wakeup ──
1040            if token == EVENTFD_TOKEN {
1041                // This CQE is the armed READ completing; the re-arm at
1042                // the end of the branch replaces it before anything can
1043                // observe the gap, so `eventfd_armed` stays `true`
1044                // through the pair. The teardown drain clears it for
1045                // the one case where a completion is never processed
1046                // here (a CQE still queued when the loop breaks).
1047                if result >= 0 {
1048                    // Process all pending registrations.
1049                    while let Ok(reg) = command_rx.try_recv() {
1050                        let fd = reg.reader.as_raw_fd();
1051                        let entry = ConnectionEntry {
1052                            connection_id: reg.connection_id.0,
1053                            addr: reg.addr,
1054                            role: reg.role,
1055                            key_hash: reg.key_hash,
1056                            fd,
1057                            _reader: reg.reader,
1058                            ingress: ClientIngress::new(Vec::with_capacity(MAX_FRAME_SIZE + 4)),
1059                            // Armed just below.
1060                            recv: RecvState::Armed,
1061                            last_activity: Instant::now(),
1062                        };
1063                        let idx = slab.insert(entry);
1064                        fd_to_slab.insert(fd, idx);
1065
1066                        // Submit multishot RECV for this connection.
1067                        push_recv_multi(&mut ring, fd, idx);
1068                    }
1069                } else {
1070                    error!(error = result, "eventfd read error");
1071                }
1072
1073                // Re-submit eventfd read for the next wakeup
1074                // (`eventfd_armed` stays `true` — see above).
1075                push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
1076                continue;
1077            }
1078
1079            // ── Connection multishot RECV completion ──
1080
1081            let slab_idx = token as usize;
1082            // A cleared F_MORE means THIS armed op posts no further CQEs.
1083            let terminal = (flags & IORING_CQE_F_MORE) == 0;
1084
1085            // Extract the buffer (if any) before acting on `result`: a
1086            // buffer can ride ANY recv CQE, and one that is not recycled
1087            // is leaked from the pool forever. On 6.8 the kernel
1088            // recycles internally before posting error/EOF CQEs, but the
1089            // supported floor is 5.19, where that guarantee could not be
1090            // established — the defensive recycle is one branch.
1091            let buf_id = if (flags & IORING_CQE_F_BUFFER) != 0 {
1092                Some((flags >> IORING_CQE_BUFFER_SHIFT) as usize)
1093            } else {
1094                None
1095            };
1096
1097            let Some(entry) = slab.get_mut(slab_idx) else {
1098                // No connection holds the index: an index is freed only
1099                // once nothing armed can post for it, so this does not
1100                // happen; its buffer goes back all the same.
1101                if let Some(bid) = buf_id {
1102                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
1103                }
1104                continue;
1105            };
1106
1107            // `ENOBUFS`: the pool ran dry at buffer-selection time, and
1108            // the multishot ended WITHOUT reading — the bytes sit in the
1109            // socket buffer. Not a client error, and it must not
1110            // disconnect (a bare `result <= 0 ⇒ remove` here dropped
1111            // innocent clients under burst): the receive is re-armed, and
1112            // recycles from data CQEs earlier in this drain have already
1113            // refilled the buf_ring (publication is immediate), while the
1114            // re-arm SQE submits after the whole drain, so the retry finds
1115            // buffers. `ECANCELED`: a park's cancel landed. A data CQE
1116            // without a buffer cannot happen with buffer-select receives;
1117            // defensively, it is taken as no bytes.
1118            let completion = if result == -libc::ENOBUFS || result == -libc::ECANCELED {
1119                RecvCompletion::Empty
1120            } else if result <= 0 {
1121                RecvCompletion::Closed
1122            } else if buf_id.is_none() {
1123                debug!(slab_idx, "recv CQE without buffer flag");
1124                RecvCompletion::Empty
1125            } else {
1126                RecvCompletion::Data
1127            };
1128            let (recv, step) = entry.recv.on_cqe(completion, terminal);
1129            entry.recv = recv;
1130            let fd = entry.fd;
1131
1132            let (Some(buf_id), CqeStep::Process | CqeStep::Keep) = (buf_id, step) else {
1133                // No bytes to take: a buffer riding the CQE (one can ride
1134                // ANY recv CQE, as above) goes straight back.
1135                if let Some(bid) = buf_id {
1136                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
1137                }
1138                match step {
1139                    CqeStep::Discard { free } => {
1140                        // A torn-down connection: its bytes belong to a
1141                        // repudiated peer, and its index is freed only at
1142                        // the terminal CQE, so it cannot be handed to a
1143                        // new registration while the kernel can still post
1144                        // CQEs carrying it.
1145                        if free && let Some(dead) = slab.remove(slab_idx) {
1146                            debug!(
1147                                connection_id = dead.connection_id,
1148                                "teardown complete, slab index released"
1149                            );
1150                        }
1151                    }
1152                    CqeStep::Remove => {
1153                        // Disconnect (0) or error (negative errno) —
1154                        // terminal for the armed op, so the index is
1155                        // immediately safe to free.
1156                        if let Some(removed) = slab.remove(slab_idx) {
1157                            if result == 0 {
1158                                debug!(
1159                                    connection_id = removed.connection_id,
1160                                    addr = %removed.addr,
1161                                    "client disconnected"
1162                                );
1163                            } else {
1164                                debug!(
1165                                    connection_id = removed.connection_id,
1166                                    addr = %removed.addr,
1167                                    error = result,
1168                                    "recv error"
1169                                );
1170                            }
1171                            fd_to_slab.remove(&removed.fd);
1172                            // Dropped error: a dead control channel means
1173                            // the response stage is gone and the server is
1174                            // shutting down.
1175                            let _ = control_tx.send(ControlEvent::Disconnected {
1176                                connection_id: removed.connection_id,
1177                            });
1178                        }
1179                    }
1180                    CqeStep::Arm => push_recv_multi(&mut ring, fd, slab_idx),
1181                    // `Process` and `Keep` without a buffer are `Empty`'s,
1182                    // which never yields them.
1183                    CqeStep::Nothing | CqeStep::Process | CqeStep::Keep => {}
1184                }
1185                continue;
1186            };
1187
1188            let n = result as usize;
1189
1190            // Trace timestamp: the moment the kernel handed us this recv's
1191            // bytes. Captured once per CQE — not per frame — and stamped
1192            // onto every InputSlot parsed from this buffer below, so the
1193            // reader-ingest and server-e2e stages measure from true wire
1194            // receipt (frame decode included) rather than re-sampling after
1195            // each decode (which excluded decode and drifted forward for
1196            // later frames in a multi-frame recv).
1197            #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
1198            let recv_ts = melin_transport_core::trace::mono_trace_ns();
1199
1200            // Any successful recv resets the idle timeout.
1201            entry.last_activity = batch_now;
1202
1203            // Copy received data from the shared buffer pool into the
1204            // connection's parse buffer: kept even while it is parked (a
1205            // CQE that landed before the park's cancel), for the retry.
1206            let buf_start = buf_id * BUF_SIZE;
1207            entry
1208                .ingress
1209                .extend_from_slice(&buffer_pool[buf_start..buf_start + n]);
1210
1211            // Recycle the consumed buffer. Must happen after the copy-out
1212            // above — from this line on the kernel may write fresh recv
1213            // data into the slot.
1214            recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, buf_id);
1215
1216            if step == CqeStep::Keep {
1217                continue;
1218            }
1219
1220            // Extract and publish complete frames.
1221            let action = process_frames::<A, R>(
1222                entry,
1223                &mut producer,
1224                decoder,
1225                halt,
1226                &mut early_replies,
1227                metrics,
1228                batch_wall_ns,
1229                recv_ts,
1230                #[cfg(feature = "latency-trace")]
1231                &mut publish_rec,
1232                #[cfg(feature = "tick-to-trade")]
1233                &mut ingest_rec,
1234            );
1235            let (recv, step) = entry.recv.after_frames(action, batch_now);
1236            entry.recv = recv;
1237            // A new park: a parked connection's bytes are kept, not
1238            // processed (`CqeStep::Keep`), so none was parked before this.
1239            if matches!(recv, RecvState::Parked { .. }) {
1240                any_parked = true;
1241                metrics.record_park();
1242                debug!(connection_id = entry.connection_id, "connection parked");
1243            }
1244            apply_frames_step(
1245                &mut ring,
1246                &mut slab,
1247                &mut fd_to_slab,
1248                control_tx,
1249                slab_idx,
1250                step,
1251            );
1252        }
1253
1254        // Retry every parked connection's buffer: on the park-retry
1255        // timer, and on any other wake. One that comes back processed is
1256        // read again; one that parks again stays parked. The pass counts
1257        // those still parked for the gauge: it runs whenever one may be
1258        // (see `any_parked`), so a gauge left above zero is corrected by
1259        // the next pass, and one that finds none sets it to zero.
1260        if any_parked {
1261            any_parked = false;
1262            let mut still_parked = 0u64;
1263            let mut long_parked = 0usize;
1264            #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
1265            let recv_ts = melin_transport_core::trace::mono_trace_ns();
1266            for idx in 0..slab.entries.len() {
1267                let Some(entry) = slab.get_mut(idx) else {
1268                    continue;
1269                };
1270                let RecvState::Parked { since, .. } = entry.recv else {
1271                    continue;
1272                };
1273                if entry.recv.park_cancel_overdue(batch_now) {
1274                    tracing::warn!(
1275                        connection_id = entry.connection_id,
1276                        "a parked connection's receive cancel went unanswered (is io_uring \
1277                         AsyncCancel filtered on this host?): closing the connection"
1278                    );
1279                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
1280                    continue;
1281                }
1282                let action = process_frames::<A, R>(
1283                    entry,
1284                    &mut producer,
1285                    decoder,
1286                    halt,
1287                    &mut early_replies,
1288                    metrics,
1289                    batch_wall_ns,
1290                    recv_ts,
1291                    #[cfg(feature = "latency-trace")]
1292                    &mut publish_rec,
1293                    #[cfg(feature = "tick-to-trade")]
1294                    &mut ingest_rec,
1295                );
1296                let (recv, step) = entry.recv.after_frames(action, batch_now);
1297                entry.recv = recv;
1298                match action {
1299                    // Still parked, since it first parked.
1300                    FrameAction::Park => {
1301                        any_parked = true;
1302                        still_parked += 1;
1303                        if LongParkReport::parked_long(since, batch_now) {
1304                            long_parked += 1;
1305                        }
1306                    }
1307                    FrameAction::Continue => {
1308                        // Not read while parked: its idle clock restarts
1309                        // now, not at its last bytes.
1310                        entry.last_activity = batch_now;
1311                        debug!(connection_id = entry.connection_id, "connection resumed");
1312                    }
1313                    // Torn down below.
1314                    FrameAction::Disconnect => {}
1315                }
1316                apply_frames_step(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx, step);
1317            }
1318            metrics.set_parked(still_parked);
1319            long_park_report.report(long_parked, batch_now);
1320        }
1321
1322        // Scan for idle connections that have exceeded the timeout.
1323        // Coarse gate: only scan once per second to avoid unnecessary
1324        // iteration during high-throughput phases when submit_and_wait
1325        // returns immediately with CQEs.
1326        if let Some(timeout) = connection_timeout {
1327            let now = Instant::now();
1328            if now.duration_since(last_timeout_scan) >= Duration::from_secs(1) {
1329                last_timeout_scan = now;
1330                stale.clear();
1331                for (idx, slot) in slab.entries.iter().enumerate() {
1332                    // Dying entries are already mid-teardown: their
1333                    // `last_activity` stays stale by design, and a second
1334                    // teardown would emit a duplicate Disconnected. A
1335                    // parked connection is not idle: the node is not
1336                    // reading it.
1337                    if let Some(entry) = slot
1338                        && !matches!(entry.recv, RecvState::Dying | RecvState::Parked { .. })
1339                        && now.duration_since(entry.last_activity) > timeout
1340                    {
1341                        debug!(
1342                            connection_id = entry.connection_id,
1343                            addr = %entry.addr,
1344                            "connection timed out"
1345                        );
1346                        stale.push(idx);
1347                    }
1348                }
1349                for &idx in &stale {
1350                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
1351                }
1352            }
1353        }
1354
1355        // Hand buffered latency samples to the stats registry before
1356        // parking in `submit_and_wait` at the top of the next
1357        // iteration — once traffic stops this thread can sleep in the
1358        // kernel indefinitely, which is precisely when the bench
1359        // scrapes /stats-dump. Reuses the per-batch clock read.
1360        #[cfg(feature = "latency-trace")]
1361        if batch_now.duration_since(last_stats_flush)
1362            >= melin_transport_core::trace::IDLE_FLUSH_INTERVAL
1363        {
1364            last_stats_flush = batch_now;
1365            publish_rec.flush();
1366            #[cfg(feature = "tick-to-trade")]
1367            ingest_rec.flush();
1368        }
1369    }
1370
1371    // ── Teardown quiesce ────────────────────────────────────────────
1372    // Armed operations hold kernel pointers into the buffer pool, the
1373    // buf_ring memory, and the eventfd buffer. Closing the ring fd only
1374    // triggers ASYNCHRONOUS cancellation (the kernel's ring-exit work),
1375    // which can still be touching those allocations after this function
1376    // returns and frees them — the same corruption class fixed on both
1377    // replication transports (see `crate::uring_teardown`). Quiesce
1378    // *provably*: wake every armed operation, then reap CQEs until none
1379    // is owed, and leak the buffers if that cannot be shown within the
1380    // deadline. "CQ went quiet" is not proof — a wake that silently
1381    // failed leaves armed ops and a quiet CQ.
1382    //
1383    // The wake cannot rely on the cancel opcode alone: some hosts
1384    // filter io_uring opcodes (the reason the buf_ring path has a
1385    // legacy fallback), and a filtered cancel completes with EINVAL
1386    // while the armed operations live on. So every operation that has
1387    // to be proven down gets a wake it must answer: `shutdown(2)` on
1388    // each connection socket completes its multishot RECV with EOF,
1389    // and a wakeup write completes the armed eventfd READ. The cancel
1390    // is still pushed as an accelerator where the host honours it. The
1391    // tick timeout is not in the proof at all — see `owed` below.
1392    // Panic unwind skips all of this and accepts the (tiny,
1393    // process-is-dying) exit-work window; declaration order still
1394    // closes the ring fd before the frees.
1395    let proven = {
1396        let cancel_all = opcode::AsyncCancel2::new(types::CancelBuilder::any())
1397            .build()
1398            .user_data(CANCEL_TOKEN);
1399        unsafe {
1400            // Ignore a full SQ: the wakes below do not depend on it.
1401            let _ = ring.submission().push(&cancel_all);
1402        }
1403        for entry in slab.entries.iter().flatten() {
1404            crate::uring_teardown::wake_pending_ops(entry.fd);
1405        }
1406        // Completes the armed eventfd READ into `eventfd_buf` (still
1407        // alive). Best-effort: on failure the cancel is the fallback.
1408        let wake: u64 = 1;
1409        unsafe {
1410            libc::write(
1411                wakeup_fd.as_raw_fd(),
1412                &wake as *const u64 as *const libc::c_void,
1413                8,
1414            );
1415        }
1416        // Flush SQEs pushed but never submitted (re-arms from the last
1417        // batch, the cancel above): until they reach the kernel no CQE
1418        // is coming for them, but their armed-state flags are already
1419        // set, so abandoning them in the SQ would stall the proof
1420        // below. On a submit error the flags stay set and the deadline
1421        // routes to the leak path — the conservative outcome.
1422        loop {
1423            match ring.submit() {
1424                Ok(_) => break,
1425                Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1426                Err(_) => break,
1427            }
1428        }
1429        let mut backoff = crate::uring_teardown::DrainBackoff::new();
1430        loop {
1431            for cqe in ring.completion() {
1432                match cqe.user_data() {
1433                    EVENTFD_TOKEN => eventfd_armed = false,
1434                    // None of these touch the allocations freed below,
1435                    // so reaping them settles nothing: cancel results
1436                    // carry no information, legacy ProvideBuffers
1437                    // completions only registered addresses, and the
1438                    // tick timeout reads nothing after submit (see
1439                    // `owed`), nor does the park-retry timeout, for the
1440                    // same reason.
1441                    PROVIDE_BUFS_TOKEN | CANCEL_TOKEN | TICK_TIMEOUT_TOKEN | PARK_RETRY_TOKEN => {}
1442                    // Connection tokens are slab indices.
1443                    idx => {
1444                        // A terminal CQE retires that connection's arm.
1445                        // Data CQEs landing mid-teardown are discarded
1446                        // and their buffers deliberately not recycled —
1447                        // nothing re-arms, so the pool cannot be
1448                        // selected from again once every arm is down.
1449                        // Whatever state it was in, the kernel owes it
1450                        // nothing more: `Idle` says so (the entry is never
1451                        // used again).
1452                        if (cqe.flags() & IORING_CQE_F_MORE) == 0
1453                            && let Some(entry) = slab.get_mut(idx as usize)
1454                        {
1455                            entry.recv = RecvState::Idle;
1456                        }
1457                    }
1458                }
1459            }
1460            // `tick_armed` is deliberately absent: an armed
1461            // `IORING_OP_TIMEOUT` holds no pointer into any of the
1462            // allocations below. The kernel copies its `Timespec` out
1463            // of `tick_ts` when the SQE is *submitted* (which is why
1464            // that binding only has to outlive the submit — see its
1465            // declaration), and the ring's own exit work retires the
1466            // timer. Waiting for it would prove nothing and cost
1467            // plenty: nothing here can wake a timeout, so the only
1468            // things that retire one are it firing at its own cadence
1469            // — `--tick-interval-ms`, operator-set and unbounded — or
1470            // the cancel above, which is exactly what a host that
1471            // filters opcodes refuses. Gating the proof on it would
1472            // hand those hosts a full-deadline stall and a spurious
1473            // "leaking" warning on every clean shutdown.
1474            let owed = eventfd_armed
1475                || slab
1476                    .entries
1477                    .iter()
1478                    .flatten()
1479                    .any(|e| e.recv.kernel_owes_cqe());
1480            if !owed {
1481                break true;
1482            }
1483            if !backoff.wait() {
1484                break false;
1485            }
1486        }
1487    };
1488    if !proven {
1489        // The kernel may still write the pool or the eventfd buffer, or
1490        // read buf_ring entries. Leak all three rather than hand the
1491        // allocator memory the kernel still touches — a bounded one-off
1492        // cost on a thread that is exiting anyway. `mem::forget` on the
1493        // `BufRing` also skips its ring-memory dealloc, which is the
1494        // point.
1495        // Report what was still owed, not `tick_armed` — naming a
1496        // timer that was never part of the proof would point the next
1497        // reader at the wrong operation.
1498        tracing::warn!(
1499            eventfd_armed,
1500            armed_connections = slab
1501                .entries
1502                .iter()
1503                .flatten()
1504                .filter(|e| e.recv.kernel_owes_cqe())
1505                .count(),
1506            "io_uring teardown drain did not complete; leaking reader buffers"
1507        );
1508        std::mem::forget(buffer_pool);
1509        std::mem::forget(buf_ring);
1510        std::mem::forget(eventfd_buf);
1511    }
1512
1513    // No `close` here. This thread shares the eventfd with
1514    // `UringReaderHandle`, which writes wakeups to it, and closing it from
1515    // this side left that write racing a reused descriptor number. Dropping
1516    // the `Arc` at the end of this scope releases our share; the descriptor
1517    // itself closes when the handle drops too.
1518}
1519
1520// ---------------------------------------------------------------------------
1521// SQE helpers
1522// ---------------------------------------------------------------------------
1523
1524/// Queue an SQE, first handing the already-queued ones to the kernel if
1525/// the submission queue is full.
1526///
1527/// [`RingSizing`] sizes the SQ for a drain's worth of SQEs on the
1528/// buf_ring path — two per connection plus housekeeping — so there the
1529/// flush fires only when the reader transiently tracks more connections
1530/// than the cap: a dying connection stays in the reader until its cancel
1531/// completes, while the response stage has already released its slot and
1532/// the accept loop may have admitted a replacement. The legacy
1533/// ProvideBuffers fallback adds one SQE per
1534/// recycled buffer on top, which the sizing does not count; this is what
1535/// keeps that path from overrunning the queue. Submitting mid-drain is
1536/// harmless: the CQEs being processed were copied out before any SQE was
1537/// pushed, and the queued SQEs only reach the kernel sooner.
1538///
1539/// Panics only if the submit itself fails, which leaves queued SQEs that
1540/// point into live buffers with nowhere to go.
1541///
1542/// # Safety
1543///
1544/// As for [`io_uring::SubmissionQueue::push`]: every buffer the SQE
1545/// references must stay valid until its operation completes.
1546unsafe fn push_sqe(ring: &mut IoUring, sqe: &io_uring::squeue::Entry, what: &str) {
1547    // SAFETY: forwarded from the caller.
1548    if unsafe { ring.submission().push(sqe) }.is_ok() {
1549        return;
1550    }
1551    loop {
1552        match ring.submit() {
1553            Ok(_) => break,
1554            Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1555            Err(e) => panic!("io_uring submit failed while making room for {what}: {e}"),
1556        }
1557    }
1558    // SAFETY: forwarded from the caller.
1559    unsafe { ring.submission().push(sqe) }.unwrap_or_else(|_| {
1560        panic!("io_uring SQ still full for {what} after submitting every queued SQE")
1561    });
1562}
1563
1564/// Register the provided buffer pool via a legacy ProvideBuffers op —
1565/// the fallback when buf_ring registration is rejected. Submits
1566/// synchronously — called once at startup, before the reader reports
1567/// that it started, and only after the preferred path already failed.
1568fn register_buffer_pool(
1569    ring: &mut IoUring,
1570    pool_ptr: *mut u8,
1571    num_buffers: u16,
1572) -> std::io::Result<()> {
1573    let sqe = opcode::ProvideBuffers::new(pool_ptr, BUF_SIZE as i32, num_buffers, BUF_GROUP_ID, 0)
1574        .build()
1575        .user_data(PROVIDE_BUFS_TOKEN);
1576
1577    // SAFETY: the pool outlives the ring (declared before it in
1578    // `reader_loop`), and ProvideBuffers only records its address.
1579    unsafe {
1580        ring.submission().push(&sqe).map_err(|_| {
1581            std::io::Error::other("io_uring SQ full during buffer pool registration")
1582        })?;
1583    }
1584
1585    loop {
1586        match ring.submit_and_wait(1) {
1587            Ok(_) => break,
1588            Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1589            Err(e) => return Err(e),
1590        }
1591    }
1592
1593    // Check the completion result.
1594    let cqe = ring
1595        .completion()
1596        .next()
1597        .ok_or_else(|| std::io::Error::other("no CQE after ProvideBuffers"))?;
1598    if cqe.result() < 0 {
1599        return Err(std::io::Error::from_raw_os_error(-cqe.result()));
1600    }
1601    Ok(())
1602}
1603
1604/// Re-provide a single consumed buffer back to the pool (legacy fallback
1605/// mode). Pushed to SQ without immediate submission — batched with the
1606/// next submit_and_wait. A drain can recycle up to the whole pool on top
1607/// of the SQEs the ring is sized for, so this is the push that relies on
1608/// [`push_sqe`] making room.
1609fn re_provide_buffer(ring: &mut IoUring, pool_ptr: *mut u8, buf_id: usize) {
1610    let buf_ptr = unsafe { pool_ptr.add(buf_id * BUF_SIZE) };
1611    let sqe = opcode::ProvideBuffers::new(buf_ptr, BUF_SIZE as i32, 1, BUF_GROUP_ID, buf_id as u16)
1612        .build()
1613        .user_data(PROVIDE_BUFS_TOKEN);
1614
1615    // SAFETY: the buffer lies inside the pool, which outlives the ring.
1616    unsafe { push_sqe(ring, &sqe, "a legacy ProvideBuffers recycle") };
1617}
1618
1619/// Return a consumed buffer to the shared pool, in whichever recycle
1620/// mode registration selected.
1621///
1622/// Hard assert, not debug: the bid comes from a kernel CQE, this path
1623/// runs per CQE (off the per-frame budget), and an out-of-range bid
1624/// handed back to the kernel would point a future recv's DMA at
1625/// arbitrary heap — the one failure mode worth an unconditional branch.
1626fn recycle_buffer(
1627    ring: &mut IoUring,
1628    buf_ring: &mut BufRing,
1629    use_buf_ring: bool,
1630    buffer_pool: &[u8],
1631    buf_id: usize,
1632) {
1633    assert!(
1634        buf_id < buffer_pool.len() / BUF_SIZE,
1635        "kernel returned out-of-pool buffer id {buf_id}"
1636    );
1637    if use_buf_ring {
1638        buf_ring.push(buf_id as u16);
1639    } else {
1640        re_provide_buffer(ring, buffer_pool.as_ptr() as *mut u8, buf_id);
1641    }
1642}
1643
1644/// Begin tearing down a connection the reader decided to drop while its
1645/// multishot RECV may still be armed (malformed frame, idle timeout, a
1646/// superseded node), parked or not.
1647///
1648/// The slab index must NOT be freed yet: the kernel can still post CQEs
1649/// carrying it, and the slab's LIFO free list would hand the index to
1650/// the next registration — the old peer's bytes would then be parsed
1651/// under the new connection's identity, key hash, and permissions
1652/// (request injection; audit review F1). Instead: sever the peer,
1653/// mark the entry dying, cancel the armed op (unless a park's cancel is
1654/// already in flight), and let the terminal CQE free the index. When
1655/// nothing is owed there is nothing that can post — free immediately.
1656fn begin_teardown<R>(
1657    ring: &mut IoUring,
1658    slab: &mut ConnectionSlab<R>,
1659    fd_to_slab: &mut HashMap<RawFd, usize>,
1660    control_tx: &mpsc::Sender<ControlEvent>,
1661    idx: usize,
1662) {
1663    let Some(entry) = slab.get_mut(idx) else {
1664        return;
1665    };
1666    let (recv, step) = entry.recv.on_teardown();
1667    if step == TeardownStep::AlreadyDying {
1668        return;
1669    }
1670    entry.recv = recv;
1671    let connection_id = entry.connection_id;
1672    let fd = entry.fd;
1673    // Sever the peer now. The fd itself must stay open until the armed
1674    // op completes (the kernel holds a file reference for it anyway);
1675    // shutdown(2) stops both directions immediately. Best-effort:
1676    // ENOTCONN just means the peer already went away.
1677    unsafe {
1678        libc::shutdown(fd, libc::SHUT_RDWR);
1679    }
1680    fd_to_slab.remove(&fd);
1681    // Dropped error: a dead control channel means the response stage is
1682    // gone and the server is shutting down.
1683    let _ = control_tx.send(ControlEvent::Disconnected { connection_id });
1684
1685    match step {
1686        TeardownStep::Cancel => push_recv_cancel(ring, idx, "a connection teardown's AsyncCancel"),
1687        // The park's cancel is in flight: its terminal CQE frees the index.
1688        TeardownStep::AwaitCancel => {}
1689        // Nothing owed ⇒ no future CQEs can carry this index.
1690        TeardownStep::FreeNow => {
1691            slab.remove(idx);
1692        }
1693        // Returned above, before the peer was told anything twice.
1694        TeardownStep::AlreadyDying => {}
1695    }
1696}
1697
1698/// Act on what [`RecvState::after_frames`] decided for connection `idx`.
1699fn apply_frames_step<R>(
1700    ring: &mut IoUring,
1701    slab: &mut ConnectionSlab<R>,
1702    fd_to_slab: &mut HashMap<RawFd, usize>,
1703    control_tx: &mpsc::Sender<ControlEvent>,
1704    idx: usize,
1705    step: FramesStep,
1706) {
1707    match step {
1708        FramesStep::Nothing => {}
1709        FramesStep::Arm => {
1710            if let Some(entry) = slab.get_mut(idx) {
1711                push_recv_multi(ring, entry.fd, idx);
1712            }
1713        }
1714        // With nothing reading the socket its receive buffer fills, and
1715        // TCP's window closes on the client.
1716        FramesStep::Cancel => push_recv_cancel(ring, idx, "a park's AsyncCancel"),
1717        // Deferred teardown, NOT an immediate slab free — the multishot
1718        // may still be armed and its index must not be reused until the
1719        // terminal CQE (audit review F1).
1720        FramesStep::Teardown => begin_teardown(ring, slab, fd_to_slab, control_tx, idx),
1721    }
1722}
1723
1724/// Push a multishot RECV SQE for connection `idx` on `fd`. The kernel will
1725/// produce CQEs continuously until EOF, error, or buffer pool exhaustion —
1726/// no resubmission needed unless multishot terminates. Only on a
1727/// transition of [`RecvState`] to `Armed`.
1728fn push_recv_multi(ring: &mut IoUring, fd: RawFd, idx: usize) {
1729    let sqe = opcode::RecvMulti::new(types::Fd(fd), BUF_GROUP_ID)
1730        .build()
1731        .user_data(idx as u64);
1732
1733    // SAFETY: buffer-select RECV writes only into the provided-buffer
1734    // pool, which outlives the ring.
1735    unsafe { push_sqe(ring, &sqe, "a multishot RECV") };
1736}
1737
1738/// Cancel connection `idx`'s armed multishot RECV (`what` names why, for
1739/// a failed push). Its terminal CQE, `ECANCELED` or whatever completed
1740/// first, drives the state machine; the cancel's own completion carries
1741/// nothing.
1742fn push_recv_cancel(ring: &mut IoUring, idx: usize, what: &str) {
1743    let sqe = opcode::AsyncCancel::new(idx as u64)
1744        .build()
1745        .user_data(CANCEL_TOKEN);
1746    // SAFETY: AsyncCancel references no memory.
1747    unsafe { push_sqe(ring, &sqe, what) };
1748}
1749
1750/// Push a READ SQE for the eventfd (wakeup notification).
1751fn push_eventfd_read(ring: &mut IoUring, wakeup_fd: RawFd, buf: *mut u8) {
1752    let sqe = opcode::Read::new(types::Fd(wakeup_fd), buf, 8)
1753        .build()
1754        .user_data(EVENTFD_TOKEN);
1755
1756    // SAFETY: `buf` is the boxed eventfd buffer, declared before the
1757    // ring in `reader_loop` so it outlives it.
1758    unsafe { push_sqe(ring, &sqe, "the eventfd READ") };
1759}
1760
1761// ---------------------------------------------------------------------------
1762// Frame parsing
1763// ---------------------------------------------------------------------------
1764
1765/// Extract complete frames from `conn.ingress` and publish them as
1766/// `InputSlot`s, answering through `early_replies` the requests not
1767/// published (see [`process_client_frames`](crate::client_frames::process_client_frames)).
1768/// `batch_wall_ns` is the wall-clock timestamp captured once per CQE batch
1769/// by the caller (see `reader_loop`); all non-query requests published in
1770/// this call share it, sparing the reader a per-request
1771/// `clock_gettime(CLOCK_REALTIME)` on the hot path. Returns what to do
1772/// with the connection: keep reading, close it, or park it.
1773#[allow(clippy::too_many_arguments)]
1774fn process_frames<A: Application, R>(
1775    conn: &mut ConnectionEntry<R>,
1776    producer: &mut ring::Producer<InputSlot<A::Event>>,
1777    decoder: &dyn ErasedDecoder<A::Event>,
1778    halt: &HaltGate,
1779    early_replies: &mut EarlyReplySender<A::Report>,
1780    metrics: &IngressMetrics,
1781    batch_wall_ns: u64,
1782    recv_ts: melin_transport_core::trace::MonoTraceInstant,
1783    #[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
1784    #[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
1785) -> FrameAction {
1786    // Every answer the reader decides on, a busy for a full ring among
1787    // them, goes to the response stage through the early-reply queue: the
1788    // response stage owns all egress on a client socket, so no reader-side
1789    // send can land between the two halves of a partially flushed reply.
1790    crate::client_frames::process_client_frames::<A>(
1791        &mut conn.ingress,
1792        conn.connection_id,
1793        conn.key_hash,
1794        conn.role,
1795        producer,
1796        decoder,
1797        halt,
1798        early_replies,
1799        metrics,
1800        batch_wall_ns,
1801        recv_ts,
1802        #[cfg(feature = "latency-trace")]
1803        publish_rec,
1804        #[cfg(feature = "tick-to-trade")]
1805        ingest_rec,
1806    )
1807}
1808
1809#[cfg(test)]
1810mod tests {
1811    //! Unit tests for [`process_frames`]. The function has several exit
1812    //! paths (normal end, partial parse-buf, pipeline-full, oversize or
1813    //! non-request frame, full early-reply queue), each with subtle
1814    //! batch-commit ordering requirements. These tests pin that behaviour
1815    //! against a synthetic decoder so refactors of the batch path (e.g.
1816    //! moving the batch up to span the whole CQE drain) can't silently
1817    //! regress the "earlier frames must be visible before ServerBusy /
1818    //! disconnect" guarantees.
1819    use super::*;
1820    use crate::client_frames::MAX_REQUEST_BODY;
1821    use crate::early_reply::{EarlyKind, EarlyReply};
1822    use melin_app::auth::NoRoles;
1823    use melin_app::decoder::{Decoded, RequestDecoder};
1824    use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1825    use melin_journal::JournalEvent;
1826    use melin_pipeline::ring::DisruptorBuilder;
1827    use melin_transport_core::health::IngressTally;
1828    use melin_transport_core::pipeline::RequestId;
1829    use melin_wire_protocol::control::RefusalKind;
1830    use melin_wire_protocol::control_codec::{
1831        TAG_APP, TAG_CHALLENGE_RESPONSE, TAG_LEN, TAG_RESPONSE_HEARTBEAT,
1832    };
1833    use std::io::{ErrorKind, Read};
1834    use std::os::unix::net::UnixStream;
1835    use std::sync::atomic::AtomicU64;
1836    use std::time::Duration;
1837
1838    /// The reader's enter policy. Each case is a syscall-per-drain
1839    /// decision on the hot path, and getting one wrong is either a lost
1840    /// wakeup (skipping with an empty CQ) or unsubmitted SQEs (skipping
1841    /// with a non-empty SQ) — both silent.
1842    mod ring_entry {
1843        use super::super::{RingEntry, ring_entry};
1844
1845        #[test]
1846        fn visible_completions_and_nothing_to_submit_need_no_syscall() {
1847            assert_eq!(ring_entry(0, true), RingEntry::Skip);
1848        }
1849
1850        #[test]
1851        fn queued_sqes_are_always_handed_over() {
1852            // Never skip with work in the SQ: those entries would sit
1853            // there until some later iteration happened to enter.
1854            assert_eq!(ring_entry(1, true), RingEntry::Submit);
1855            assert_eq!(ring_entry(8, false), RingEntry::SubmitAndWait);
1856        }
1857
1858        #[test]
1859        fn an_empty_cq_is_the_only_thing_that_parks_the_thread() {
1860            assert_eq!(ring_entry(0, false), RingEntry::SubmitAndWait);
1861            // ...and visible work never does, whatever the SQ holds.
1862            for sq in [0, 1, 64] {
1863                assert_ne!(ring_entry(sq, true), RingEntry::SubmitAndWait);
1864            }
1865        }
1866    }
1867
1868    /// Minimal `AppEvent` for these tests. `Copy` is required by `AppEvent`;
1869    /// the on-wire codec is unused because [`ByteDecoder`] never invokes it
1870    /// (frames are interpreted directly from their one body byte).
1871    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1872    enum TestEvent {
1873        Cmd(u8),
1874        Query,
1875    }
1876
1877    impl AppEvent for TestEvent {
1878        const MAX_ENCODED_SIZE: usize = 2;
1879
1880        fn encoded_size(&self) -> usize {
1881            // Unused — the tests never round-trip through encode/decode.
1882            2
1883        }
1884        fn encode(&self, _buf: &mut [u8]) -> usize {
1885            unreachable!("process_frames does not encode app events")
1886        }
1887        fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
1888            unreachable!("process_frames does not decode app events directly")
1889        }
1890        fn is_query(&self) -> bool {
1891            matches!(self, TestEvent::Query)
1892        }
1893    }
1894
1895    /// Placeholder `Application` impl. `process_frames` calls only
1896    /// `build_reject`, for a write refused while halted; the rest
1897    /// `unreachable!`.
1898    #[derive(Default)]
1899    struct TestApp;
1900
1901    /// What [`TestApp::build_reject`] returns: the refused event and why,
1902    /// so a test can tell refusals apart.
1903    type TestReport = (TestEvent, RejectReason);
1904
1905    impl Application for TestApp {
1906        type Event = TestEvent;
1907        type Report = TestReport;
1908        type QueryResponse = ();
1909        type Sizing = ();
1910        const APP_VERSION: u16 = 0;
1911        fn apply(&mut self, _event: TestEvent, _ctx: &ApplyCtx, _out: &mut Vec<TestReport>) {
1912            unreachable!()
1913        }
1914        fn query(&self, _event: TestEvent, _ctx: &QueryCtx) -> Option<()> {
1915            unreachable!()
1916        }
1917        fn tick(&mut self, _now_ns: u64, _out: &mut Vec<TestReport>) {
1918            unreachable!()
1919        }
1920        fn build_reject(event: &TestEvent, reason: RejectReason) -> TestReport {
1921            (*event, reason)
1922        }
1923        fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1924            unreachable!()
1925        }
1926        fn restore<R: Read>(_r: &mut R) -> std::io::Result<Self> {
1927            unreachable!()
1928        }
1929    }
1930
1931    /// Stateless decoder that maps a request's single body byte to a
1932    /// [`Decoded`] outcome. Lets each test feed a precise mix of permitted,
1933    /// filtered, denied, and decode-error frames without standing up the
1934    /// real wire codec.
1935    ///
1936    /// Body-byte mapping (`0x00..=0xFB` map 1:1 to a Permitted command,
1937    /// reserving the top four byte values for the non-Permitted outcomes):
1938    ///   * `0xFC` -> `Filter`
1939    ///   * `0xFD` -> `PermissionDenied`
1940    ///   * `0xFE` -> `DecodeError`
1941    ///   * `0xFF` -> `Permitted` with `is_query == true`
1942    ///   * `0x00..=0xFB` -> `Permitted(Cmd(byte))`
1943    struct ByteDecoder;
1944
1945    impl RequestDecoder for ByteDecoder {
1946        type Event = TestEvent;
1947        // The reader never looks at a role: operator keys are enough.
1948        type Role = NoRoles;
1949        fn decode(&self, body: &[u8], _role: ClientRole<NoRoles>) -> Decoded<TestEvent> {
1950            match body.first().copied() {
1951                None => Decoded::DecodeError("empty body"),
1952                Some(0xFC) => Decoded::Filter,
1953                Some(0xFD) => Decoded::PermissionDenied("denied"),
1954                Some(0xFE) => Decoded::DecodeError("bad"),
1955                Some(0xFF) => Decoded::Permitted(TestEvent::Query),
1956                Some(b) => Decoded::Permitted(TestEvent::Cmd(b)),
1957            }
1958        }
1959    }
1960
1961    /// A frame under any tag — `[u32 LE length][tag][rest]` — as no
1962    /// well-behaved client sends after the handshake unless `tag` is the
1963    /// request's and `rest` starts with an ID.
1964    fn tagged_frame(tag: u8, rest: &[u8]) -> Vec<u8> {
1965        let len = (TAG_LEN + rest.len()) as u32;
1966        let mut f = Vec::with_capacity(4 + len as usize);
1967        f.extend_from_slice(&len.to_le_bytes());
1968        f.push(tag);
1969        f.extend_from_slice(rest);
1970        f
1971    }
1972
1973    /// Request `id`'s frame as a client sends it —
1974    /// `[u32 LE length][TAG_REQUEST][id][body]`.
1975    fn request_frame(id: u64, body: &[u8]) -> Vec<u8> {
1976        let mut buf = vec![0u8; melin_wire_protocol::framing::REQUEST_HEADER_LEN + body.len()];
1977        let n = melin_wire_protocol::framing::frame_request(&mut buf, RequestId(id), |out| {
1978            out[..body.len()].copy_from_slice(body);
1979            Ok::<_, melin_wire_protocol::framing::RequestFrameError>(body.len())
1980        })
1981        .expect("a test request fits");
1982        buf.truncate(n);
1983        buf
1984    }
1985
1986    /// The ID [`frame`] gives a request carrying `byte`: distinct per byte,
1987    /// and never the byte itself, so a test cannot pass by confusing the
1988    /// two.
1989    fn id_of(byte: u8) -> RequestId {
1990        RequestId(0x1_0000 + u64::from(byte))
1991    }
1992
1993    /// The frame most tests use: a request whose ID is [`id_of`] `byte`,
1994    /// and `byte` as the one body byte [`ByteDecoder`] keys on.
1995    fn frame(byte: u8) -> Vec<u8> {
1996        request_frame(id_of(byte).0, &[byte])
1997    }
1998
1999    /// Length prefix announcing an oversize frame. No payload bytes follow —
2000    /// `process_frames` decides on the prefix alone, before waiting for the
2001    /// body.
2002    fn oversize_prefix() -> [u8; 4] {
2003        ((MAX_FRAME_SIZE as u32) + 1).to_le_bytes()
2004    }
2005
2006    /// Test fixture bundle. Grouped into a struct rather than returned as a
2007    /// 4-tuple to keep clippy happy (`type_complexity`) and to give each
2008    /// field a name at call sites.
2009    struct Fixture {
2010        conn: ConnectionEntry<UnixStream>,
2011        producer: ring::Producer<InputSlot<TestEvent>>,
2012        consumer: ring::Consumer<InputSlot<TestEvent>>,
2013        /// Client-side end of the socket pair — read from this to inspect
2014        /// any `ServerBusy` bytes the function under test writes.
2015        peer: UnixStream,
2016    }
2017
2018    /// Build a fresh fixture: a `ConnectionEntry` backed by a `UnixStream`
2019    /// pair plus a single-consumer disruptor of the requested capacity.
2020    fn make_fixture(ring_capacity: usize) -> Fixture {
2021        let (server_side, peer) = UnixStream::pair().expect("UnixStream::pair");
2022        // Short read timeout on the peer so assertions of "no ServerBusy
2023        // written" return promptly instead of hanging the test.
2024        peer.set_read_timeout(Some(Duration::from_millis(50)))
2025            .expect("set_read_timeout");
2026
2027        let entry = ConnectionEntry::<UnixStream> {
2028            connection_id: 7,
2029            addr: "127.0.0.1:1".parse().expect("addr parses"),
2030            role: ClientRole::Operator,
2031            key_hash: 0xC0FFEE_u64,
2032            fd: server_side.as_raw_fd(),
2033            _reader: server_side,
2034            ingress: ClientIngress::new(Vec::with_capacity(64)),
2035            recv: RecvState::Idle,
2036            last_activity: Instant::now(),
2037        };
2038
2039        let (producer, mut consumers) =
2040            DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
2041                .add_consumer()
2042                .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2043        let consumer = consumers.pop().expect("consumer present");
2044
2045        Fixture {
2046            conn: entry,
2047            producer,
2048            consumer,
2049            peer,
2050        }
2051    }
2052
2053    /// Invoke `process_frames::<TestApp, UnixStream>` on a standalone gate,
2054    /// threading the feature-gated histogram args when the relevant
2055    /// features are on so the call compiles in every `cargo test`
2056    /// configuration.
2057    ///
2058    /// Returns the call's outcome plus every early reply the call queued:
2059    /// a busy goes to the response stage through the early-reply queue,
2060    /// never as reader-side socket bytes, so busy assertions read the
2061    /// queue, not the peer.
2062    fn run_process_frames(
2063        conn: &mut ConnectionEntry<UnixStream>,
2064        producer: &mut ring::Producer<InputSlot<TestEvent>>,
2065    ) -> (FrameAction, Vec<EarlyReply<TestReport>>) {
2066        let (mut early, mut queue) = early_channel();
2067        let action = run_process_frames_with(conn, producer, &gate(None, false), &mut early);
2068        (action, released(&mut queue))
2069    }
2070
2071    /// Every early reply visible on `queue`, oldest first: what the
2072    /// response stage would send, with no flush on the reader's side
2073    /// after the call that queued them.
2074    fn released(
2075        queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
2076    ) -> Vec<EarlyReply<TestReport>> {
2077        let boundary = crate::batch_boundary::BatchBoundary::new();
2078        let at = boundary.at_boundary().expect("between events");
2079        let mut out = Vec::new();
2080        queue.register(|| {}).drain(&at, |reply| out.push(reply));
2081        out
2082    }
2083
2084    /// The early reply the fixture connection (id 7) gets for request
2085    /// `id`.
2086    fn early_reply(id: RequestId, kind: EarlyKind<TestReport>) -> EarlyReply<TestReport> {
2087        EarlyReply {
2088            connection_id: 7,
2089            request_id: id,
2090            kind,
2091        }
2092    }
2093
2094    /// A halt gate: `replicas` connected (`None` for standalone), fenced
2095    /// or not.
2096    fn gate(replicas: Option<u32>, fenced: bool) -> HaltGate {
2097        gate_counting_into(replicas, fenced, Arc::new(AtomicU64::new(0)))
2098    }
2099
2100    /// [`gate`], counting what it refuses into `refused`.
2101    fn gate_counting_into(
2102        replicas: Option<u32>,
2103        fenced: bool,
2104        refused: Arc<AtomicU64>,
2105    ) -> HaltGate {
2106        let fence = Arc::new(melin_transport_core::fence::FenceState::new(0));
2107        if fenced {
2108            fence.fence();
2109        }
2110        HaltGate::new(
2111            replicas.map(crate::halt::test_support::presence),
2112            fence,
2113            refused,
2114        )
2115    }
2116
2117    fn refused_count(refused: &AtomicU64) -> u64 {
2118        refused.load(std::sync::atomic::Ordering::Relaxed)
2119    }
2120
2121    /// The ack-policy mailbox's producer side, for a reader no test sends
2122    /// a request to.
2123    fn policy_claim() -> crate::policy_control::BootedClaim {
2124        crate::policy_control::test_support::booted(
2125            &crate::policy_control::test_support::replicated(crate::ack_policy::AckPolicy::Disk),
2126        )
2127    }
2128
2129    /// A reader parked with nothing to do (no connection, no tick) is
2130    /// woken by an `ACK-POLICY` request and journals it. Halt-plan step 0
2131    /// through the reader: on a halted primary the operator's `disk` is
2132    /// journaled forced, and the halt is lifted before the request returns
2133    /// published, so the reader takes the next write. Once stopped, the
2134    /// reader is detached.
2135    #[test]
2136    fn a_parked_reader_journals_an_ack_policy_request() {
2137        use crate::ack_policy::AckPolicy;
2138        use crate::halt::Verdict;
2139        use crate::halt::test_support::{gate_over, presence};
2140        use crate::policy_control::RequestOutcome;
2141
2142        let replicas = presence(0);
2143        let halt = || gate_over(Some(Arc::clone(&replicas)));
2144        assert_eq!(
2145            halt().verdict(),
2146            Verdict::Refuse(RejectReason::ReplicaDisconnected)
2147        );
2148        let (handle, mut consumer, control) = spawn_parked_reader(halt(), None);
2149        assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
2150        let (_, slot) = consumer.try_consume().expect("the entry is on the ring");
2151        assert_eq!(
2152            slot.event,
2153            JournalEvent::AckPolicyChange(melin_journal::AckPolicyChange {
2154                policy: AckPolicy::Disk.to_byte(),
2155                unwitnessed: true,
2156            })
2157        );
2158        assert_eq!(halt().verdict(), Verdict::Take);
2159        handle.shutdown();
2160        handle.join();
2161        assert_eq!(
2162            control.request(AckPolicy::Ram),
2163            RequestOutcome::Staged,
2164            "a stopped reader is detached"
2165        );
2166    }
2167
2168    /// A policy staged while the primary started is journaled as the
2169    /// reader attaches, before it runs: ahead of a runtime request it
2170    /// then publishes, and never forced.
2171    #[test]
2172    fn a_reader_journals_a_policy_staged_during_bring_up_first() {
2173        use crate::ack_policy::AckPolicy;
2174        use crate::halt::test_support::{gate_over, presence};
2175        use crate::policy_control::RequestOutcome;
2176
2177        let (handle, mut consumer, control) =
2178            spawn_parked_reader(gate_over(Some(presence(0))), Some(AckPolicy::Disk));
2179        assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
2180        handle.shutdown();
2181        handle.join();
2182        let mut changes = Vec::new();
2183        while let Some((_, slot)) = consumer.try_consume() {
2184            if let JournalEvent::AckPolicyChange(change) = slot.event {
2185                changes.push(change.unwitnessed);
2186            }
2187        }
2188        assert_eq!(
2189            changes,
2190            [false, true],
2191            "staged first, then the forced runtime one"
2192        );
2193    }
2194
2195    /// A reader over `halt` with no connection and no tick, parked (in all
2196    /// likelihood: the callers must not depend on it either way), whose
2197    /// mailbox starts at `disk+ram` and had `staged` requested between its
2198    /// claim and the reader's start (a primary still starting); and the
2199    /// consumer of its input ring.
2200    fn spawn_parked_reader(
2201        halt: HaltGate,
2202        staged: Option<crate::ack_policy::AckPolicy>,
2203    ) -> (
2204        UringReaderHandle<UnixStream>,
2205        ring::Consumer<InputSlot<TestEvent>>,
2206        Arc<crate::policy_control::AckPolicyControl>,
2207    ) {
2208        use crate::ack_policy::AckPolicy;
2209        use crate::policy_control::RequestOutcome;
2210        use crate::policy_control::test_support::{control_with_publish_wait, first_change};
2211
2212        let (mut producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
2213            .add_consumer()
2214            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2215        let consumer = consumers.pop().expect("consumer");
2216        // Dropped: the reader's sends to it are best-effort.
2217        let (control_tx, _) = mpsc::channel();
2218        // Long enough that a reader that is never woken fails the request.
2219        let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_secs(30));
2220        // Booted on a lineage already under the claim's policy: no entry.
2221        let claim =
2222            control
2223                .claim()
2224                .journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt);
2225        if let Some(policy) = staged {
2226            assert_eq!(control.request(policy), RequestOutcome::Staged);
2227        }
2228        let handle = spawn_reader::<TestApp, UnixStream>(
2229            producer,
2230            Arc::new(ByteDecoder),
2231            halt,
2232            early_channel().0,
2233            Arc::new(IngressMetrics::default()),
2234            claim,
2235            control_tx,
2236            0,
2237            None,
2238            None, // no tick: nothing but the wakeup brings the loop round
2239            RingSizing::for_max_connections(4).expect("a supported cap"),
2240            Arc::new(AtomicBool::new(false)),
2241        )
2242        .expect("the reader starts");
2243        std::thread::sleep(Duration::from_millis(50));
2244        (handle, consumer, control)
2245    }
2246
2247    /// An early-reply queue, at a node's capacity.
2248    fn early_channel() -> (
2249        EarlyReplySender<TestReport>,
2250        crate::early_reply::EarlyReplyQueue<TestReport>,
2251    ) {
2252        early_channel_of(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY)
2253    }
2254
2255    /// An early-reply queue that fills at `capacity` (a power of two).
2256    fn early_channel_of(
2257        capacity: usize,
2258    ) -> (
2259        EarlyReplySender<TestReport>,
2260        crate::early_reply::EarlyReplyQueue<TestReport>,
2261    ) {
2262        crate::early_reply::early_reply_channel(capacity)
2263    }
2264
2265    /// [`run_process_frames`] against a given halt gate and early-reply
2266    /// queue.
2267    fn run_process_frames_with(
2268        conn: &mut ConnectionEntry<UnixStream>,
2269        producer: &mut ring::Producer<InputSlot<TestEvent>>,
2270        halt: &HaltGate,
2271        early: &mut EarlyReplySender<TestReport>,
2272    ) -> FrameAction {
2273        run_process_frames_counting(conn, producer, halt, early, &IngressMetrics::default())
2274    }
2275
2276    /// [`run_process_frames_with`], counting what it answers early into
2277    /// `metrics`.
2278    fn run_process_frames_counting(
2279        conn: &mut ConnectionEntry<UnixStream>,
2280        producer: &mut ring::Producer<InputSlot<TestEvent>>,
2281        halt: &HaltGate,
2282        early: &mut EarlyReplySender<TestReport>,
2283        metrics: &IngressMetrics,
2284    ) -> FrameAction {
2285        #[cfg(feature = "latency-trace")]
2286        let mut publish_rec = melin_transport_core::trace::register_stage("test: publish");
2287        #[cfg(feature = "tick-to-trade")]
2288        let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest");
2289
2290        #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
2291        let recv_ts = melin_transport_core::trace::mono_trace_ns();
2292
2293        process_frames::<TestApp, UnixStream>(
2294            conn,
2295            producer,
2296            &ByteDecoder,
2297            halt,
2298            early,
2299            metrics,
2300            0xDEAD_BEEF,
2301            recv_ts,
2302            #[cfg(feature = "latency-trace")]
2303            &mut publish_rec,
2304            #[cfg(feature = "tick-to-trade")]
2305            &mut ingest_rec,
2306        )
2307    }
2308
2309    /// The counts `answers` should leave on the ingress metrics: a busy
2310    /// is shed, a filtered request filtered, a refusal refused as its
2311    /// kind. A halt refusal is not among them: the halt gate counts it.
2312    fn ingress_counts_of(answers: &[EarlyReply<TestReport>]) -> IngressTally {
2313        let mut tally = IngressTally::default();
2314        for reply in answers {
2315            match reply.kind {
2316                EarlyKind::Busy => tally.shed += 1,
2317                EarlyKind::Filtered => tally.filtered += 1,
2318                EarlyKind::Refused { kind, .. } => tally.refused(kind),
2319                EarlyKind::Rejected(_) => {}
2320            }
2321        }
2322        tally
2323    }
2324
2325    /// Drain `consumer` into a Vec of `(seq, slot)` until it yields `None`.
2326    /// Used to assert exact event sequences after `process_frames` returns.
2327    fn drain(
2328        consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
2329    ) -> Vec<(u64, InputSlot<TestEvent>)> {
2330        let mut out = Vec::new();
2331        while let Some(pair) = consumer.try_consume() {
2332            out.push(pair);
2333        }
2334        out
2335    }
2336
2337    /// Whether the peer can read anything within its configured timeout:
2338    /// the reader never writes the client socket (egress belongs to the
2339    /// response stage), so it must not.
2340    fn peer_got_bytes(peer: &mut UnixStream) -> bool {
2341        let mut buf = [0u8; 1];
2342        match peer.read(&mut buf) {
2343            Ok(n) => n > 0,
2344            Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => false,
2345            Err(e) => panic!("unexpected peer read error: {e}"),
2346        }
2347    }
2348
2349    #[test]
2350    fn process_frames_publishes_all_frames_after_single_commit() {
2351        // Capacity > number of frames — every frame must succeed and become
2352        // visible to the consumer after the trailing `batch.commit()`.
2353        let Fixture {
2354            mut conn,
2355            mut producer,
2356            mut consumer,
2357            mut peer,
2358        } = make_fixture(16);
2359        for byte in [0x01, 0x02, 0x03, 0x04, 0x05] {
2360            conn.ingress.extend_from_slice(&frame(byte));
2361        }
2362
2363        let (action, early) = run_process_frames(&mut conn, &mut producer);
2364        assert_eq!(
2365            action,
2366            FrameAction::Continue,
2367            "no oversize frame ⇒ no disconnect"
2368        );
2369
2370        let events = drain(&mut consumer);
2371        assert_eq!(events.len(), 5, "all 5 frames must be visible");
2372        for (i, (seq, slot)) in events.iter().enumerate() {
2373            assert_eq!(*seq, i as u64, "seq monotonic from 0");
2374            assert_eq!(slot.connection_id, 7);
2375            assert_eq!(slot.key_hash, 0xC0FFEE_u64);
2376            let byte = (i + 1) as u8;
2377            // The client's ID travels in the slot.
2378            assert_eq!(slot.request_id, id_of(byte));
2379            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2380            // Non-query event ⇒ inherits the caller-supplied wall-clock.
2381            assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
2382        }
2383        // Parse buffer fully consumed.
2384        assert!(conn.ingress.bytes().is_empty());
2385        // Every request published: nothing answered early, and the
2386        // reader never writes the client socket (egress belongs to the
2387        // response stage).
2388        assert!(early.is_empty(), "{early:?}");
2389        assert!(
2390            !peer_got_bytes(&mut peer),
2391            "reader must never write the client socket"
2392        );
2393    }
2394
2395    /// A ring slot is reused once its consumer has passed it, and a request
2396    /// published into it is built whole: nothing of the previous occupant
2397    /// survives, a request ID and a journal sequence included (a stale
2398    /// sequence would be adopted by the journal stage as a replica's).
2399    #[test]
2400    fn process_frames_rebuilds_every_field_of_a_reused_slot() {
2401        const CAPACITY: usize = 4;
2402        let Fixture {
2403            mut conn,
2404            mut producer,
2405            mut consumer,
2406            ..
2407        } = make_fixture(CAPACITY);
2408        let stale = InputSlot {
2409            connection_id: 99,
2410            request_id: RequestId(0xBAD),
2411            key_hash: 1,
2412            sequence: 77,
2413            timestamp_ns: 5,
2414            event: JournalEvent::App(TestEvent::Cmd(0xEE)),
2415            ..InputSlot::default()
2416        };
2417        for _ in 0..CAPACITY {
2418            producer.publish(stale);
2419        }
2420        assert_eq!(drain(&mut consumer).len(), CAPACITY, "every slot dirtied");
2421
2422        for byte in 1..=CAPACITY as u8 {
2423            conn.ingress.extend_from_slice(&frame(byte));
2424        }
2425        let (action, _early) = run_process_frames(&mut conn, &mut producer);
2426        assert_eq!(action, FrameAction::Continue);
2427
2428        let events = drain(&mut consumer);
2429        assert_eq!(events.len(), CAPACITY, "every dirtied slot reused");
2430        for (i, (_, slot)) in events.iter().enumerate() {
2431            assert_eq!(slot.connection_id, 7);
2432            assert_eq!(slot.request_id, id_of(i as u8 + 1), "the frame's own ID");
2433            assert_eq!(slot.key_hash, 0xC0FFEE_u64);
2434            assert_eq!(slot.sequence, 0, "the journal stage allocates it");
2435            assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
2436            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(i as u8 + 1)));
2437        }
2438    }
2439
2440    /// A halted reader publishes no write: each is refused under its ID,
2441    /// while queries still go through. Nothing refused can reach the journal, so replay cannot
2442    /// apply a write the client was told failed.
2443    #[test]
2444    fn a_halted_reader_refuses_writes_before_publishing_and_still_publishes_queries() {
2445        let Fixture {
2446            mut conn,
2447            mut producer,
2448            mut consumer,
2449            ..
2450        } = make_fixture(16);
2451        // Write, query, write, query.
2452        for byte in [0x01, 0xFF, 0x02, 0xFF] {
2453            conn.ingress.extend_from_slice(&frame(byte));
2454        }
2455        let (mut early, mut queue) = early_channel();
2456        let counted = Arc::new(AtomicU64::new(0));
2457
2458        let action = run_process_frames_with(
2459            &mut conn,
2460            &mut producer,
2461            &gate_counting_into(Some(0), false, Arc::clone(&counted)),
2462            &mut early,
2463        );
2464        assert_eq!(action, FrameAction::Continue);
2465        assert!(conn.ingress.bytes().is_empty(), "every frame consumed");
2466        assert_eq!(
2467            refused_count(&counted),
2468            2,
2469            "the two writes are counted, the queries are not"
2470        );
2471
2472        let published: Vec<_> = drain(&mut consumer)
2473            .into_iter()
2474            .map(|(seq, slot)| (seq, slot.event))
2475            .collect();
2476        assert_eq!(
2477            published,
2478            [
2479                (0, JournalEvent::App(TestEvent::Query)),
2480                (1, JournalEvent::App(TestEvent::Query)),
2481            ],
2482            "only the queries enter the pipeline"
2483        );
2484
2485        let expect = |byte| {
2486            early_reply(
2487                id_of(byte),
2488                EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
2489            )
2490        };
2491        assert_eq!(
2492            released(&mut queue),
2493            [expect(0x01), expect(0x02)],
2494            "each write is answered under its ID"
2495        );
2496    }
2497
2498    /// A fenced node closes the connection, whatever its replica count:
2499    /// nothing is published, nothing is refused, nothing is counted.
2500    #[test]
2501    fn a_fenced_reader_closes_the_connection() {
2502        let Fixture {
2503            mut conn,
2504            mut producer,
2505            mut consumer,
2506            ..
2507        } = make_fixture(16);
2508        for byte in [0x01, 0xFF] {
2509            conn.ingress.extend_from_slice(&frame(byte));
2510        }
2511        let (mut early, mut queue) = early_channel();
2512        let counted = Arc::new(AtomicU64::new(0));
2513
2514        let action = run_process_frames_with(
2515            &mut conn,
2516            &mut producer,
2517            &gate_counting_into(Some(1), true, Arc::clone(&counted)),
2518            &mut early,
2519        );
2520
2521        assert_eq!(action, FrameAction::Disconnect, "the connection is dropped");
2522        assert!(
2523            drain(&mut consumer).is_empty(),
2524            "not even the query is published"
2525        );
2526        assert!(released(&mut queue).is_empty(), "no early reply is queued");
2527        assert_eq!(refused_count(&counted), 0);
2528    }
2529
2530    /// An answer with nowhere to go parks its connection: the frames
2531    /// before it are committed, nothing from the frame that met the full
2532    /// queue on is published or answered, and that frame and the one behind
2533    /// it stay buffered. Once the queue has room, a call with no new bytes
2534    /// answers or publishes them as it would have. Each kind of early reply
2535    /// meets it the same way: a halt refusal (counted once answered), a
2536    /// filtered and a refused request, and a busy on a full ring, whose
2537    /// shed tail survives the park, so the frame behind it is shed too
2538    /// though the ring has room by then.
2539    #[test]
2540    fn a_full_early_reply_queue_parks_before_the_frame() {
2541        let refused_as =
2542            |byte: u8, kind, reason| early_reply(id_of(byte), EarlyKind::Refused { kind, reason });
2543        let rejected = |byte: u8| {
2544            early_reply(
2545                id_of(byte),
2546                EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
2547            )
2548        };
2549        let cmd_3 = [JournalEvent::App(TestEvent::Cmd(0x03))];
2550        // (slots already taken in a two-slot ring, halted, the frame that
2551        // meets the full queue, what the call after the park answers, and
2552        // publishes)
2553        type Case<'a> = (
2554            usize,
2555            bool,
2556            u8,
2557            Vec<EarlyReply<TestReport>>,
2558            &'a [JournalEvent<TestEvent>],
2559        );
2560        let cases: [Case; 5] = [
2561            (0, true, 0x02, vec![rejected(0x02), rejected(0x03)], &[]),
2562            (
2563                0,
2564                false,
2565                0xFC,
2566                vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2567                &cmd_3,
2568            ),
2569            (
2570                0,
2571                false,
2572                0xFD,
2573                vec![refused_as(0xFD, RefusalKind::PermissionDenied, "denied")],
2574                &cmd_3,
2575            ),
2576            (
2577                0,
2578                false,
2579                0xFE,
2580                vec![refused_as(0xFE, RefusalKind::Malformed, "bad")],
2581                &cmd_3,
2582            ),
2583            // The ring has room for the first frame only: the second is
2584            // shed, and the third with it.
2585            (
2586                1,
2587                false,
2588                0x02,
2589                vec![
2590                    early_reply(id_of(0x02), EarlyKind::Busy),
2591                    early_reply(id_of(0x03), EarlyKind::Busy),
2592                ],
2593                &[],
2594            ),
2595        ];
2596        for (prefill, halted, byte, answers, publishes) in cases {
2597            let Fixture {
2598                mut conn,
2599                mut producer,
2600                mut consumer,
2601                ..
2602            } = make_fixture(2);
2603            for _ in 0..prefill {
2604                producer.publish(InputSlot::default());
2605            }
2606            // A query first, which is published even while halted.
2607            conn.ingress.extend_from_slice(&frame(0xFF));
2608            conn.ingress.extend_from_slice(&frame(byte));
2609            conn.ingress.extend_from_slice(&frame(0x03));
2610            let (mut early, mut queue) = early_channel_of(4);
2611            let filler = early_reply(RequestId(0), EarlyKind::Busy);
2612            let mut filled = 0;
2613            while early.try_send(filler).is_ok() {
2614                filled += 1;
2615            }
2616
2617            let counted = Arc::new(AtomicU64::new(0));
2618            let halt = gate_counting_into(halted.then_some(0), false, Arc::clone(&counted));
2619            let metrics = IngressMetrics::default();
2620            let action =
2621                run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
2622
2623            assert_eq!(action, FrameAction::Park, "byte {byte:#04x}");
2624            assert_eq!(
2625                refused_count(&counted),
2626                0,
2627                "byte {byte:#04x}: a write parked on is not refused yet"
2628            );
2629            assert_eq!(
2630                metrics.counts(),
2631                IngressTally::default(),
2632                "byte {byte:#04x}: nor is anything else counted"
2633            );
2634            let published: Vec<_> = drain(&mut consumer)
2635                .into_iter()
2636                .skip(prefill)
2637                .map(|(_, slot)| slot.event)
2638                .collect();
2639            assert_eq!(
2640                published,
2641                [JournalEvent::App(TestEvent::Query)],
2642                "byte {byte:#04x}: the frame before is committed, nothing after"
2643            );
2644            assert_eq!(
2645                conn.ingress.bytes(),
2646                [frame(byte), frame(0x03)].concat(),
2647                "byte {byte:#04x}: the frame parked on stays, with the tail"
2648            );
2649            let queued = released(&mut queue);
2650            assert_eq!(
2651                queued.len(),
2652                filled,
2653                "byte {byte:#04x}: nothing more was queued"
2654            );
2655            assert!(queued.iter().all(|reply| *reply == filler));
2656
2657            // The queue has room again (and so does the ring): the retry
2658            // takes no new bytes.
2659            let action =
2660                run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
2661            assert_eq!(action, FrameAction::Continue, "byte {byte:#04x}");
2662            assert_eq!(
2663                metrics.counts(),
2664                ingress_counts_of(&answers),
2665                "byte {byte:#04x}: each answer counted once, as its kind"
2666            );
2667            assert_eq!(released(&mut queue), answers, "byte {byte:#04x}");
2668            let published: Vec<_> = drain(&mut consumer)
2669                .into_iter()
2670                .map(|(_, slot)| slot.event)
2671                .collect();
2672            assert_eq!(published, publishes, "byte {byte:#04x}");
2673            assert!(conn.ingress.bytes().is_empty(), "byte {byte:#04x}");
2674            assert_eq!(conn.ingress.shed_remaining(), 0, "byte {byte:#04x}");
2675            assert_eq!(
2676                refused_count(&counted),
2677                if halted { 2 } else { 0 },
2678                "byte {byte:#04x}: each refused write counted once"
2679            );
2680        }
2681    }
2682
2683    /// Whatever the call returns, every early reply it staged is visible
2684    /// to the response stage by then: the queue is flushed on every way
2685    /// out, and nothing here flushes it after the call (`released` reads
2686    /// it as the stage does). One case per way out: every frame
2687    /// processed, a shed tail answered whole, a close on a frame that is
2688    /// not a request (also in the middle of a shed tail) and on an
2689    /// oversized prefix, and a park (also in the middle of a shed tail),
2690    /// each after an answer was staged. A fenced node stages nothing.
2691    #[test]
2692    fn every_way_out_leaves_the_early_replies_it_staged_visible() {
2693        let refused = |byte: u8| {
2694            early_reply(
2695                id_of(byte),
2696                EarlyKind::Refused {
2697                    kind: if byte == 0xFD {
2698                        RefusalKind::PermissionDenied
2699                    } else {
2700                        RefusalKind::Malformed
2701                    },
2702                    reason: if byte == 0xFD { "denied" } else { "bad" },
2703                },
2704            )
2705        };
2706        let busy = |byte: u8| early_reply(id_of(byte), EarlyKind::Busy);
2707        let not_a_request = tagged_frame(0x00, &[0x03; 9]);
2708        // (case, ring capacity, early-reply queue capacity, frames, the
2709        // outcome, what it stages)
2710        let cases = [
2711            (
2712                "every frame processed",
2713                16,
2714                16,
2715                vec![frame(0xFC), frame(0x01)],
2716                FrameAction::Continue,
2717                vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2718            ),
2719            (
2720                "a full ring",
2721                2,
2722                16,
2723                vec![
2724                    frame(0xFD),
2725                    frame(0x01),
2726                    frame(0x02),
2727                    frame(0x03),
2728                    frame(0x04),
2729                ],
2730                FrameAction::Continue,
2731                vec![refused(0xFD), busy(0x03), busy(0x04)],
2732            ),
2733            (
2734                "a frame that is not a request",
2735                16,
2736                16,
2737                vec![frame(0xFE), not_a_request.clone()],
2738                FrameAction::Disconnect,
2739                vec![refused(0xFE)],
2740            ),
2741            (
2742                "a frame that is not a request, in a shed tail",
2743                2,
2744                16,
2745                vec![
2746                    frame(0x01),
2747                    frame(0x02),
2748                    frame(0x03),
2749                    not_a_request,
2750                    frame(0x04),
2751                ],
2752                FrameAction::Disconnect,
2753                vec![busy(0x03)],
2754            ),
2755            (
2756                "an oversized prefix",
2757                16,
2758                16,
2759                vec![frame(0xFC), oversize_prefix().to_vec()],
2760                FrameAction::Disconnect,
2761                vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2762            ),
2763            (
2764                "a park",
2765                16,
2766                2,
2767                vec![frame(0xFC), frame(0xFE), frame(0xFD)],
2768                FrameAction::Park,
2769                vec![early_reply(id_of(0xFC), EarlyKind::Filtered), refused(0xFE)],
2770            ),
2771            (
2772                "a park in a shed tail",
2773                2,
2774                2,
2775                vec![
2776                    frame(0x01),
2777                    frame(0x02),
2778                    frame(0x03),
2779                    frame(0x04),
2780                    frame(0x05),
2781                ],
2782                FrameAction::Park,
2783                vec![busy(0x03), busy(0x04)],
2784            ),
2785        ];
2786        for (case, ring_capacity, queue_capacity, frames, outcome, expected) in cases {
2787            let Fixture {
2788                mut conn,
2789                mut producer,
2790                ..
2791            } = make_fixture(ring_capacity);
2792            for frame in frames {
2793                conn.ingress.extend_from_slice(&frame);
2794            }
2795            let (mut early, mut queue) = early_channel_of(queue_capacity);
2796            let action =
2797                run_process_frames_with(&mut conn, &mut producer, &gate(None, false), &mut early);
2798            assert_eq!(action, outcome, "{case}");
2799            assert_eq!(released(&mut queue), expected, "{case}");
2800        }
2801    }
2802
2803    /// The caller stamps `recv_ts` once per recv (at the kernel-return
2804    /// site) and every frame parsed from that buffer must carry that exact
2805    /// value. Guards against a regression to per-frame `recv_ts` capture,
2806    /// which excluded decode from the reader-ingest / server-e2e windows
2807    /// and drifted forward for later frames in a multi-frame recv. Gated on
2808    /// `latency-trace` because `recv_ts` is a real `u64` only then (`()`
2809    /// otherwise, leaving nothing to assert).
2810    #[cfg(feature = "latency-trace")]
2811    #[test]
2812    fn process_frames_stamps_every_slot_with_caller_recv_ts() {
2813        let Fixture {
2814            mut conn,
2815            mut producer,
2816            mut consumer,
2817            ..
2818        } = make_fixture(16);
2819        for byte in [0x01, 0x02, 0x03, 0x04] {
2820            conn.ingress.extend_from_slice(&frame(byte));
2821        }
2822
2823        // A recognizable sentinel the caller would have captured at the
2824        // recv site; distinct from the wall-clock stamp (0xDEAD_BEEF).
2825        const RECV_TS: u64 = 0x5EED_5EED;
2826
2827        let mut publish_rec = melin_transport_core::trace::register_stage("test: publish recv_ts");
2828        #[cfg(feature = "tick-to-trade")]
2829        let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest recv_ts");
2830
2831        let (mut early, _queue) = early_channel();
2832        let action = process_frames::<TestApp, UnixStream>(
2833            &mut conn,
2834            &mut producer,
2835            &ByteDecoder,
2836            &gate(None, false),
2837            &mut early,
2838            &IngressMetrics::default(),
2839            0xDEAD_BEEF,
2840            RECV_TS,
2841            &mut publish_rec,
2842            #[cfg(feature = "tick-to-trade")]
2843            &mut ingest_rec,
2844        );
2845        assert_eq!(action, FrameAction::Continue);
2846
2847        let events = drain(&mut consumer);
2848        assert_eq!(events.len(), 4, "all frames published");
2849        for (_seq, slot) in &events {
2850            assert_eq!(
2851                slot.recv_ts, RECV_TS,
2852                "every frame from one recv shares the single caller-supplied recv_ts"
2853            );
2854        }
2855    }
2856
2857    #[test]
2858    fn process_frames_rotates_batch_at_commit_every_cap() {
2859        // Push more events than `COMMIT_EVERY` (= 16) into a recv-cycle.
2860        // The cap must trigger at least one mid-loop commit so the
2861        // consumer sees the first capacity-many events before the
2862        // remainder lands. Validates the visibility-delay cap from the
2863        // perf branch — without it, all 32 events would commit together
2864        // and the first frame would wait for the 32nd to decode.
2865        //
2866        // Capacity 64 leaves room for the entire input (no Full); ring
2867        // backpressure is exercised separately in
2868        // `a_full_ring_queues_busy_for_the_frame_that_met_it`.
2869        let Fixture {
2870            mut conn,
2871            mut producer,
2872            mut consumer,
2873            ..
2874        } = make_fixture(64);
2875        const EVENT_COUNT: usize = 32;
2876        for i in 0..EVENT_COUNT {
2877            // Use bytes 1..=32 (each ≤ 0xFB so ByteDecoder yields
2878            // `Permitted` with `TestEvent::Cmd(byte)`).
2879            conn.ingress.extend_from_slice(&frame((i + 1) as u8));
2880        }
2881
2882        let (action, _early) = run_process_frames(&mut conn, &mut producer);
2883        assert_eq!(
2884            action,
2885            FrameAction::Continue,
2886            "no oversize / no Full ⇒ no disconnect"
2887        );
2888
2889        let events = drain(&mut consumer);
2890        assert_eq!(events.len(), EVENT_COUNT, "every event visible");
2891        for (i, (seq, slot)) in events.iter().enumerate() {
2892            assert_eq!(*seq, i as u64, "seq contiguous across batch rotations");
2893            let byte = (i + 1) as u8;
2894            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2895        }
2896        assert!(conn.ingress.bytes().is_empty());
2897    }
2898
2899    #[test]
2900    fn process_frames_query_event_skips_wall_clock_stamp() {
2901        // `AppEvent::is_query` events bypass the journal stamp — verify
2902        // the timestamp is zeroed even when a non-zero batch_wall_ns was
2903        // supplied.
2904        let Fixture {
2905            mut conn,
2906            mut producer,
2907            mut consumer,
2908            ..
2909        } = make_fixture(8);
2910        conn.ingress.extend_from_slice(&frame(0xFF)); // body byte → Query
2911
2912        let (action, _early) = run_process_frames(&mut conn, &mut producer);
2913        assert_eq!(action, FrameAction::Continue);
2914
2915        let events = drain(&mut consumer);
2916        assert_eq!(events.len(), 1);
2917        let (_, slot) = &events[0];
2918        assert_eq!(slot.event, JournalEvent::App(TestEvent::Query));
2919        assert_eq!(
2920            slot.timestamp_ns, 0,
2921            "query events must skip the wall-clock stamp"
2922        );
2923    }
2924
2925    #[test]
2926    fn a_full_ring_sheds_the_frame_that_met_it_and_every_complete_frame_behind() {
2927        // Ring capacity 4 + 7 frames, the last cut short ⇒ first 4 commit,
2928        // the 5th triggers Full, the 6th is shed with it, the partial 7th
2929        // waits. Validates:
2930        //   * `Err(Full)` does not roll back the prior 4.
2931        //   * The frame that triggered Full, and the complete frame behind
2932        //     it, are answered `Busy`, each with its ID, without being
2933        //     decoded or published, and the connection is kept.
2934        //   * The busy goes to the response stage through the early-reply
2935        //     queue, and nothing is written to the socket from here.
2936        //   * A trailing partial frame is not part of the tail: it stays,
2937        //     and once whole it is judged on its own.
2938        let Fixture {
2939            mut conn,
2940            mut producer,
2941            mut consumer,
2942            mut peer,
2943        } = make_fixture(4);
2944        for byte in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] {
2945            conn.ingress.extend_from_slice(&frame(byte));
2946        }
2947        let partial = frame(0x07);
2948        conn.ingress.extend_from_slice(&partial[..5]);
2949
2950        let (action, early) = run_process_frames(&mut conn, &mut producer);
2951        assert_eq!(
2952            action,
2953            FrameAction::Continue,
2954            "Full does not drop the connection"
2955        );
2956
2957        let events = drain(&mut consumer);
2958        assert_eq!(
2959            events.len(),
2960            4,
2961            "only the first capacity-many frames are visible"
2962        );
2963        for (i, (_, slot)) in events.iter().enumerate() {
2964            let byte = (i + 1) as u8;
2965            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2966        }
2967
2968        // The busy goes to the response stage — never directly onto the
2969        // socket, where it could tear a partially-flushed response frame
2970        // (audit review F3).
2971        assert_eq!(
2972            early,
2973            [
2974                early_reply(id_of(0x05), EarlyKind::Busy),
2975                early_reply(id_of(0x06), EarlyKind::Busy),
2976            ],
2977            "a busy for the frame that met the full ring, and for the one behind"
2978        );
2979        assert!(
2980            !peer_got_bytes(&mut peer),
2981            "reader must never write the client socket"
2982        );
2983
2984        // Only the partial frame is left, outside any tail.
2985        assert_eq!(conn.ingress.bytes(), &partial[..5]);
2986        assert_eq!(conn.ingress.shed_remaining(), 0);
2987
2988        // The ring has room again (drained above) and the 7th completes:
2989        // published, as a request on a later receive is.
2990        conn.ingress.extend_from_slice(&partial[5..]);
2991        let (action, early) = run_process_frames(&mut conn, &mut producer);
2992        assert_eq!(action, FrameAction::Continue);
2993        assert!(early.is_empty(), "{early:?}");
2994        let events = drain(&mut consumer);
2995        assert_eq!(events.len(), 1);
2996        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x07)));
2997    }
2998
2999    /// Regression for the 2026-08 io_uring audit, finding 2 and review
3000    /// finding F3 (docs/internal/io-uring-audit-2026-08.md): the
3001    /// pipeline-full path must not touch the client socket at all. The
3002    /// original defect was a blocking `write(2)` of ServerBusy that
3003    /// parked the reader thread whenever the offender's socket buffer
3004    /// was also full — stalling ingress for *every* connection exactly
3005    /// on the overload path (against that code this test never
3006    /// returns). The busy notice now routes to the response stage
3007    /// through the early-reply queue, which also closes the
3008    /// frame-tearing race of any reader-side send. The socket-buffer
3009    /// fill stays: it proves the path is insensitive to socket state.
3010    #[test]
3011    fn pipeline_full_never_touches_the_client_socket() {
3012        let Fixture {
3013            mut conn,
3014            mut producer,
3015            mut consumer,
3016            mut peer,
3017        } = make_fixture(2);
3018
3019        // Fill the server→peer direction until the kernel refuses more —
3020        // the state a non-reading client leaves the connection in.
3021        let junk = [0u8; 4096];
3022        loop {
3023            let n = unsafe {
3024                libc::send(
3025                    conn.fd,
3026                    junk.as_ptr().cast(),
3027                    junk.len(),
3028                    libc::MSG_DONTWAIT,
3029                )
3030            };
3031            if n < 0 {
3032                assert_eq!(
3033                    std::io::Error::last_os_error().raw_os_error(),
3034                    Some(libc::EAGAIN),
3035                    "unexpected errno while filling socket"
3036                );
3037                break;
3038            }
3039        }
3040
3041        // Capacity 2 + 3 frames ⇒ the third triggers Full ⇒ ServerBusy.
3042        conn.ingress.extend_from_slice(&frame(0x01));
3043        conn.ingress.extend_from_slice(&frame(0x02));
3044        conn.ingress.extend_from_slice(&frame(0x03));
3045
3046        let start = std::time::Instant::now();
3047        let (action, early) = run_process_frames(&mut conn, &mut producer);
3048        // Generous bound to stay unflaky under CI load — the fixed path
3049        // returns in microseconds, the broken one only when the peer
3050        // reads (here: never).
3051        assert!(
3052            start.elapsed() < Duration::from_secs(1),
3053            "pipeline-full path blocked on a full socket"
3054        );
3055        assert_eq!(
3056            action,
3057            FrameAction::Continue,
3058            "Full does not drop the connection"
3059        );
3060        assert_eq!(
3061            drain(&mut consumer).len(),
3062            2,
3063            "the frames that fit are still published"
3064        );
3065        assert_eq!(
3066            early,
3067            [early_reply(id_of(0x03), EarlyKind::Busy)],
3068            "busy notice routed to response stage"
3069        );
3070        // Nothing beyond the pre-fill junk arrives: drain it, then the
3071        // read must time out rather than yield a reader-written frame.
3072        let mut sink = [0u8; 4096];
3073        loop {
3074            match peer.read(&mut sink) {
3075                Ok(0) => panic!("unexpected EOF"),
3076                Ok(_) => continue,
3077                Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
3078                    break;
3079                }
3080                Err(e) => panic!("unexpected peer read error: {e}"),
3081            }
3082        }
3083    }
3084
3085    #[test]
3086    fn process_frames_oversize_commits_prior_frames_then_signals_disconnect() {
3087        // Two valid frames followed by an oversize length prefix must:
3088        //   * publish the two valid frames (commit-before-break) so the
3089        //     pipeline observes them even though we're about to tear the
3090        //     connection down,
3091        //   * return `true` so the caller drops the connection,
3092        //   * answer nothing early (a busy is for a full ring).
3093        let Fixture {
3094            mut conn,
3095            mut producer,
3096            mut consumer,
3097            mut peer,
3098        } = make_fixture(16);
3099        conn.ingress.extend_from_slice(&frame(0x01));
3100        conn.ingress.extend_from_slice(&frame(0x02));
3101        conn.ingress.extend_from_slice(&oversize_prefix());
3102
3103        let (action, early) = run_process_frames(&mut conn, &mut producer);
3104        assert_eq!(
3105            action,
3106            FrameAction::Disconnect,
3107            "oversize frame must request disconnect"
3108        );
3109
3110        let events = drain(&mut consumer);
3111        assert_eq!(
3112            events.len(),
3113            2,
3114            "prior frames are committed before the break"
3115        );
3116        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3117        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
3118
3119        assert!(early.is_empty(), "{early:?}");
3120        assert!(
3121            !peer_got_bytes(&mut peer),
3122            "reader must never write the client socket"
3123        );
3124    }
3125
3126    /// Each decoder outcome that publishes nothing yields its early reply,
3127    /// with the frame's ID, in order among the other early replies; all
3128    /// bytes are consumed.
3129    #[test]
3130    fn each_decoder_outcome_yields_its_early_reply_with_the_frames_id() {
3131        // Mixed batch: Permitted, Filter, PermissionDenied, DecodeError,
3132        // Permitted, and a request with an empty body (a decode error).
3133        let Fixture {
3134            mut conn,
3135            mut producer,
3136            mut consumer,
3137            ..
3138        } = make_fixture(16);
3139        for byte in [0x01, 0xFC, 0xFD, 0xFE, 0x02] {
3140            conn.ingress.extend_from_slice(&frame(byte));
3141        }
3142        conn.ingress.extend_from_slice(&request_frame(99, b""));
3143
3144        let (action, early) = run_process_frames(&mut conn, &mut producer);
3145        assert_eq!(
3146            action,
3147            FrameAction::Continue,
3148            "a refused request keeps the connection"
3149        );
3150
3151        let events = drain(&mut consumer);
3152        assert_eq!(events.len(), 2);
3153        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3154        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
3155        assert_eq!(
3156            early,
3157            [
3158                early_reply(id_of(0xFC), EarlyKind::Filtered),
3159                early_reply(
3160                    id_of(0xFD),
3161                    EarlyKind::Refused {
3162                        kind: RefusalKind::PermissionDenied,
3163                        reason: "denied"
3164                    }
3165                ),
3166                early_reply(
3167                    id_of(0xFE),
3168                    EarlyKind::Refused {
3169                        kind: RefusalKind::Malformed,
3170                        reason: "bad"
3171                    }
3172                ),
3173                early_reply(
3174                    RequestId(99),
3175                    EarlyKind::Refused {
3176                        kind: RefusalKind::Malformed,
3177                        reason: "empty body"
3178                    }
3179                ),
3180            ]
3181        );
3182        assert!(
3183            conn.ingress.bytes().is_empty(),
3184            "all bytes advanced past compaction"
3185        );
3186    }
3187
3188    /// The request tag and ID are the runtime's to read: a frame that is
3189    /// not a request — an empty one, one under any other tag, a request
3190    /// too short for its ID — closes the connection before any decoder
3191    /// sees it, with the frames before it committed and nothing after it
3192    /// read. One such frame per connection. The decoder gets the body
3193    /// alone, so a body byte equal to a protocol tag is just that —
3194    /// `frame(0x01)` carries the heartbeat's value as a command.
3195    #[test]
3196    fn a_frame_that_is_not_a_request_closes_the_connection() {
3197        let mut not_requests = vec![
3198            // An empty frame: no tag at all.
3199            0u32.to_le_bytes().to_vec(),
3200            // A request tag with fewer bytes than an ID behind it.
3201            tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[0x03; 7]),
3202            tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[]),
3203        ];
3204        // A zeroed tag, the protocol's own frames, the application tag of
3205        // a protocol-0 request, and an application tag as a pre-release
3206        // build framed one — each would decode to `Cmd(0x03)` if a
3207        // decoder saw it.
3208        for tag in [
3209            0x00,
3210            TAG_CHALLENGE_RESPONSE,
3211            TAG_RESPONSE_HEARTBEAT,
3212            TAG_APP,
3213            0x10,
3214        ] {
3215            not_requests.push(tagged_frame(tag, &[0x03; 9]));
3216        }
3217        for bad in not_requests {
3218            let Fixture {
3219                mut conn,
3220                mut producer,
3221                mut consumer,
3222                ..
3223            } = make_fixture(16);
3224            conn.ingress.extend_from_slice(&frame(0x01));
3225            conn.ingress.extend_from_slice(&frame(0xFC));
3226            conn.ingress.extend_from_slice(&bad);
3227            conn.ingress.extend_from_slice(&frame(0x02));
3228
3229            let (action, early) = run_process_frames(&mut conn, &mut producer);
3230            assert_eq!(
3231                action,
3232                FrameAction::Disconnect,
3233                "{bad:?} must close the connection"
3234            );
3235
3236            let events = drain(&mut consumer);
3237            assert_eq!(events.len(), 1, "{bad:?}");
3238            assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3239            // The answer decided before it is still sent.
3240            assert_eq!(early, [early_reply(id_of(0xFC), EarlyKind::Filtered)]);
3241        }
3242    }
3243
3244    #[test]
3245    fn process_frames_preserves_partial_trailing_frame() {
3246        // One complete frame followed by a truncated length prefix. The
3247        // complete frame must publish; the partial bytes must survive
3248        // compaction at the front of `parse_buf` for the next recv-cycle.
3249        let Fixture {
3250            mut conn,
3251            mut producer,
3252            mut consumer,
3253            ..
3254        } = make_fixture(16);
3255        conn.ingress.extend_from_slice(&frame(0x42));
3256        // Three of four length-prefix bytes — not even a length to judge
3257        // yet, so the loop breaks before consuming anything from the
3258        // partial.
3259        conn.ingress.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
3260
3261        let (action, _early) = run_process_frames(&mut conn, &mut producer);
3262        assert_eq!(action, FrameAction::Continue);
3263
3264        let events = drain(&mut consumer);
3265        assert_eq!(events.len(), 1);
3266        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x42)));
3267        assert_eq!(
3268            conn.ingress.bytes(),
3269            [0xDE, 0xAD, 0xBE],
3270            "partial length prefix preserved for next recv-cycle"
3271        );
3272    }
3273
3274    /// The cap is inclusive: a frame of exactly `MAX_FRAME_SIZE` payload
3275    /// bytes is waited for while it arrives and then published, and a
3276    /// prefix one past it drops the connection on its own, with no payload
3277    /// behind it and nothing in front.
3278    #[test]
3279    fn process_frames_admits_a_frame_at_the_limit_and_drops_one_past_it() {
3280        let Fixture {
3281            mut conn,
3282            mut producer,
3283            mut consumer,
3284            ..
3285        } = make_fixture(16);
3286        let mut body = vec![0x05; MAX_REQUEST_BODY];
3287        body[0] = 0x2A;
3288        let at_limit = request_frame(5, &body);
3289        assert_eq!(at_limit.len(), 4 + MAX_FRAME_SIZE);
3290
3291        // The prefix and part of the payload: kept, not judged oversized.
3292        conn.ingress.extend_from_slice(&at_limit[..4 + 1]);
3293        let (action, _early) = run_process_frames(&mut conn, &mut producer);
3294        assert_eq!(
3295            action,
3296            FrameAction::Continue,
3297            "a frame at the limit is not oversized"
3298        );
3299        assert_eq!(conn.ingress.bytes(), &at_limit[..4 + 1]);
3300        assert!(drain(&mut consumer).is_empty());
3301
3302        // The rest of it arrives: published, buffer drained.
3303        conn.ingress.extend_from_slice(&at_limit[4 + 1..]);
3304        let (action, _early) = run_process_frames(&mut conn, &mut producer);
3305        assert_eq!(action, FrameAction::Continue);
3306        let events = drain(&mut consumer);
3307        assert_eq!(events.len(), 1);
3308        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x2A)));
3309        assert_eq!(events[0].1.request_id, RequestId(5));
3310        assert!(conn.ingress.bytes().is_empty());
3311
3312        // One byte more, announced by a prefix alone: dropped at once.
3313        conn.ingress.extend_from_slice(&oversize_prefix());
3314        let (action, _early) = run_process_frames(&mut conn, &mut producer);
3315        assert_eq!(
3316            action,
3317            FrameAction::Disconnect,
3318            "oversize prefix alone must request disconnect"
3319        );
3320        assert!(drain(&mut consumer).is_empty());
3321    }
3322
3323    #[test]
3324    fn process_frames_empty_buffer_is_noop() {
3325        // No bytes in parse_buf ⇒ loop never enters; commit is the
3326        // documented zero-slot no-op; no busy event; no disconnect.
3327        let Fixture {
3328            mut conn,
3329            mut producer,
3330            mut consumer,
3331            mut peer,
3332        } = make_fixture(4);
3333
3334        let (action, early) = run_process_frames(&mut conn, &mut producer);
3335        assert_eq!(action, FrameAction::Continue);
3336        assert_eq!(drain(&mut consumer).len(), 0);
3337        assert!(conn.ingress.bytes().is_empty());
3338        assert!(early.is_empty());
3339        assert!(!peer_got_bytes(&mut peer));
3340    }
3341
3342    /// The shedding and parking rules over random streams of requests of
3343    /// every decoder outcome, split into random receives, through a ring
3344    /// and an early-reply queue of random small sizes that the "pipeline"
3345    /// drains at random. A receive arrives only once the previous call did
3346    /// not park; a park is followed by a retry with no new bytes once the
3347    /// queue was drained, as the reader's retry is.
3348    mod shedding {
3349        use super::*;
3350        use proptest::prelude::*;
3351
3352        /// The body bytes [`ByteDecoder`] maps to each outcome: commands,
3353        /// a filtered one, a denied one, a malformed one, a query.
3354        fn body() -> impl Strategy<Value = u8> {
3355            prop_oneof![1u8..=8, Just(0xFC), Just(0xFD), Just(0xFE), Just(0xFF)]
3356        }
3357
3358        prop_compose! {
3359            fn script()(
3360                bodies in prop::collection::vec(body(), 1..40),
3361                ring_capacity in prop_oneof![Just(2usize), Just(4), Just(8)],
3362                queue_capacity in prop_oneof![Just(2usize), Just(4)],
3363            )(
3364                cuts in prop::collection::vec(0..bodies.len() * 14 + 1, 0..12),
3365                drains in prop::collection::vec((0usize..4, any::<bool>()), 64),
3366                bodies in Just(bodies),
3367                ring_capacity in Just(ring_capacity),
3368                queue_capacity in Just(queue_capacity),
3369            ) -> (Vec<u8>, Vec<usize>, Vec<(usize, bool)>, usize, usize) {
3370                (bodies, cuts, drains, ring_capacity, queue_capacity)
3371            }
3372        }
3373
3374        proptest! {
3375            #![proptest_config(ProptestConfig::with_cases(256))]
3376            #[test]
3377            fn every_request_is_published_or_answered_once_and_a_shed_tail_whole(
3378                (bodies, mut cuts, drains, ring_capacity, queue_capacity) in script()
3379            ) {
3380                // Request `i` carries ID `i`; every frame is 14 bytes.
3381                let frames: Vec<Vec<u8>> = bodies
3382                    .iter()
3383                    .enumerate()
3384                    .map(|(i, body)| request_frame(i as u64, &[*body]))
3385                    .collect();
3386                let stream = frames.concat();
3387                cuts.push(0);
3388                cuts.push(stream.len());
3389                cuts.sort_unstable();
3390                cuts.dedup();
3391
3392                let Fixture { mut conn, mut producer, mut consumer, .. } =
3393                    make_fixture(ring_capacity);
3394                let (mut early, mut queue) = early_channel_of(queue_capacity);
3395                let halt = gate(None, false);
3396                let metrics = IngressMetrics::default();
3397                let mut published: Vec<u64> = Vec::new();
3398                let mut answered: Vec<EarlyReply<TestReport>> = Vec::new();
3399                // Per receive (an episode: the call after it and the
3400                // retries of its parks), how many frames were complete.
3401                let mut episodes: Vec<usize> = Vec::new();
3402                let mut drains = drains.into_iter().cycle();
3403                let mut take = |consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
3404                                queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
3405                                published: &mut Vec<u64>,
3406                                answered: &mut Vec<EarlyReply<TestReport>>,
3407                                all: bool| {
3408                    let (events, replies) = drains.next().expect("cycled");
3409                    for _ in 0..if all { usize::MAX } else { events } {
3410                        let Some((_, slot)) = consumer.try_consume() else { break };
3411                        published.push(slot.request_id.0);
3412                    }
3413                    if all || replies {
3414                        answered.extend(released(queue));
3415                    }
3416                };
3417
3418                let mut receives = cuts.windows(2).map(|w| &stream[w[0]..w[1]]);
3419                let mut action = FrameAction::Continue;
3420                loop {
3421                    if action == FrameAction::Park {
3422                        // The response stage drains the queue; the retry
3423                        // brings no new bytes.
3424                        answered.extend(released(&mut queue));
3425                    } else if let Some(bytes) = receives.next() {
3426                        conn.ingress.extend_from_slice(bytes);
3427                        let received = stream.len() - receives.clone().map(<[u8]>::len).sum::<usize>();
3428                        episodes.push(received / frames[0].len());
3429                    } else {
3430                        break;
3431                    }
3432                    action = run_process_frames_counting(
3433                        &mut conn, &mut producer, &halt, &mut early, &metrics,
3434                    );
3435                    prop_assert_ne!(action, FrameAction::Disconnect);
3436                    take(&mut consumer, &mut queue, &mut published, &mut answered, false);
3437                }
3438                take(&mut consumer, &mut queue, &mut published, &mut answered, true);
3439                prop_assert!(conn.ingress.bytes().is_empty());
3440                prop_assert_eq!(conn.ingress.shed_remaining(), 0);
3441                // Each early answer counted once, as its kind, however
3442                // many parks its frame was retried through.
3443                prop_assert_eq!(metrics.counts(), ingress_counts_of(&answered));
3444
3445                // Every request exactly once: published, or answered as its
3446                // decoder outcome says, or busy.
3447                let mut seen: Vec<u64> = published.clone();
3448                seen.extend(answered.iter().map(|reply| reply.request_id.0));
3449                seen.sort_unstable();
3450                prop_assert_eq!(seen, (0..bodies.len() as u64).collect::<Vec<_>>());
3451                for id in &published {
3452                    let body = bodies[*id as usize];
3453                    prop_assert!(body <= 8 || body == 0xFF, "published {body:#04x}");
3454                }
3455                let mut busy: Vec<u64> = Vec::new();
3456                for reply in &answered {
3457                    let body = bodies[reply.request_id.0 as usize];
3458                    match reply.kind {
3459                        EarlyKind::Busy => busy.push(reply.request_id.0),
3460                        EarlyKind::Filtered => prop_assert_eq!(body, 0xFC),
3461                        EarlyKind::Refused { kind: RefusalKind::PermissionDenied, .. } => {
3462                            prop_assert_eq!(body, 0xFD)
3463                        }
3464                        EarlyKind::Refused { kind: RefusalKind::Malformed, .. } => {
3465                            prop_assert_eq!(body, 0xFE)
3466                        }
3467                        EarlyKind::Rejected(_) => prop_assert!(false, "not halted"),
3468                    }
3469                }
3470                // A request is answered in the first episode in which it
3471                // was complete (an episode ends only once every complete
3472                // frame is processed), and a shed request takes every
3473                // complete frame behind it then: busy too, never
3474                // published, never decoded.
3475                for &k in &busy {
3476                    let complete = episodes
3477                        .iter()
3478                        .copied()
3479                        .find(|&complete| complete > k as usize)
3480                        .expect("answered once complete");
3481                    for j in k + 1..complete as u64 {
3482                        prop_assert!(
3483                            busy.contains(&j),
3484                            "request {j} was in the tail shed at {k}, \
3485                             {complete} complete: {busy:?}"
3486                        );
3487                    }
3488                }
3489            }
3490        }
3491    }
3492
3493    /// Every way a connection's receive state can change, against the
3494    /// CQEs the kernel can post and the outcomes a buffer can have: a park
3495    /// cancels an armed receive, a parked connection keeps the bytes that
3496    /// land before the cancel and is never re-armed while a cancel is in
3497    /// flight, a teardown issues no second cancel, and a dying connection
3498    /// is freed only on its terminal CQE.
3499    mod recv_state {
3500        use super::super::{
3501            CqeStep, FrameAction, FramesStep, RecvCompletion, RecvState, TeardownStep,
3502        };
3503        use std::time::{Duration, Instant};
3504
3505        const DATA: RecvCompletion = RecvCompletion::Data;
3506        const EMPTY: RecvCompletion = RecvCompletion::Empty;
3507        const CLOSED: RecvCompletion = RecvCompletion::Closed;
3508
3509        fn parked(cancelling: bool, since: Instant) -> RecvState {
3510            RecvState::Parked { cancelling, since }
3511        }
3512
3513        /// The normal path: data on an armed receive is processed; its
3514        /// end, with or without data, re-arms it once processed, and an
3515        /// empty end (a dry pool) re-arms at once.
3516        #[test]
3517        fn an_armed_receive_is_processed_and_re_armed_when_it_ends() {
3518            let now = Instant::now();
3519            assert_eq!(
3520                RecvState::Armed.on_cqe(DATA, false),
3521                (RecvState::Armed, CqeStep::Process)
3522            );
3523            assert_eq!(
3524                RecvState::Armed.on_cqe(DATA, true),
3525                (RecvState::Idle, CqeStep::Process)
3526            );
3527            assert_eq!(
3528                RecvState::Idle.after_frames(FrameAction::Continue, now),
3529                (RecvState::Armed, FramesStep::Arm)
3530            );
3531            assert_eq!(
3532                RecvState::Armed.after_frames(FrameAction::Continue, now),
3533                (RecvState::Armed, FramesStep::Nothing)
3534            );
3535            assert_eq!(
3536                RecvState::Armed.on_cqe(EMPTY, true),
3537                (RecvState::Armed, CqeStep::Arm)
3538            );
3539            assert_eq!(
3540                RecvState::Armed.on_cqe(EMPTY, false),
3541                (RecvState::Armed, CqeStep::Nothing)
3542            );
3543            assert_eq!(
3544                RecvState::Armed.on_cqe(CLOSED, true),
3545                (RecvState::Idle, CqeStep::Remove)
3546            );
3547            // Nothing was armed: an end changes nothing, and one with no
3548            // bytes arms a receive.
3549            assert_eq!(RecvState::Idle.after_terminal(), RecvState::Idle);
3550            assert_eq!(RecvState::Dying.after_terminal(), RecvState::Dying);
3551            assert_eq!(
3552                RecvState::Idle.on_cqe(EMPTY, true),
3553                (RecvState::Armed, CqeStep::Arm)
3554            );
3555        }
3556
3557        /// A park on an armed receive cancels it; bytes landing before the
3558        /// cancel's terminal CQE are kept, not processed; the terminal CQE
3559        /// (the cancel's, or an end of the receive's own) leaves it parked
3560        /// with nothing owed and does not re-arm; a retry that processes
3561        /// the buffer re-arms it.
3562        #[test]
3563        fn a_park_cancels_keeps_late_bytes_and_re_arms_on_resume() {
3564            let since = Instant::now();
3565            let later = since + std::time::Duration::from_secs(5);
3566            assert_eq!(
3567                RecvState::Armed.after_frames(FrameAction::Park, since),
3568                (parked(true, since), FramesStep::Cancel)
3569            );
3570            assert_eq!(
3571                parked(true, since).on_cqe(DATA, false),
3572                (parked(true, since), CqeStep::Keep)
3573            );
3574            assert_eq!(
3575                parked(true, since).on_cqe(DATA, true),
3576                (parked(false, since), CqeStep::Keep)
3577            );
3578            assert_eq!(
3579                parked(true, since).on_cqe(EMPTY, true),
3580                (parked(false, since), CqeStep::Nothing),
3581                "no re-arm while parked"
3582            );
3583            assert_eq!(
3584                parked(true, since).on_cqe(EMPTY, false),
3585                (parked(true, since), CqeStep::Nothing)
3586            );
3587            // Parked again by a retry: still parked since it first did.
3588            assert_eq!(
3589                parked(false, since).after_frames(FrameAction::Park, later),
3590                (parked(false, since), FramesStep::Nothing)
3591            );
3592            assert_eq!(
3593                parked(false, since).after_frames(FrameAction::Continue, later),
3594                (RecvState::Armed, FramesStep::Arm)
3595            );
3596            // A park on a receive already ended needs no cancel.
3597            assert_eq!(
3598                RecvState::Idle.after_frames(FrameAction::Park, since),
3599                (parked(false, since), FramesStep::Nothing)
3600            );
3601        }
3602
3603        /// A retry that processes the buffer before the cancel's terminal
3604        /// CQE arms nothing: the connection resumes on that CQE, and the
3605        /// bytes that land before it are processed, as the connection is
3606        /// no longer parked. Parking again there needs no second cancel.
3607        #[test]
3608        fn a_resume_before_the_cancel_lands_arms_on_its_terminal_cqe() {
3609            let since = Instant::now();
3610            assert_eq!(
3611                parked(true, since).after_frames(FrameAction::Continue, since),
3612                (RecvState::Resuming, FramesStep::Nothing)
3613            );
3614            assert_eq!(
3615                RecvState::Resuming.on_cqe(DATA, false),
3616                (RecvState::Resuming, CqeStep::Process)
3617            );
3618            assert_eq!(
3619                RecvState::Resuming.after_frames(FrameAction::Continue, since),
3620                (RecvState::Resuming, FramesStep::Nothing)
3621            );
3622            assert_eq!(
3623                RecvState::Resuming.on_cqe(EMPTY, true),
3624                (RecvState::Armed, CqeStep::Arm)
3625            );
3626            assert_eq!(
3627                RecvState::Resuming.on_cqe(DATA, true),
3628                (RecvState::Idle, CqeStep::Process)
3629            );
3630            assert_eq!(
3631                RecvState::Resuming.after_frames(FrameAction::Park, since),
3632                (parked(true, since), FramesStep::Nothing)
3633            );
3634        }
3635
3636        /// Teardown by state: an armed receive is cancelled; a cancel in
3637        /// flight (a park's, or a resume's) is awaited, never doubled;
3638        /// nothing owed frees at once; a second teardown does nothing.
3639        /// A dying connection discards its CQEs and is freed on the
3640        /// terminal one only. A connection whose peer closes while parked
3641        /// is removed.
3642        #[test]
3643        fn teardown_cancels_once_and_frees_once() {
3644            let since = Instant::now();
3645            assert_eq!(
3646                RecvState::Armed.on_teardown(),
3647                (RecvState::Dying, TeardownStep::Cancel)
3648            );
3649            for cancelling in [parked(true, since), RecvState::Resuming] {
3650                assert_eq!(
3651                    cancelling.on_teardown(),
3652                    (RecvState::Dying, TeardownStep::AwaitCancel),
3653                    "{cancelling:?}"
3654                );
3655            }
3656            for owed_nothing in [parked(false, since), RecvState::Idle] {
3657                assert_eq!(
3658                    owed_nothing.on_teardown(),
3659                    (owed_nothing, TeardownStep::FreeNow),
3660                    "{owed_nothing:?}"
3661                );
3662            }
3663            assert_eq!(
3664                RecvState::Dying.on_teardown(),
3665                (RecvState::Dying, TeardownStep::AlreadyDying)
3666            );
3667            for completion in [DATA, EMPTY, CLOSED] {
3668                assert_eq!(
3669                    RecvState::Dying.on_cqe(completion, false),
3670                    (RecvState::Dying, CqeStep::Discard { free: false })
3671                );
3672                assert_eq!(
3673                    RecvState::Dying.on_cqe(completion, true),
3674                    (RecvState::Dying, CqeStep::Discard { free: true })
3675                );
3676            }
3677            assert_eq!(parked(true, since).on_cqe(CLOSED, true).1, CqeStep::Remove);
3678            // A buffer that closes the connection tears it down, whatever
3679            // the state.
3680            for state in [
3681                RecvState::Armed,
3682                RecvState::Idle,
3683                RecvState::Resuming,
3684                parked(true, since),
3685            ] {
3686                assert_eq!(
3687                    state.after_frames(FrameAction::Disconnect, since),
3688                    (state, FramesStep::Teardown)
3689                );
3690            }
3691        }
3692
3693        /// Only a park whose cancel is still in flight past the deadline
3694        /// counts as unhonoured: not one at the deadline, not one whose
3695        /// cancel has landed however long it has been parked, and no
3696        /// other state.
3697        #[test]
3698        fn only_a_park_cancel_in_flight_past_the_deadline_is_overdue() {
3699            let since = Instant::now();
3700            let past = since + super::super::PARK_CANCEL_DEADLINE + Duration::from_nanos(1);
3701            let at = since + super::super::PARK_CANCEL_DEADLINE;
3702            assert!(parked(true, since).park_cancel_overdue(past));
3703            assert!(!parked(true, since).park_cancel_overdue(at));
3704            assert!(!parked(true, since).park_cancel_overdue(since));
3705            for state in [
3706                parked(false, since),
3707                RecvState::Armed,
3708                RecvState::Idle,
3709                RecvState::Resuming,
3710                RecvState::Dying,
3711            ] {
3712                assert!(!state.park_cancel_overdue(past), "{state:?}");
3713            }
3714        }
3715
3716        /// What the shutdown quiesce waits for: a CQE the kernel owes.
3717        #[test]
3718        fn the_kernel_owes_a_cqe_while_a_receive_or_a_cancel_is_in_flight() {
3719            let since = Instant::now();
3720            for owed in [
3721                RecvState::Armed,
3722                RecvState::Resuming,
3723                RecvState::Dying,
3724                parked(true, since),
3725            ] {
3726                assert!(owed.kernel_owes_cqe(), "{owed:?}");
3727            }
3728            for free in [RecvState::Idle, parked(false, since)] {
3729                assert!(!free.kernel_owes_cqe(), "{free:?}");
3730            }
3731        }
3732    }
3733
3734    /// End-to-end soak through the real reader loop: 4 connections
3735    /// concurrently write 10 000 frames each, one `write(2)` per frame,
3736    /// so the receive path churns through the shared buffer pool and
3737    /// its recycle machinery under genuine concurrency (buf_ring where
3738    /// the kernel supports it, legacy ProvideBuffers otherwise — this
3739    /// test is the coverage for whichever mode the host engages).
3740    ///
3741    /// The failure signatures of a recycle bug are exactly what is
3742    /// asserted: a torn or misordered frame (a buffer reused while the
3743    /// kernel still owned it), a lost frame, or a spurious disconnect
3744    /// (`ENOBUFS` mishandled as a client error).
3745    #[test]
3746    fn reader_loop_soak_delivers_every_frame_in_order() {
3747        use std::io::Write as _;
3748
3749        const CONNS: u64 = 4;
3750        const FRAMES: u32 = 10_000;
3751
3752        // Capacity ≥ total frames: even if this thread is descheduled
3753        // and stops draining, the ring cannot fill, so the reader never
3754        // sheds load (which would legitimately drop frames and turn a
3755        // CI hiccup into a false failure).
3756        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
3757            .add_consumer()
3758            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3759        let mut consumer = consumers.pop().expect("consumer");
3760        let (control_tx, control_rx) = mpsc::channel();
3761        let shutdown = Arc::new(AtomicBool::new(false));
3762
3763        let mut handle = spawn_reader::<TestApp, UnixStream>(
3764            producer,
3765            Arc::new(ByteDecoder),
3766            gate(None, false),
3767            early_channel().0,
3768            Arc::new(IngressMetrics::default()),
3769            policy_claim(),
3770            control_tx,
3771            0,    // "do not pin" sentinel
3772            None, // no idle timeout — a CI stall must not disconnect
3773            None, // no tick generator
3774            // Sized for exactly these connections: the smallest pool the
3775            // sizing hands out, so the soak runs the pool dry (ENOBUFS,
3776            // re-arm) far more often than a production-sized one would.
3777            RingSizing::for_max_connections(CONNS).expect("a supported cap"),
3778            Arc::clone(&shutdown),
3779        )
3780        .expect("the reader starts");
3781
3782        let mut writers = Vec::new();
3783        for id in 0..CONNS {
3784            let (client, server_side) = UnixStream::pair().expect("socketpair");
3785            handle.register(ReaderRegistration {
3786                connection_id: ConnectionId(id),
3787                reader: server_side,
3788                addr: "127.0.0.1:1".parse().expect("addr"),
3789                role: ClientRole::Operator,
3790                key_hash: id,
3791            });
3792            writers.push(std::thread::spawn(move || {
3793                let mut client = client;
3794                for i in 0..FRAMES {
3795                    let byte = (i % 200 + 1) as u8;
3796                    client.write_all(&frame(byte)).expect("client write");
3797                    // Brief pauses fragment the stream so the reader
3798                    // sees many small recvs (heavy buffer churn) rather
3799                    // than a few large coalesced ones.
3800                    if i % 512 == 0 {
3801                        std::thread::sleep(Duration::from_micros(200));
3802                    }
3803                }
3804                client // keep the socket alive until after the drain
3805            }));
3806        }
3807
3808        // Drain until every frame arrived or the deadline passes. The
3809        // deadline is generous — the run takes well under a second on
3810        // an idle machine — because a genuine recycle deadlock must
3811        // fail loudly, not hang CI.
3812        let deadline = Instant::now() + Duration::from_secs(60);
3813        let mut per_conn_count = [0u32; CONNS as usize];
3814        let mut total: u64 = 0;
3815        while total < CONNS * FRAMES as u64 {
3816            assert!(
3817                Instant::now() < deadline,
3818                "soak stalled: {total} of {} frames after 60s ({per_conn_count:?})",
3819                CONNS * FRAMES as u64
3820            );
3821            while let Some((_seq, slot)) = consumer.try_consume() {
3822                let conn = slot.connection_id as usize;
3823                assert!(conn < CONNS as usize, "unknown connection id");
3824                let expected = (per_conn_count[conn] % 200 + 1) as u8;
3825                assert_eq!(
3826                    slot.event,
3827                    JournalEvent::App(TestEvent::Cmd(expected)),
3828                    "conn {conn} frame {} out of order or corrupted",
3829                    per_conn_count[conn]
3830                );
3831                per_conn_count[conn] += 1;
3832                total += 1;
3833            }
3834            std::hint::spin_loop();
3835        }
3836
3837        // Any control event at this point is a defect: sockets are
3838        // still alive (held by the writer join handles) and the ring
3839        // never filled. Every arm diverges, so inspecting the first
3840        // queued event suffices.
3841        if let Ok(event) = control_rx.try_recv() {
3842            match event {
3843                ControlEvent::Disconnected { connection_id } => {
3844                    panic!("spurious disconnect of connection {connection_id}")
3845                }
3846                ControlEvent::Connected { .. } => {
3847                    unreachable!("reader never sends Connected")
3848                }
3849            }
3850        }
3851
3852        let _clients: Vec<UnixStream> = writers
3853            .into_iter()
3854            .map(|w| w.join().expect("writer thread"))
3855            .collect();
3856        handle.shutdown();
3857        handle.join();
3858    }
3859
3860    /// Shutdown with live, armed connections must quiesce the ring
3861    /// provably and promptly. The armed multishot RECVs sit on sockets
3862    /// whose peers never disconnect, so nothing completes them on its
3863    /// own — the teardown's wakes (per-connection `shutdown(2)`, the
3864    /// eventfd write) are what retire them, including on hosts that
3865    /// filter the AsyncCancel opcode, where the old cancel-then-quiet
3866    /// teardown could conclude "done" with arms still live against the
3867    /// soon-freed pool. A drain that cannot prove quiescence only
3868    /// returns at the full deadline (and leaks the buffers), so
3869    /// returning well inside it is the proof this test can observe.
3870    #[test]
3871    fn shutdown_quiesces_armed_connections_promptly() {
3872        use std::io::Write as _;
3873
3874        const CONNS: u64 = 3;
3875        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(1024)
3876            .add_consumer()
3877            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3878        let mut consumer = consumers.pop().expect("consumer");
3879        let (control_tx, _control_rx) = mpsc::channel();
3880        let shutdown = Arc::new(AtomicBool::new(false));
3881        let mut handle = spawn_reader::<TestApp, UnixStream>(
3882            producer,
3883            Arc::new(ByteDecoder),
3884            gate(None, false),
3885            early_channel().0,
3886            Arc::new(IngressMetrics::default()),
3887            policy_claim(),
3888            control_tx,
3889            0,    // "do not pin" sentinel
3890            None, // no idle timeout
3891            None, // no tick generator
3892            RingSizing::for_max_connections(CONNS).expect("a supported cap"),
3893            Arc::clone(&shutdown),
3894        )
3895        .expect("the reader starts");
3896
3897        let mut clients = Vec::new();
3898        for id in 0..CONNS {
3899            let (mut client, server_side) = UnixStream::pair().expect("socketpair");
3900            handle.register(ReaderRegistration {
3901                connection_id: ConnectionId(id),
3902                reader: server_side,
3903                addr: "127.0.0.1:1".parse().expect("addr"),
3904                role: ClientRole::Operator,
3905                key_hash: id,
3906            });
3907            // One delivered frame per connection proves its multishot
3908            // is armed and active before the shutdown fires.
3909            client.write_all(&frame(7)).expect("client write");
3910            clients.push(client);
3911        }
3912        let deadline = Instant::now() + Duration::from_secs(30);
3913        let mut seen = 0u64;
3914        while seen < CONNS {
3915            assert!(
3916                Instant::now() < deadline,
3917                "frames never arrived ({seen}/{CONNS})"
3918            );
3919            while consumer.try_consume().is_some() {
3920                seen += 1;
3921            }
3922            std::hint::spin_loop();
3923        }
3924
3925        // The clients stay alive across the shutdown: no peer
3926        // disconnect can complete the armed RECVs — only the teardown's
3927        // own wakes can.
3928        let started = Instant::now();
3929        handle.shutdown();
3930        handle.join();
3931        let elapsed = started.elapsed();
3932        assert!(
3933            elapsed < crate::uring_teardown::DRAIN_TIMEOUT / 4,
3934            "reader teardown took {elapsed:?}, close to the drain deadline — \
3935             armed operations were not woken"
3936        );
3937        drop(clients);
3938    }
3939
3940    /// Regression for audit review F1
3941    /// (docs/internal/io-uring-audit-2026-08.md): a connection torn down
3942    /// while its multishot RECV is still armed must not have its slab
3943    /// index reused while the kernel can still post CQEs carrying it.
3944    /// Pre-fix, the LIFO free list handed client A's index straight to
3945    /// client B; A's socket stayed open (the armed op holds a file
3946    /// reference past the fd close), and A's continued bytes were parsed
3947    /// under B's connection id, key hash, and permissions — request
3948    /// injection under someone else's identity. Post-fix, A's entry dies
3949    /// in place until the cancelled op's terminal CQE, so B gets a fresh
3950    /// index and A's post-teardown bytes are discarded.
3951    #[test]
3952    fn torn_down_connection_cannot_inject_frames_into_a_reused_slot() {
3953        use std::io::Write as _;
3954
3955        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
3956            .add_consumer()
3957            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3958        let mut consumer = consumers.pop().expect("consumer");
3959        let (control_tx, control_rx) = mpsc::channel();
3960        let shutdown = Arc::new(AtomicBool::new(false));
3961        let mut handle = spawn_reader::<TestApp, UnixStream>(
3962            producer,
3963            Arc::new(ByteDecoder),
3964            gate(None, false),
3965            early_channel().0,
3966            Arc::new(IngressMetrics::default()),
3967            policy_claim(),
3968            control_tx,
3969            0,
3970            None,
3971            None,
3972            // A and B, plus room for A's entry to linger while dying.
3973            RingSizing::for_max_connections(4).expect("a supported cap"),
3974            Arc::clone(&shutdown),
3975        )
3976        .expect("the reader starts");
3977
3978        const A_ID: u64 = 100;
3979        const B_ID: u64 = 200;
3980
3981        // Client A: valid frames (0x01..=0x08), an oversize prefix (the
3982        // teardown trigger), then a sustained stream of would-be
3983        // injection frames from A's byte range.
3984        let (a_client, a_server) = UnixStream::pair().expect("socketpair");
3985        handle.register(ReaderRegistration {
3986            connection_id: ConnectionId(A_ID),
3987            reader: a_server,
3988            addr: "127.0.0.1:1".parse().expect("addr"),
3989            role: ClientRole::Operator,
3990            key_hash: A_ID,
3991        });
3992        let a_writer = std::thread::spawn(move || {
3993            let mut a = a_client;
3994            for byte in 0x01..=0x08u8 {
3995                let _ = a.write_all(&frame(byte));
3996            }
3997            let _ = a.write_all(&oversize_prefix());
3998            // Keep writing after the reader repudiates us. Write errors
3999            // (EPIPE once teardown's shutdown(2) lands) are the fix
4000            // doing its job — ignore them and keep trying.
4001            let stop = Instant::now() + Duration::from_millis(300);
4002            while Instant::now() < stop {
4003                let _ = a.write_all(&frame(0x05));
4004                std::thread::sleep(Duration::from_micros(50));
4005            }
4006            a
4007        });
4008
4009        // Wait for A's teardown announcement, then register B — the
4010        // instant a buggy free list would hand B the still-armed index.
4011        let deadline = Instant::now() + Duration::from_secs(10);
4012        loop {
4013            match control_rx.try_recv() {
4014                Ok(ControlEvent::Disconnected { connection_id }) if connection_id == A_ID => break,
4015                Ok(_) => {}
4016                Err(_) => {
4017                    assert!(Instant::now() < deadline, "no Disconnected for A");
4018                    std::thread::sleep(Duration::from_millis(1));
4019                }
4020            }
4021        }
4022
4023        const B_FRAMES: u32 = 2_000;
4024        let (b_client, b_server) = UnixStream::pair().expect("socketpair");
4025        handle.register(ReaderRegistration {
4026            connection_id: ConnectionId(B_ID),
4027            reader: b_server,
4028            addr: "127.0.0.1:2".parse().expect("addr"),
4029            role: ClientRole::Operator,
4030            key_hash: B_ID,
4031        });
4032        let b_writer = std::thread::spawn(move || {
4033            let mut b = b_client;
4034            for i in 0..B_FRAMES {
4035                let byte = 0x21 + (i % 0x10) as u8; // 0x21..=0x30, disjoint from A
4036                b.write_all(&frame(byte)).expect("B write");
4037                if i % 128 == 0 {
4038                    std::thread::sleep(Duration::from_micros(100));
4039                }
4040            }
4041            b
4042        });
4043
4044        // Drain until all of B's frames arrive. Every B-attributed slot
4045        // must carry B's key hash and a B-range byte; anything else is
4046        // the injection this test exists to prevent.
4047        let deadline = Instant::now() + Duration::from_secs(30);
4048        let mut b_seen = 0u32;
4049        while b_seen < B_FRAMES {
4050            assert!(
4051                Instant::now() < deadline,
4052                "stalled: {b_seen}/{B_FRAMES} B frames"
4053            );
4054            while let Some((_seq, slot)) = consumer.try_consume() {
4055                match slot.event {
4056                    JournalEvent::App(TestEvent::Cmd(byte)) => {
4057                        if slot.connection_id == B_ID {
4058                            assert_eq!(slot.key_hash, B_ID, "B slot with foreign key hash");
4059                            assert!(
4060                                (0x21..=0x30).contains(&byte),
4061                                "byte {byte:#x} injected into B's stream"
4062                            );
4063                            b_seen += 1;
4064                        } else {
4065                            assert_eq!(slot.connection_id, A_ID, "unknown connection id");
4066                            assert!(
4067                                (0x01..=0x08).contains(&byte),
4068                                "unexpected byte {byte:#x} on A"
4069                            );
4070                        }
4071                    }
4072                    ref other => panic!("unexpected event {other:?}"),
4073                }
4074            }
4075            std::hint::spin_loop();
4076        }
4077
4078        let _a = a_writer.join().expect("A writer");
4079        let _b = b_writer.join().expect("B writer");
4080        handle.shutdown();
4081        handle.join();
4082    }
4083
4084    /// Parking through the real reader loop, on a real io_uring over
4085    /// socket pairs: a small input ring the test drains (or not), and an
4086    /// early-reply queue of capacity 2 that the test drains in the
4087    /// response stage's place, so it fills on demand. Filtered requests
4088    /// (`0xFC`) are the early replies that fill it.
4089    mod parking {
4090        use super::*;
4091        use crate::early_reply::EarlyReplyQueue;
4092        use melin_transport_core::fence::FenceState;
4093        use std::io::Write as _;
4094
4095        const FILTERED: u8 = 0xFC;
4096
4097        /// A running reader, the ends the test plays the pipeline with,
4098        /// and its client connections.
4099        struct Rig {
4100            handle: UringReaderHandle<UnixStream>,
4101            consumer: ring::Consumer<InputSlot<TestEvent>>,
4102            queue: EarlyReplyQueue<TestReport>,
4103            control_rx: mpsc::Receiver<ControlEvent>,
4104            fence: Arc<FenceState>,
4105            metrics: Arc<IngressMetrics>,
4106        }
4107
4108        impl Rig {
4109            /// A reader over an input ring of `ring_capacity` and a
4110            /// two-entry early-reply queue, with `connection_timeout`.
4111            fn start(ring_capacity: usize, connection_timeout: Option<Duration>) -> Self {
4112                let (producer, mut consumers) =
4113                    DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
4114                        .add_consumer()
4115                        .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
4116                let consumer = consumers.pop().expect("consumer");
4117                let (control_tx, control_rx) = mpsc::channel();
4118                let fence = Arc::new(FenceState::new(0));
4119                let halt = HaltGate::new(None, Arc::clone(&fence), Arc::new(AtomicU64::new(0)));
4120                let (early, queue) = early_channel_of(2);
4121                let metrics = Arc::new(IngressMetrics::default());
4122                let handle = spawn_reader::<TestApp, UnixStream>(
4123                    producer,
4124                    Arc::new(ByteDecoder),
4125                    halt,
4126                    early,
4127                    Arc::clone(&metrics),
4128                    policy_claim(),
4129                    control_tx,
4130                    0,
4131                    connection_timeout,
4132                    None,
4133                    RingSizing::for_max_connections(4).expect("a supported cap"),
4134                    Arc::new(AtomicBool::new(false)),
4135                )
4136                .expect("the reader starts");
4137                Rig {
4138                    handle,
4139                    consumer,
4140                    queue,
4141                    control_rx,
4142                    fence,
4143                    metrics,
4144                }
4145            }
4146
4147            /// Wait until the parked gauge reads `parked`; panics after ten
4148            /// seconds. The gauge is set at the end of the reader's retry
4149            /// pass, which may come just after what the test saw.
4150            fn wait_for_parked(&self, parked: u64) {
4151                let deadline = Instant::now() + Duration::from_secs(10);
4152                while self.metrics.parked() != parked {
4153                    assert!(
4154                        Instant::now() < deadline,
4155                        "the parked gauge stayed at {}, not {parked}",
4156                        self.metrics.parked()
4157                    );
4158                    std::thread::sleep(Duration::from_millis(1));
4159                }
4160            }
4161
4162            /// Register connection `id`; returns the client's end and a
4163            /// second handle on the server's end (the response stage's
4164            /// dup, in a node).
4165            fn connect(&mut self, id: u64) -> (UnixStream, UnixStream) {
4166                let (client, server_side) = UnixStream::pair().expect("socketpair");
4167                let dup = server_side.try_clone().expect("dup");
4168                self.handle.register(ReaderRegistration {
4169                    connection_id: ConnectionId(id),
4170                    reader: server_side,
4171                    addr: "127.0.0.1:1".parse().expect("addr"),
4172                    role: ClientRole::Operator,
4173                    key_hash: id,
4174                });
4175                (client, dup)
4176            }
4177
4178            /// Every early reply queued so far, as the response stage
4179            /// would take them: making room in the queue.
4180            fn take_replies(&mut self) -> Vec<EarlyReply<TestReport>> {
4181                released(&mut self.queue)
4182            }
4183
4184            /// Every request published so far, by ID.
4185            fn take_published(&mut self) -> Vec<RequestId> {
4186                drain(&mut self.consumer)
4187                    .into_iter()
4188                    .map(|(_, slot)| slot.request_id)
4189                    .collect()
4190            }
4191
4192            /// Wait until `done` holds of what was published and answered
4193            /// so far (accumulated into the two vectors), taking both as
4194            /// the pipeline would; panics after ten seconds.
4195            fn wait_for(
4196                &mut self,
4197                published: &mut Vec<RequestId>,
4198                replies: &mut Vec<EarlyReply<TestReport>>,
4199                done: impl Fn(&[RequestId], &[EarlyReply<TestReport>]) -> bool,
4200            ) {
4201                let deadline = Instant::now() + Duration::from_secs(10);
4202                loop {
4203                    published.extend(self.take_published());
4204                    replies.extend(self.take_replies());
4205                    if done(published, replies) {
4206                        return;
4207                    }
4208                    assert!(
4209                        Instant::now() < deadline,
4210                        "stalled: published {published:?}, answered {replies:?}"
4211                    );
4212                    std::thread::sleep(Duration::from_millis(1));
4213                }
4214            }
4215
4216            /// The `Disconnected` events sent so far, by connection.
4217            fn disconnects(&self) -> Vec<u64> {
4218                self.control_rx
4219                    .try_iter()
4220                    .map(|event| match event {
4221                        ControlEvent::Disconnected { connection_id } => connection_id,
4222                        ControlEvent::Connected { .. } => {
4223                            unreachable!("the reader never sends Connected")
4224                        }
4225                    })
4226                    .collect()
4227            }
4228
4229            fn stop(self) {
4230                self.handle.shutdown();
4231                self.handle.join();
4232            }
4233        }
4234
4235        /// What every test's connection writes: three filtered requests,
4236        /// the third of which parks it (two fill the queue), then five
4237        /// writes, in one burst.
4238        fn burst() -> Vec<u8> {
4239            let mut bytes = Vec::new();
4240            for id in 1..=3 {
4241                bytes.extend(request_frame(id, &[FILTERED]));
4242            }
4243            for id in 4..=8 {
4244                bytes.extend(request_frame(id, &[0x01]));
4245            }
4246            bytes
4247        }
4248
4249        fn filtered_ids(replies: &[EarlyReply<TestReport>]) -> Vec<RequestId> {
4250            replies
4251                .iter()
4252                .map(|reply| {
4253                    assert_eq!(reply.kind, EarlyKind::Filtered, "{reply:?}");
4254                    reply.request_id
4255                })
4256                .collect()
4257        }
4258
4259        /// Park the connection: write the burst, wait until the reader has
4260        /// answered the first two (left in the queue, which they fill) and
4261        /// parked on the third, and check that nothing behind it was
4262        /// published. The parked gauge is set at the end of a retry pass,
4263        /// so by the time it reads one the park's cancel has landed.
4264        fn park(rig: &mut Rig, client: &mut UnixStream) {
4265            client.write_all(&burst()).expect("client write");
4266            rig.wait_for_parked(1);
4267            let published = rig.take_published();
4268            assert!(published.is_empty(), "published past a park: {published:?}");
4269        }
4270
4271        /// The lost-signals scenario: a parked connection resumes without
4272        /// a byte more from its client once the queue has room, and its
4273        /// pipelined writes all complete, in order, each once. Bytes keep
4274        /// coming while it is parked too, and are kept.
4275        #[test]
4276        fn a_parked_connection_resumes_without_new_bytes() {
4277            let mut rig = Rig::start(64, None);
4278            let (mut client, _dup) = rig.connect(1);
4279            park(&mut rig, &mut client);
4280            // While parked, more writes behind: kept for the retry (a
4281            // data CQE landing before the park's cancel, or the socket's
4282            // buffer after it).
4283            for id in 9..=12 {
4284                client
4285                    .write_all(&request_frame(id, &[0x02]))
4286                    .expect("client write");
4287            }
4288            std::thread::sleep(Duration::from_millis(50));
4289            assert!(
4290                rig.take_published().is_empty(),
4291                "nothing read past the park"
4292            );
4293            // Counted as parked, once, however many retries it has been
4294            // through; the two filtered requests answered so far counted.
4295            assert_eq!(rig.metrics.parked(), 1);
4296            assert_eq!(rig.metrics.parks(), 1);
4297            assert_eq!(rig.metrics.counts().filtered, 2);
4298
4299            let (mut published, mut replies) = (Vec::new(), Vec::new());
4300            rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 9);
4301            assert_eq!(published, (4..=12).map(RequestId).collect::<Vec<_>>());
4302            assert_eq!(
4303                filtered_ids(&replies),
4304                (1..=3).map(RequestId).collect::<Vec<_>>()
4305            );
4306            assert!(rig.disconnects().is_empty(), "the connection is kept");
4307            // Resumed: no longer parked, and still parked only once; the
4308            // request it parked on counted once, when answered.
4309            rig.wait_for_parked(0);
4310            assert_eq!(rig.metrics.parks(), 1);
4311            assert_eq!(rig.metrics.counts().filtered, 3);
4312
4313            // Read again normally once resumed.
4314            client
4315                .write_all(&request_frame(13, &[0x03]))
4316                .expect("client write");
4317            rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 10);
4318            assert_eq!(published.last(), Some(&RequestId(13)));
4319            drop(client);
4320            rig.stop();
4321        }
4322
4323        /// Many parks and resumes in a row, with the client writing one
4324        /// frame per `write` as fast as it can, so data CQEs keep landing
4325        /// around each park's cancel: every request is published or
4326        /// answered exactly once, the writes in order.
4327        #[test]
4328        fn repeated_parks_lose_and_repeat_nothing() {
4329            const REQUESTS: u64 = 600;
4330            let mut rig = Rig::start(1024, None);
4331            let (client, _dup) = rig.connect(1);
4332            let writer = std::thread::spawn(move || {
4333                let mut client = client;
4334                for id in 1..=REQUESTS {
4335                    let body = if id % 3 == 0 { 0x01 } else { FILTERED };
4336                    client
4337                        .write_all(&request_frame(id, &[body]))
4338                        .expect("client write");
4339                }
4340                client
4341            });
4342            let (mut published, mut replies) = (Vec::new(), Vec::new());
4343            rig.wait_for(&mut published, &mut replies, |p, r| {
4344                (p.len() + r.len()) as u64 >= REQUESTS
4345            });
4346            let _client = writer.join().expect("writer");
4347            assert_eq!(
4348                published,
4349                (1..=REQUESTS)
4350                    .filter(|id| id % 3 == 0)
4351                    .map(RequestId)
4352                    .collect::<Vec<_>>()
4353            );
4354            assert_eq!(
4355                filtered_ids(&replies),
4356                (1..=REQUESTS)
4357                    .filter(|id| id % 3 != 0)
4358                    .map(RequestId)
4359                    .collect::<Vec<_>>()
4360            );
4361            assert!(rig.disconnects().is_empty());
4362            rig.stop();
4363        }
4364
4365        /// The idle scan skips a parked connection: its client is not
4366        /// idle, the node is not reading it. Resumed, it is judged on its
4367        /// idle clock restarted then.
4368        #[test]
4369        fn a_parked_connection_is_not_reaped_as_idle() {
4370            let mut rig = Rig::start(64, Some(Duration::from_millis(100)));
4371            let (mut client, _dup) = rig.connect(1);
4372            park(&mut rig, &mut client);
4373            // Past several scans (one a second) and many timeouts.
4374            std::thread::sleep(Duration::from_millis(2_500));
4375            assert!(rig.disconnects().is_empty(), "reaped while parked");
4376
4377            let (mut published, mut replies) = (Vec::new(), Vec::new());
4378            rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 5);
4379            // Idle from its resume on, it is reaped as any other. The scan
4380            // runs only when the reader wakes, so a second, busy client
4381            // keeps waking it.
4382            let (mut other, _other_dup) = rig.connect(2);
4383            let deadline = Instant::now() + Duration::from_secs(10);
4384            let mut disconnects = Vec::new();
4385            let mut id = 100;
4386            while disconnects.is_empty() {
4387                assert!(Instant::now() < deadline, "never reaped once resumed");
4388                id += 1;
4389                other.write_all(&request_frame(id, &[0x01])).expect("write");
4390                rig.take_published();
4391                disconnects.extend(rig.disconnects());
4392                std::thread::sleep(Duration::from_millis(10));
4393            }
4394            assert_eq!(disconnects, [1], "the resumed connection, not the busy one");
4395            drop((client, other));
4396            rig.stop();
4397        }
4398
4399        /// A parked connection whose retry closes it — the node fenced
4400        /// while it was parked — is torn down once: one `Disconnected`,
4401        /// and its index is freed for the next connection.
4402        #[test]
4403        fn a_parked_connection_closed_by_its_retry_is_torn_down_once() {
4404            let mut rig = Rig::start(64, None);
4405            let (mut client, _dup) = rig.connect(1);
4406            // Parked, its cancel landed (see `park`): torn down with nothing
4407            // owed.
4408            park(&mut rig, &mut client);
4409            rig.fence.fence();
4410            let deadline = Instant::now() + Duration::from_secs(10);
4411            let mut disconnects = Vec::new();
4412            while disconnects.is_empty() {
4413                assert!(Instant::now() < deadline, "never torn down");
4414                disconnects.extend(rig.disconnects());
4415                std::thread::sleep(Duration::from_millis(1));
4416            }
4417            std::thread::sleep(Duration::from_millis(50));
4418            disconnects.extend(rig.disconnects());
4419            assert_eq!(disconnects, [1], "torn down once");
4420            assert!(rig.take_published().is_empty());
4421            // Gone, so no longer counted as parked.
4422            rig.wait_for_parked(0);
4423            assert_eq!(rig.metrics.parks(), 1);
4424            rig.stop();
4425        }
4426
4427        /// A request that is not a request, in a shed tail a park cut
4428        /// short, closes the connection when the retry reaches it: the
4429        /// tail before it answered busy, nothing past the full ring
4430        /// published, one `Disconnected`.
4431        #[test]
4432        fn a_non_request_in_a_parked_shed_tail_closes_the_connection_once() {
4433            // Two slots, never drained: the third write is shed.
4434            let mut rig = Rig::start(2, None);
4435            let (mut client, _dup) = rig.connect(1);
4436            let mut bytes = Vec::new();
4437            for id in 1..=5 {
4438                bytes.extend(request_frame(id, &[0x01]));
4439            }
4440            bytes.extend(tagged_frame(0x00, &[0x03; 9]));
4441            client.write_all(&bytes).expect("client write");
4442
4443            let (mut published, mut replies) = (Vec::new(), Vec::new());
4444            // Writes 3 and 4 are shed and fill the queue; 5 parks.
4445            let deadline = Instant::now() + Duration::from_secs(10);
4446            while published.len() < 2 {
4447                assert!(Instant::now() < deadline, "the ring never filled");
4448                published.extend(
4449                    drain(&mut rig.consumer)
4450                        .into_iter()
4451                        .map(|(_, s)| s.request_id),
4452                );
4453                std::thread::sleep(Duration::from_millis(1));
4454            }
4455            // Room in the ring now, but 5 is in the tail.
4456            std::thread::sleep(Duration::from_millis(50));
4457            assert!(rig.disconnects().is_empty(), "parked, not closed");
4458            let mut disconnects = Vec::new();
4459            rig.wait_for(&mut published, &mut replies, |_, r| r.len() == 3);
4460            let deadline = Instant::now() + Duration::from_secs(10);
4461            while disconnects.is_empty() {
4462                assert!(Instant::now() < deadline, "never closed");
4463                disconnects.extend(rig.disconnects());
4464                std::thread::sleep(Duration::from_millis(1));
4465            }
4466            std::thread::sleep(Duration::from_millis(50));
4467            disconnects.extend(rig.disconnects());
4468            assert_eq!(disconnects, [1], "torn down once");
4469            assert_eq!(published, [RequestId(1), RequestId(2)]);
4470            assert_eq!(
4471                replies,
4472                (3..=5)
4473                    .map(|id| early_reply_for(1, RequestId(id), EarlyKind::Busy))
4474                    .collect::<Vec<_>>()
4475            );
4476            rig.stop();
4477        }
4478
4479        /// A parked connection the response stage tears down (a
4480        /// `shutdown(2)` on its own dup, after a send failure) is freed
4481        /// once: the stage drops the connection's early replies, which
4482        /// makes room, the retry re-arms the receive, and the receive
4483        /// reports the close. One `Disconnected`, and the next connection
4484        /// is served.
4485        #[test]
4486        fn a_parked_connection_torn_down_by_the_response_stage_is_freed_once() {
4487            let mut rig = Rig::start(64, None);
4488            let (mut client, dup) = rig.connect(1);
4489            park(&mut rig, &mut client);
4490            // SAFETY: a live descriptor, owned by `dup`.
4491            unsafe { libc::shutdown(dup.as_raw_fd(), libc::SHUT_RDWR) };
4492            let deadline = Instant::now() + Duration::from_secs(10);
4493            let mut disconnects = Vec::new();
4494            while disconnects.is_empty() {
4495                assert!(Instant::now() < deadline, "never freed");
4496                rig.take_replies();
4497                rig.take_published();
4498                disconnects.extend(rig.disconnects());
4499                std::thread::sleep(Duration::from_millis(1));
4500            }
4501            std::thread::sleep(Duration::from_millis(50));
4502            disconnects.extend(rig.disconnects());
4503            assert_eq!(disconnects, [1], "freed once");
4504
4505            let (mut next, _next_dup) = rig.connect(2);
4506            next.write_all(&request_frame(1, &[0x01])).expect("write");
4507            let mut published = Vec::new();
4508            rig.wait_for(&mut published, &mut Vec::new(), |p, _| {
4509                p.contains(&RequestId(1))
4510            });
4511            rig.stop();
4512        }
4513
4514        /// [`early_reply`] for connection `connection_id`.
4515        fn early_reply_for(
4516            connection_id: u64,
4517            request_id: RequestId,
4518            kind: EarlyKind<TestReport>,
4519        ) -> EarlyReply<TestReport> {
4520            EarlyReply {
4521                connection_id,
4522                request_id,
4523                kind,
4524            }
4525        }
4526    }
4527
4528    /// A reader that cannot create its io_uring instance reports it to
4529    /// the spawner as an error naming the thread, instead of panicking
4530    /// on a thread nobody is watching. A zero-entry ring stands in for
4531    /// the locked-memory limit: both fail `io_uring_setup`.
4532    #[test]
4533    fn a_ring_that_cannot_be_created_is_a_startup_error() {
4534        let (producer, _consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
4535            .add_consumer()
4536            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
4537        let (control_tx, _control_rx) = mpsc::channel();
4538        let result = spawn_reader::<TestApp, UnixStream>(
4539            producer,
4540            Arc::new(ByteDecoder),
4541            gate(None, false),
4542            early_channel().0,
4543            Arc::new(IngressMetrics::default()),
4544            policy_claim(),
4545            control_tx,
4546            0,
4547            None,
4548            None,
4549            RingSizing::unchecked(0, 32),
4550            Arc::new(AtomicBool::new(false)),
4551        );
4552        let err = match result {
4553            Ok(_) => panic!("a zero-entry ring cannot be created"),
4554            Err(e) => e,
4555        };
4556        let msg = err.to_string();
4557        assert!(msg.contains("uring-reader"), "{msg}");
4558        assert!(msg.contains("io_uring"), "{msg}");
4559    }
4560
4561    /// `push_sqe` on a full SQ submits what is queued and pushes again,
4562    /// instead of failing: pushing more SQEs than the ring holds must
4563    /// lose none of them.
4564    #[test]
4565    fn push_sqe_makes_room_when_the_sq_is_full() {
4566        const ENTRIES: u32 = 8;
4567        // Three ring-fulls, so the full-SQ branch is taken more than once.
4568        const PUSHES: u64 = 3 * ENTRIES as u64;
4569        // The CQ is twice the SQ by default; size it to hold every
4570        // completion so none is dropped before it is counted.
4571        let mut ring = IoUring::builder()
4572            .setup_cqsize(PUSHES as u32)
4573            .build(ENTRIES)
4574            .expect("create an 8-entry io_uring");
4575        for token in 0..PUSHES {
4576            let nop = opcode::Nop::new().build().user_data(token);
4577            // SAFETY: a NOP references no memory.
4578            unsafe { push_sqe(&mut ring, &nop, "test NOP") };
4579        }
4580        ring.submit_and_wait(PUSHES as usize)
4581            .expect("submit the remaining NOPs");
4582        let mut seen: Vec<u64> = ring.completion().map(|cqe| cqe.user_data()).collect();
4583        seen.sort_unstable();
4584        assert_eq!(seen, (0..PUSHES).collect::<Vec<_>>());
4585    }
4586}