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::halt::{HaltGate, RefusalSender};
33use melin_app::Application;
34use melin_app::auth::Permission;
35use melin_app::decoder::RequestDecoder;
36
37/// Decoder type alias: request decoder bound to the application's `Event`
38/// type. Mirrors [`crate::response::ResponseEncoderArc`]; hides
39/// the `dyn RequestDecoder<Event = …>` spelling at call sites that thread
40/// the decoder through several functions.
41pub type RequestDecoderArc<A> = Arc<dyn RequestDecoder<Event = <A as Application>::Event>>;
42use melin_app::unix_epoch_nanos;
43use melin_pipeline::ring;
44use melin_transport_core::pipeline::InputSlot;
45
46/// Size of each provided buffer. 4 KiB accommodates multiple frames per
47/// recv (frames are typically <100 bytes).
48const BUF_SIZE: usize = 4096;
49
50/// Number of provided buffers in the shared pool. Must be a power of two
51/// (buf_ring ABI) and large enough for concurrent in-flight recvs across
52/// all connections. On exhaustion the kernel completes the multishot
53/// with `ENOBUFS` (no data consumed) and the loop re-arms it after the
54/// drain's recycles refill the ring. 2048 supports up to ~1024
55/// connections per reader thread; recycling is now a shared-memory store
56/// (no SQE), so raising this no longer interacts with `RING_SIZE`.
57const NUM_BUFFERS: u16 = 2048;
58
59/// Buffer group ID for the provided recv buffer pool.
60const BUF_GROUP_ID: u16 = 0;
61
62use crate::client_frames::MAX_FRAME_SIZE;
63
64/// io_uring submission queue depth. Power of 2, sized for up to ~1024
65/// connections per reader thread (multishot RECVs + eventfd read; buffer
66/// recycling goes through the buf_ring, not the SQ).
67const RING_SIZE: u32 = 4096;
68
69/// User data sentinel for the eventfd read SQE.
70const EVENTFD_TOKEN: u64 = u64::MAX;
71
72/// User data sentinel for legacy ProvideBuffers CQEs (fallback recycle
73/// path only). Best-effort re-provisions: we log errors but don't act
74/// on success.
75const PROVIDE_BUFS_TOKEN: u64 = u64::MAX - 1;
76
77/// User data sentinel for AsyncCancel SQEs (connection teardown and the
78/// shutdown quiesce). The completion carries no actionable information:
79/// `ENOENT`/`EALREADY` just mean the target op already finished.
80const CANCEL_TOKEN: u64 = u64::MAX - 3;
81
82/// User data sentinel for the tick timeout SQE. The reader arms a single
83/// `IORING_OP_TIMEOUT` per cadence so `submit_and_wait` returns at the tick
84/// deadline even when no client traffic is flowing. The CQE itself carries
85/// no information; the loop body checks `Instant::now()` against the next
86/// deadline and emits the actual `JournalEvent::Tick`.
87const TICK_TIMEOUT_TOKEN: u64 = u64::MAX - 2;
88
89/// CQE flag: buffer ID is valid in upper 16 bits of flags.
90const IORING_CQE_F_BUFFER: u32 = 1 << 0;
91
92/// CQE flag: more completions coming from this multishot operation.
93const IORING_CQE_F_MORE: u32 = 1 << 1;
94
95/// Bit shift to extract buffer ID from CQE flags.
96const IORING_CQE_BUFFER_SHIFT: u32 = 16;
97
98use melin_wire_protocol::control::ConnectionId;
99
100/// Command sent from the accept loop to a reader thread.
101pub struct ReaderRegistration<R> {
102    pub connection_id: ConnectionId,
103    pub reader: R,
104    pub addr: SocketAddr,
105    /// Permission level established during the auth handshake.
106    pub permission: Permission,
107    /// FxHash of the client's Ed25519 public key. Stored per-connection
108    /// and copied into every InputSlot for per-key idempotency dedup.
109    pub key_hash: u64,
110}
111
112/// Handle for the accept loop to register connections with the io_uring reader.
113pub struct UringReaderHandle<R> {
114    tx: mpsc::Sender<ReaderRegistration<R>>,
115    /// Wakeup eventfd, shared with the reader thread.
116    ///
117    /// `Arc<OwnedFd>` rather than a `RawFd` so the descriptor cannot be
118    /// closed while the other side still holds it: the close happens in
119    /// `OwnedFd`'s `Drop`, which runs when the last holder goes away, and
120    /// no code here is able to call `close` early because none of it owns
121    /// a raw descriptor to close.
122    ///
123    /// The reader thread used to close this itself while this handle was
124    /// still writing wakeups to it — a use-after-close whose real danger
125    /// was descriptor reuse, not `EBADF`: once the number is free, a
126    /// journal segment or an accepted connection can be assigned it, and
127    /// the next wakeup writes eight bytes into that instead. Found by
128    /// ThreadSanitizer.
129    event_fd: Arc<OwnedFd>,
130    join_handle: Option<JoinHandle<()>>,
131    shutdown: Arc<AtomicBool>,
132}
133
134impl<R> UringReaderHandle<R> {
135    /// Register a new connection with the reader thread.
136    ///
137    /// If the reader thread's channel is dead (thread panicked), logs an
138    /// error and signals shutdown so the server can restart cleanly.
139    pub fn register(&mut self, registration: ReaderRegistration<R>) {
140        if self.tx.send(registration).is_ok() {
141            // Signal the eventfd to wake the reader from io_uring_enter.
142            let val: u64 = 1;
143            unsafe {
144                libc::write(
145                    self.event_fd.as_raw_fd(),
146                    &val as *const u64 as *const libc::c_void,
147                    8,
148                );
149            }
150        } else {
151            error!("reader thread dead, cannot register connection");
152            self.shutdown.store(true, Ordering::Relaxed);
153        }
154    }
155
156    /// Signal the reader thread to shut down and wake it from io_uring_enter.
157    pub fn shutdown(&self) {
158        self.shutdown.store(true, Ordering::Relaxed);
159        let val: u64 = 1;
160        unsafe {
161            libc::write(
162                self.event_fd.as_raw_fd(),
163                &val as *const u64 as *const libc::c_void,
164                8,
165            );
166        }
167    }
168
169    /// Join the reader thread. Call after `shutdown()`.
170    pub fn join(mut self) {
171        if let Some(handle) = self.join_handle.take()
172            && let Err(panic) = handle.join()
173        {
174            let msg = panic
175                .downcast_ref::<&str>()
176                .copied()
177                .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
178                .unwrap_or("<non-string panic>");
179            error!(message = msg, "reader thread panicked");
180        }
181    }
182}
183
184/// Spawn the io_uring reader thread. Returns a handle for registering
185/// connections.
186///
187/// One reader thread serves every TCP connection on the server. io_uring
188/// with multishot RECV multiplexes thousands of sockets efficiently and the
189/// matching stage is the throughput limit, so adding more reader threads
190/// would not raise throughput — it would only re-introduce contention on
191/// the input ring's multi-producer cursor.
192///
193/// `tick_cadence: Some(d)` makes the reader the engine's tick generator: it
194/// arms an `IORING_OP_TIMEOUT` so `submit_and_wait` returns at the tick
195/// deadline even when no client traffic is flowing, then publishes a
196/// `JournalEvent::Tick { now_ns }` onto the same input ring it uses for
197/// client requests. Pass `None` to disable the tick (useful for benchmarks
198/// that don't exercise time-driven features).
199///
200/// While `halt` refuses writes, the reader answers them through
201/// `refusals` instead of publishing them — see [`crate::halt`].
202#[allow(clippy::too_many_arguments)]
203pub fn spawn_reader<A: Application, R: AsRawFd + Send + 'static>(
204    producer: ring::Producer<InputSlot<A::Event>>,
205    decoder: Arc<dyn RequestDecoder<Event = A::Event>>,
206    halt: HaltGate,
207    refusals: RefusalSender<A::Report>,
208    control_tx: mpsc::Sender<ControlEvent>,
209    core: usize,
210    connection_timeout: Option<Duration>,
211    tick_cadence: Option<Duration>,
212    shutdown: Arc<AtomicBool>,
213) -> UringReaderHandle<R>
214where
215    A::Event: Send + Sync + 'static,
216    A::Report: Send + 'static,
217{
218    let (tx, rx) = mpsc::channel();
219
220    let raw_event_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
221    assert!(raw_event_fd >= 0, "eventfd creation failed");
222
223    // SAFETY: `eventfd` returned a fresh descriptor (asserted non-negative
224    // above) that nothing else owns, so transferring ownership to `OwnedFd`
225    // is sound. From here the raw number is never closed by hand — the
226    // descriptor lives exactly as long as the last `Arc` holder.
227    let event_fd = Arc::new(unsafe { OwnedFd::from_raw_fd(raw_event_fd) });
228    let wakeup_fd = Arc::clone(&event_fd);
229    let shutdown_clone = Arc::clone(&shutdown);
230
231    let handle = std::thread::Builder::new()
232        .name("uring-reader".into())
233        .spawn(move || {
234            // `core == 0` is the unpinned sentinel, which puts the thread
235            // on the process's home CPU mask — see the `melin_app::affinity`
236            // module docs.
237            melin_app::affinity::pin_thread("uring-reader", core);
238            reader_loop::<A, R>(
239                rx,
240                wakeup_fd,
241                producer,
242                &*decoder,
243                &halt,
244                refusals,
245                &control_tx,
246                connection_timeout,
247                tick_cadence,
248                &shutdown_clone,
249            );
250        })
251        .expect("failed to spawn uring reader thread");
252
253    UringReaderHandle {
254        tx,
255        event_fd,
256        join_handle: Some(handle),
257        shutdown,
258    }
259}
260
261// ---------------------------------------------------------------------------
262// Slab-based connection storage
263// ---------------------------------------------------------------------------
264
265/// Per-connection state for multishot io_uring recv + incremental frame parsing.
266struct ConnectionEntry<R> {
267    connection_id: u64,
268    addr: SocketAddr,
269    /// Permission level from auth handshake. Checked per-request on
270    /// the reader thread (cold path), zero cost on the matching engine.
271    permission: Permission,
272    /// FxHash of the client's Ed25519 public key. Copied into every
273    /// InputSlot for per-key idempotency dedup.
274    key_hash: u64,
275    /// Owned reader — keeps the fd alive. Dropping closes the fd.
276    _reader: R,
277    fd: RawFd,
278    /// Accumulated bytes not yet parsed into complete frames.
279    /// Grows when partial frames arrive, shrinks when frames are consumed.
280    parse_buf: Vec<u8>,
281    /// True if a multishot RecvMulti is currently active for this connection.
282    /// Multishot stays active until the kernel clears IORING_CQE_F_MORE
283    /// (e.g., buffer pool exhaustion, socket error, or EOF).
284    multishot_active: bool,
285    /// Last time any data was received on this connection. Used for
286    /// idle timeout detection.
287    last_activity: Instant,
288    /// Teardown has begun (malformed frame or idle timeout) while the
289    /// multishot RECV was still armed. The slab index must stay
290    /// allocated until the armed op's terminal CQE arrives — freeing it
291    /// early lets the LIFO free list hand the index to a new
292    /// registration while the kernel can still post CQEs carrying it,
293    /// and the old peer's bytes would be parsed under the new
294    /// connection's identity and permissions (audit review F1). CQEs
295    /// for a dying entry recycle their buffers and are otherwise
296    /// discarded.
297    dying: bool,
298}
299
300/// Index-stable allocator for connection state. Slab indices are used as
301/// io_uring user_data, avoiding fd-reuse races.
302struct ConnectionSlab<R> {
303    entries: Vec<Option<ConnectionEntry<R>>>,
304    /// Recycled indices for O(1) allocation.
305    free: Vec<usize>,
306}
307
308impl<R> ConnectionSlab<R> {
309    fn new() -> Self {
310        Self {
311            entries: Vec::with_capacity(256),
312            free: Vec::new(),
313        }
314    }
315
316    /// Insert a connection, returning its stable slab index.
317    fn insert(&mut self, entry: ConnectionEntry<R>) -> usize {
318        if let Some(idx) = self.free.pop() {
319            self.entries[idx] = Some(entry);
320            idx
321        } else {
322            let idx = self.entries.len();
323            self.entries.push(Some(entry));
324            idx
325        }
326    }
327
328    fn get_mut(&mut self, idx: usize) -> Option<&mut ConnectionEntry<R>> {
329        self.entries.get_mut(idx).and_then(|e| e.as_mut())
330    }
331
332    /// Remove and return a connection entry, recycling its index.
333    fn remove(&mut self, idx: usize) -> Option<ConnectionEntry<R>> {
334        if let Some(slot) = self.entries.get_mut(idx) {
335            let removed = slot.take();
336            if removed.is_some() {
337                self.free.push(idx);
338            }
339            removed
340        } else {
341            None
342        }
343    }
344}
345
346// ---------------------------------------------------------------------------
347// Main loop
348// ---------------------------------------------------------------------------
349
350/// How the reader should approach the kernel before draining the CQ.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352enum RingEntry {
353    /// Completions are visible and there is nothing to hand over — drain
354    /// from the mmap'd ring without a syscall.
355    Skip,
356    /// SQEs to submit, but completions are already visible: hand them
357    /// over and come straight back rather than asking to wait.
358    Submit,
359    /// Nothing to process yet — submit whatever is queued and park.
360    SubmitAndWait,
361}
362
363/// The reader's enter policy. Two rules: never park while completions
364/// are already visible, and never skip the syscall while SQEs are
365/// waiting to be handed over.
366#[inline]
367fn ring_entry(sq_pending: usize, cq_ready: bool) -> RingEntry {
368    match (sq_pending > 0, cq_ready) {
369        (false, true) => RingEntry::Skip,
370        (true, true) => RingEntry::Submit,
371        (_, false) => RingEntry::SubmitAndWait,
372    }
373}
374
375/// Main io_uring reader loop. Runs until channel disconnection.
376///
377/// When `tick_cadence` is `Some`, the loop also generates the engine's
378/// scheduler ticks — see [`spawn_reader`] for the rationale.
379#[allow(clippy::too_many_arguments)]
380fn reader_loop<A: Application, R: AsRawFd>(
381    command_rx: mpsc::Receiver<ReaderRegistration<R>>,
382    // Shared with `UringReaderHandle`. Taken by value so this thread keeps
383    // the descriptor alive for as long as it is armed in the ring, and
384    // releases it by dropping the `Arc` rather than by closing the fd.
385    wakeup_fd: Arc<OwnedFd>,
386    mut producer: ring::Producer<InputSlot<A::Event>>,
387    decoder: &dyn RequestDecoder<Event = A::Event>,
388    halt: &HaltGate,
389    mut refusals: RefusalSender<A::Report>,
390    control_tx: &mpsc::Sender<ControlEvent>,
391    connection_timeout: Option<Duration>,
392    tick_cadence: Option<Duration>,
393    shutdown: &AtomicBool,
394) {
395    // Kernel-referenced memory is declared BEFORE the io_uring so it
396    // drops AFTER the ring on every exit path, including panic unwind
397    // (locals drop in reverse declaration order). The kernel holds live
398    // references into all three for as long as the ring fd is open:
399    // armed multishot RECVs select entries from the buf_ring and write
400    // into the pool, and the armed eventfd READ writes its buffer.
401
402    // Eventfd read buffer — boxed for pointer stability across SQE lifetimes.
403    let mut eventfd_buf: Box<[u8; 8]> = Box::new([0u8; 8]);
404
405    // Shared buffer pool for provided buffers. Contiguous allocation of
406    // NUM_BUFFERS × BUF_SIZE bytes. The kernel selects a buffer from this
407    // pool for each recv completion, identified by buffer ID in the CQE.
408    let mut buffer_pool = vec![0u8; NUM_BUFFERS as usize * BUF_SIZE].into_boxed_slice();
409
410    // Ring-mapped provided-buffer ring: recycling a consumed buffer is a
411    // shared-memory store, not a ProvideBuffers SQE — see `buf_ring`.
412    // Allocated unconditionally (32 KiB) so its declaration precedes the
413    // io_uring's even when the legacy fallback below ends up in use.
414    let mut buf_ring = BufRing::new(NUM_BUFFERS, buffer_pool.as_mut_ptr(), BUF_SIZE);
415
416    // SINGLE_ISSUER: this thread creates the ring and is the only one
417    // that ever submits — lets the kernel skip SQ locking, and turns any
418    // future cross-thread submission bug into an immediate EEXIST
419    // instead of a silent race. Same rationale as the journal and
420    // replication rings. (COOP_TASKRUN/DEFER_TASKRUN deliberately not
421    // set — see the journal ring's measured rationale in
422    // melin-transport-core::pipeline.)
423    let mut ring: IoUring = IoUring::builder()
424        .setup_single_issuer()
425        .build(RING_SIZE)
426        .expect("failed to create io_uring instance");
427
428    // Prefer the buf_ring; fall back to legacy ProvideBuffers SQEs if
429    // the kernel rejects the registration. Not theoretical: PBUF_RING
430    // needs kernel ≥ 5.19, and some virtualized hosts filter newer
431    // io_uring register opcodes (EINVAL) while reporting a modern
432    // uname. The fallback costs one SQE + one CQE per received chunk —
433    // degraded but fully functional, hence warn (see log conventions).
434    let use_buf_ring = match buf_ring.register(&ring, BUF_GROUP_ID) {
435        Ok(()) => true,
436        Err(e) => {
437            tracing::warn!(
438                error = %e,
439                "buf_ring registration rejected — falling back to legacy \
440                 ProvideBuffers recycling (kernel < 5.19, or a hypervisor \
441                 filtering io_uring register opcodes)"
442            );
443            register_buffer_pool(&mut ring, buffer_pool.as_mut_ptr());
444            false
445        }
446    };
447
448    let mut slab = ConnectionSlab::<R>::new();
449    // Reverse map for cleanup when a connection's fd needs removal.
450    // HashMap for O(1) lookup by fd. Sized for typical connection counts.
451    let mut fd_to_slab: HashMap<RawFd, usize> = HashMap::with_capacity(256);
452
453    // Pre-allocated CQE collection buffer. We must collect CQEs before
454    // processing because the CQ borrow must end before pushing new SQEs.
455    // Stores (user_data, result, flags) — flags needed for buffer ID and
456    // multishot continuation. Sized to the CQ depth (2× the SQ) so even
457    // a maximal drain never reallocates mid-loop.
458    let mut cqes: Vec<(u64, i32, u32)> = Vec::with_capacity(RING_SIZE as usize * 2);
459
460    // Submit the initial eventfd read so we wake on first connection.
461    // `eventfd_armed` mirrors whether that READ is pushed/armed — the
462    // kernel owes its CQE and writes `eventfd_buf` when it lands — and
463    // is maintained at every push/consume site so the teardown quiesce
464    // below can prove when the buffer is free of kernel references.
465    push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
466    let mut eventfd_armed = true;
467
468    // Stage histograms via the global registry. `publish` is the
469    // narrow ring-publish call cost (lightweight, gated on
470    // `latency-trace`); `ingest` is the full per-frame reader cost
471    // and feeds the bench's tick-to-trade decomposition (heavier,
472    // gated on `tick-to-trade`).
473    #[cfg(feature = "latency-trace")]
474    let mut publish_rec =
475        melin_transport_core::trace::register_stage("reader: publish (decode → disruptor publish)");
476    #[cfg(feature = "tick-to-trade")]
477    let mut ingest_rec =
478        melin_transport_core::trace::register_stage("reader: ingest (recv_ts → publish complete)");
479    // Paces the recorder flush at the tail of each loop iteration. This
480    // thread parks in `submit_and_wait` when there is no traffic, so
481    // without an explicit flush its samples never reach the registry.
482    #[cfg(feature = "latency-trace")]
483    let mut last_stats_flush = Instant::now();
484
485    // Coarse gate for timeout scanning — avoids scanning on every
486    // submit_and_wait return during high throughput.
487    let mut last_timeout_scan = Instant::now();
488    // Pre-allocated buffer for stale connection indices to avoid
489    // heap allocation inside the hot loop.
490    let mut stale: Vec<usize> = Vec::new();
491
492    // Tick generator state. `next_tick_deadline` is the monotonic instant the
493    // next `JournalEvent::Tick` should fire. `last_tick_ns` enforces strict
494    // monotonicity on the wall-clock timestamps published in those events
495    // (NTP can step the wall clock backwards). `tick_armed` tracks whether
496    // an `IORING_OP_TIMEOUT` SQE is currently pending; we keep at most one.
497    //
498    // `tick_ts` lives across loop iterations because the kernel reads its
499    // bytes via the SQE's addr field at submit time, not at push time. If
500    // we declared it inside the `if !tick_armed` arm-timeout block, the
501    // value would be dropped before the `submit_and_wait` below — the
502    // kernel would then read freed stack memory. (See `md-gateway` for the
503    // same pattern: it stores Timespec as a long-lived struct field.)
504    let tick_enabled = tick_cadence.is_some();
505    let cadence = tick_cadence.unwrap_or(Duration::ZERO);
506    let mut next_tick_deadline = Instant::now() + cadence;
507    let mut last_tick_ns: u64 = 0;
508    let mut tick_armed = false;
509    // Arm the very first timeout here, before entering the loop. This both
510    // (a) makes the initial `tick_ts` value actually read by the kernel
511    // (silencing the unused-assignment lint, since rustc cannot see kernel
512    // pointer reads) and (b) ensures the first `submit_and_wait` returns at
513    // the cadence even if no client traffic ever arrives.
514    let mut tick_ts = types::Timespec::new()
515        .sec(cadence.as_secs())
516        .nsec(cadence.subsec_nanos());
517    if tick_enabled {
518        let sqe = opcode::Timeout::new(&tick_ts)
519            .build()
520            .user_data(TICK_TIMEOUT_TOKEN);
521        unsafe {
522            ring.submission()
523                .push(&sqe)
524                .expect("io_uring SQ full while arming initial tick timeout");
525        }
526        tick_armed = true;
527        tracing::info!(
528            cadence_ms = cadence.as_millis() as u64,
529            "tick generator integrated into reader thread"
530        );
531    }
532
533    loop {
534        if shutdown.load(Ordering::Relaxed) {
535            break;
536        }
537
538        // Tick generator: emit any due tick before sleeping again. Done before
539        // the timeout-arm so that a freshly-emitted tick re-arms a timeout for
540        // the *new* deadline.
541        if tick_enabled {
542            let now = Instant::now();
543            if now >= next_tick_deadline {
544                let raw_now_ns = unix_epoch_nanos();
545                let now_ns = melin_transport_core::tick::clamp_monotonic(raw_now_ns, last_tick_ns);
546                last_tick_ns = now_ns;
547                melin_transport_core::tick::publish_tick(&mut producer, now_ns);
548                // Catch up rather than burst-emit if we fell badly behind.
549                let elapsed = Instant::now().saturating_duration_since(next_tick_deadline);
550                next_tick_deadline = if elapsed > cadence {
551                    Instant::now() + cadence
552                } else {
553                    next_tick_deadline + cadence
554                };
555                // The previous timeout (if any) is now stale; let it fire and
556                // be ignored, then arm a new one below.
557                tick_armed = false;
558            }
559
560            if !tick_armed {
561                let remaining = next_tick_deadline.saturating_duration_since(Instant::now());
562                // Update the loop-scoped Timespec in place. The kernel reads
563                // it via the SQE's addr pointer on submit_and_wait below, so
564                // the binding must outlive that call (it does — outer scope).
565                tick_ts = types::Timespec::new()
566                    .sec(remaining.as_secs())
567                    .nsec(remaining.subsec_nanos());
568                let sqe = opcode::Timeout::new(&tick_ts)
569                    .build()
570                    .user_data(TICK_TIMEOUT_TOKEN);
571                unsafe {
572                    ring.submission()
573                        .push(&sqe)
574                        .expect("io_uring SQ full while arming tick timeout");
575                }
576                tick_armed = true;
577            }
578        }
579
580        // Enter the kernel only when there is a reason to.
581        //
582        // Under load the CQ usually already holds completions by the
583        // time we get here: in the ring's default task-work mode the
584        // kernel posts them while this thread is in userspace parsing.
585        // Reading that is pure userspace — the CQ is mmap'd and
586        // `completion()` loads the tail with `Acquire` — so when there
587        // is also nothing to hand over, the `io_uring_enter` is a
588        // ~200 ns mode switch that buys nothing. The replication sender
589        // measured the same skip at ~6 % of its thread's cycles
590        // (`tcp_sender.rs`); this loop makes one enter per drain.
591        //
592        // When there ARE SQEs to submit we still enter, but we do not
593        // ask the kernel to *wait* if completions are already visible.
594        // Only a genuinely empty CQ parks the thread.
595        //
596        // Note this is the half of the io_uring-audit item that pairs
597        // *against* `DEFER_TASKRUN`: deferred task work only runs on an
598        // enter with GETEVENTS, so under that flag the CQ would be empty
599        // here and the skip would simply never fire.
600        let sq_pending = ring.submission().len();
601        let cq_ready = !ring.completion().is_empty();
602        let entered = match ring_entry(sq_pending, cq_ready) {
603            RingEntry::Skip => Ok(0),
604            RingEntry::Submit => ring.submit(),
605            RingEntry::SubmitAndWait => ring.submit_and_wait(1),
606        };
607        match entered {
608            Ok(_) => {}
609            Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
610            Err(e) => {
611                error!(error = %e, "io_uring submit/wait error");
612                break;
613            }
614        }
615
616        // Drain all available CQEs into the pre-allocated buffer.
617        // Must collect before processing because the CQ borrow must end
618        // before we can push new SQEs to the SQ.
619        cqes.clear();
620        cqes.extend(
621            ring.completion()
622                .map(|cqe| (cqe.user_data(), cqe.result(), cqe.flags())),
623        );
624
625        let batch_now = Instant::now();
626        // One wall-clock read per CQE batch instead of per request. The
627        // reader can see 4–6 M requests/s at peak; a per-request
628        // `unix_epoch_nanos()` was ~2.8 % of the primary's cycles
629        // (vDSO `clock_gettime(CLOCK_REALTIME)`). All requests in the
630        // same batch share the timestamp — precision loss is bounded
631        // by the CQE-drain cadence (tens of µs under load) and order
632        // timestamps are used for reporting, not matching (the engine
633        // orders by sequence, not time).
634        let batch_wall_ns = unix_epoch_nanos();
635
636        for &(token, result, flags) in &cqes {
637            // ── Tick timeout ──
638            // The CQE is just a wakeup signal — the actual tick emission
639            // happens at the top of the next loop iteration via the
640            // deadline check, so the time the tick is stamped with reflects
641            // unix_epoch_nanos at fire time, not at submit time.
642            if token == TICK_TIMEOUT_TOKEN {
643                tick_armed = false;
644                continue;
645            }
646
647            // ── Legacy ProvideBuffers completion (fallback mode only) ──
648            if token == PROVIDE_BUFS_TOKEN {
649                if result < 0 {
650                    error!(error = result, "ProvideBuffers failed");
651                }
652                continue;
653            }
654
655            // ── AsyncCancel completion ──
656            if token == CANCEL_TOKEN {
657                // Nothing to do: the cancelled op's own terminal CQE
658                // drives the state machine; ENOENT/EALREADY just mean
659                // it already finished on its own.
660                continue;
661            }
662
663            // ── Eventfd wakeup ──
664            if token == EVENTFD_TOKEN {
665                // This CQE is the armed READ completing; the re-arm at
666                // the end of the branch replaces it before anything can
667                // observe the gap, so `eventfd_armed` stays `true`
668                // through the pair. The teardown drain clears it for
669                // the one case where a completion is never processed
670                // here (a CQE still queued when the loop breaks).
671                if result >= 0 {
672                    // Process all pending registrations.
673                    while let Ok(reg) = command_rx.try_recv() {
674                        let fd = reg.reader.as_raw_fd();
675                        let entry = ConnectionEntry {
676                            connection_id: reg.connection_id.0,
677                            addr: reg.addr,
678                            permission: reg.permission,
679                            key_hash: reg.key_hash,
680                            fd,
681                            _reader: reg.reader,
682                            parse_buf: Vec::with_capacity(MAX_FRAME_SIZE + 4),
683                            multishot_active: false,
684                            last_activity: Instant::now(),
685                            dying: false,
686                        };
687                        let idx = slab.insert(entry);
688                        fd_to_slab.insert(fd, idx);
689
690                        // Submit multishot RECV for this connection.
691                        push_recv_multi(&mut ring, &mut slab, idx);
692                    }
693                } else {
694                    error!(error = result, "eventfd read error");
695                }
696
697                // Re-submit eventfd read for the next wakeup
698                // (`eventfd_armed` stays `true` — see above).
699                push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
700                continue;
701            }
702
703            // ── Connection multishot RECV completion ──
704
705            let slab_idx = token as usize;
706            let has_more = (flags & IORING_CQE_F_MORE) != 0;
707
708            // A cleared F_MORE means THIS armed op posts no further
709            // CQEs — record it before any branch, so no early `continue`
710            // below can wedge the connection with a stale `true`.
711            if !has_more && let Some(entry) = slab.get_mut(slab_idx) {
712                entry.multishot_active = false;
713            }
714
715            // Extract the buffer (if any) before acting on `result`: a
716            // buffer can ride ANY recv CQE, and one that is not recycled
717            // is leaked from the pool forever. On 6.8 the kernel
718            // recycles internally before posting error/EOF CQEs, but the
719            // supported floor is 5.19, where that guarantee could not be
720            // established — the defensive recycle is one branch.
721            let buf_id = if (flags & IORING_CQE_F_BUFFER) != 0 {
722                Some((flags >> IORING_CQE_BUFFER_SHIFT) as usize)
723            } else {
724                None
725            };
726
727            // Dying connection: teardown began while its multishot was
728            // still armed (see `begin_teardown`). Consume its CQEs
729            // without parsing — the bytes belong to a repudiated peer —
730            // but keep recycling their buffers; free the slab index only
731            // at the terminal CQE, so it cannot be handed to a new
732            // registration while the kernel can still post CQEs carrying
733            // it. Checked before the disconnect branch so the cancel's
734            // -ECANCELED completion lands here, not there (which would
735            // emit a second Disconnected event).
736            if slab.get_mut(slab_idx).is_some_and(|e| e.dying) {
737                if let Some(bid) = buf_id {
738                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
739                }
740                if !has_more && let Some(dead) = slab.remove(slab_idx) {
741                    debug!(
742                        connection_id = dead.connection_id,
743                        "teardown complete, slab index released"
744                    );
745                }
746                continue;
747            }
748
749            if result == -libc::ENOBUFS {
750                // Pool exhausted at buffer-selection time: the multishot
751                // terminated WITHOUT reading — no data was lost, it sits
752                // in the socket buffer. This is not a client error and
753                // must not disconnect (a bare `result <= 0 ⇒ remove`
754                // here dropped innocent clients under burst). Re-arm:
755                // recycles from data CQEs earlier in this drain have
756                // already refilled the buf_ring (publication is
757                // immediate), and the re-arm SQE submits after the whole
758                // drain, so the retry finds buffers.
759                if let Some(bid) = buf_id {
760                    // No buffer accompanies a selection failure by
761                    // definition; recycle defensively if one appears.
762                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
763                }
764                if has_more {
765                    // Future-kernel guard: should the kernel ever keep
766                    // the multishot alive across ENOBUFS, arming a second
767                    // one would interleave two delivery streams into the
768                    // same parse buffer.
769                    continue;
770                }
771                push_recv_multi(&mut ring, &mut slab, slab_idx);
772                continue;
773            }
774
775            if result <= 0 {
776                // Disconnect (0) or error (negative errno) — terminal
777                // for the armed op, so the index is immediately safe to
778                // free. Recycle any buffer riding the CQE first.
779                if let Some(bid) = buf_id {
780                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
781                }
782                if let Some(removed) = slab.remove(slab_idx) {
783                    if result == 0 {
784                        debug!(
785                            connection_id = removed.connection_id,
786                            addr = %removed.addr,
787                            "client disconnected"
788                        );
789                    } else {
790                        debug!(
791                            connection_id = removed.connection_id,
792                            addr = %removed.addr,
793                            error = result,
794                            "recv error"
795                        );
796                    }
797                    fd_to_slab.remove(&removed.fd);
798                    let _ = control_tx.send(ControlEvent::Disconnected {
799                        connection_id: removed.connection_id,
800                    });
801                }
802                continue;
803            }
804
805            let n = result as usize;
806
807            // Trace timestamp: the moment the kernel handed us this recv's
808            // bytes. Captured once per CQE — not per frame — and stamped
809            // onto every InputSlot parsed from this buffer below, so the
810            // reader-ingest and server-e2e stages measure from true wire
811            // receipt (frame decode included) rather than re-sampling after
812            // each decode (which excluded decode and drifted forward for
813            // later frames in a multi-frame recv).
814            #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
815            let recv_ts = melin_transport_core::trace::mono_trace_ns();
816
817            // Data CQEs always carry a buffer with buffer-select recvs.
818            // Defensive: nothing to copy or recycle without one, but the
819            // multishot bookkeeping already ran above, so a terminal
820            // no-buffer CQE re-arms instead of wedging the connection.
821            let Some(buf_id) = buf_id else {
822                debug!(slab_idx, "recv CQE without buffer flag");
823                if !has_more {
824                    push_recv_multi(&mut ring, &mut slab, slab_idx);
825                }
826                continue;
827            };
828
829            // Feed received bytes into the frame parser from the shared pool.
830            let action = if let Some(entry) = slab.get_mut(slab_idx) {
831                // Any successful recv resets the idle timeout.
832                entry.last_activity = batch_now;
833
834                // Copy received data from the shared buffer pool into the
835                // connection's parse buffer.
836                let buf_start = buf_id * BUF_SIZE;
837                entry
838                    .parse_buf
839                    .extend_from_slice(&buffer_pool[buf_start..buf_start + n]);
840
841                // Extract and publish complete frames.
842                let drop_conn = process_frames::<A, R>(
843                    entry,
844                    &mut producer,
845                    decoder,
846                    halt,
847                    &mut refusals,
848                    control_tx,
849                    batch_wall_ns,
850                    recv_ts,
851                    #[cfg(feature = "latency-trace")]
852                    &mut publish_rec,
853                    #[cfg(feature = "tick-to-trade")]
854                    &mut ingest_rec,
855                );
856                if drop_conn {
857                    Action::Remove
858                } else if !has_more {
859                    // Multishot terminated (buffer pool exhaustion or kernel
860                    // decision) but connection is healthy — resubmit.
861                    Action::Resubmit
862                } else {
863                    Action::None
864                }
865            } else {
866                // Stale CQE for a removed connection — ignore.
867                Action::None
868            };
869
870            // Recycle the consumed buffer. Must happen after the copy-out
871            // above — from this line on the kernel may write fresh recv
872            // data into the slot.
873            recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, buf_id);
874
875            match action {
876                Action::Remove => {
877                    // Deferred teardown, NOT an immediate slab free — the
878                    // multishot may still be armed and its index must not
879                    // be reused until the terminal CQE (audit review F1).
880                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, slab_idx);
881                }
882                Action::Resubmit => {
883                    push_recv_multi(&mut ring, &mut slab, slab_idx);
884                }
885                Action::None => {}
886            }
887        }
888
889        // Scan for idle connections that have exceeded the timeout.
890        // Coarse gate: only scan once per second to avoid unnecessary
891        // iteration during high-throughput phases when submit_and_wait
892        // returns immediately with CQEs.
893        if let Some(timeout) = connection_timeout {
894            let now = Instant::now();
895            if now.duration_since(last_timeout_scan) >= Duration::from_secs(1) {
896                last_timeout_scan = now;
897                stale.clear();
898                for (idx, slot) in slab.entries.iter().enumerate() {
899                    // Dying entries are already mid-teardown: their
900                    // `last_activity` stays stale by design, and a second
901                    // teardown would emit a duplicate Disconnected.
902                    if let Some(entry) = slot
903                        && !entry.dying
904                        && now.duration_since(entry.last_activity) > timeout
905                    {
906                        debug!(
907                            connection_id = entry.connection_id,
908                            addr = %entry.addr,
909                            "connection timed out"
910                        );
911                        stale.push(idx);
912                    }
913                }
914                for &idx in &stale {
915                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
916                }
917            }
918        }
919
920        // Hand buffered latency samples to the stats registry before
921        // parking in `submit_and_wait` at the top of the next
922        // iteration — once traffic stops this thread can sleep in the
923        // kernel indefinitely, which is precisely when the bench
924        // scrapes /stats-dump. Reuses the per-batch clock read.
925        #[cfg(feature = "latency-trace")]
926        if batch_now.duration_since(last_stats_flush)
927            >= melin_transport_core::trace::IDLE_FLUSH_INTERVAL
928        {
929            last_stats_flush = batch_now;
930            publish_rec.flush();
931            #[cfg(feature = "tick-to-trade")]
932            ingest_rec.flush();
933        }
934    }
935
936    // ── Teardown quiesce ────────────────────────────────────────────
937    // Armed operations hold kernel pointers into the buffer pool, the
938    // buf_ring memory, and the eventfd buffer. Closing the ring fd only
939    // triggers ASYNCHRONOUS cancellation (the kernel's ring-exit work),
940    // which can still be touching those allocations after this function
941    // returns and frees them — the same corruption class fixed on both
942    // replication transports (see `crate::uring_teardown`). Quiesce
943    // *provably*: wake every armed operation, then reap CQEs until none
944    // is owed, and leak the buffers if that cannot be shown within the
945    // deadline. "CQ went quiet" is not proof — a wake that silently
946    // failed leaves armed ops and a quiet CQ.
947    //
948    // The wake cannot rely on the cancel opcode alone: some hosts
949    // filter io_uring opcodes (the reason the buf_ring path has a
950    // legacy fallback), and a filtered cancel completes with EINVAL
951    // while the armed operations live on. So every operation that has
952    // to be proven down gets a wake it must answer: `shutdown(2)` on
953    // each connection socket completes its multishot RECV with EOF,
954    // and a wakeup write completes the armed eventfd READ. The cancel
955    // is still pushed as an accelerator where the host honours it. The
956    // tick timeout is not in the proof at all — see `owed` below.
957    // Panic unwind skips all of this and accepts the (tiny,
958    // process-is-dying) exit-work window; declaration order still
959    // closes the ring fd before the frees.
960    let proven = {
961        let cancel_all = opcode::AsyncCancel2::new(types::CancelBuilder::any())
962            .build()
963            .user_data(CANCEL_TOKEN);
964        unsafe {
965            // Ignore a full SQ: the wakes below do not depend on it.
966            let _ = ring.submission().push(&cancel_all);
967        }
968        for entry in slab.entries.iter().flatten() {
969            crate::uring_teardown::wake_pending_ops(entry.fd);
970        }
971        // Completes the armed eventfd READ into `eventfd_buf` (still
972        // alive). Best-effort: on failure the cancel is the fallback.
973        let wake: u64 = 1;
974        unsafe {
975            libc::write(
976                wakeup_fd.as_raw_fd(),
977                &wake as *const u64 as *const libc::c_void,
978                8,
979            );
980        }
981        // Flush SQEs pushed but never submitted (re-arms from the last
982        // batch, the cancel above): until they reach the kernel no CQE
983        // is coming for them, but their armed-state flags are already
984        // set, so abandoning them in the SQ would stall the proof
985        // below. On a submit error the flags stay set and the deadline
986        // routes to the leak path — the conservative outcome.
987        loop {
988            match ring.submit() {
989                Ok(_) => break,
990                Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
991                Err(_) => break,
992            }
993        }
994        let mut backoff = crate::uring_teardown::DrainBackoff::new();
995        loop {
996            for cqe in ring.completion() {
997                match cqe.user_data() {
998                    EVENTFD_TOKEN => eventfd_armed = false,
999                    // None of these touch the allocations freed below,
1000                    // so reaping them settles nothing: cancel results
1001                    // carry no information, legacy ProvideBuffers
1002                    // completions only registered addresses, and the
1003                    // tick timeout reads nothing after submit (see
1004                    // `owed`).
1005                    PROVIDE_BUFS_TOKEN | CANCEL_TOKEN | TICK_TIMEOUT_TOKEN => {}
1006                    // Connection tokens are slab indices.
1007                    idx => {
1008                        // A terminal CQE retires that connection's arm.
1009                        // Data CQEs landing mid-teardown are discarded
1010                        // and their buffers deliberately not recycled —
1011                        // nothing re-arms, so the pool cannot be
1012                        // selected from again once every arm is down.
1013                        if (cqe.flags() & IORING_CQE_F_MORE) == 0
1014                            && let Some(entry) = slab.get_mut(idx as usize)
1015                        {
1016                            entry.multishot_active = false;
1017                        }
1018                    }
1019                }
1020            }
1021            // `tick_armed` is deliberately absent: an armed
1022            // `IORING_OP_TIMEOUT` holds no pointer into any of the
1023            // allocations below. The kernel copies its `Timespec` out
1024            // of `tick_ts` when the SQE is *submitted* (which is why
1025            // that binding only has to outlive the submit — see its
1026            // declaration), and the ring's own exit work retires the
1027            // timer. Waiting for it would prove nothing and cost
1028            // plenty: nothing here can wake a timeout, so the only
1029            // things that retire one are it firing at its own cadence
1030            // — `--tick-interval-ms`, operator-set and unbounded — or
1031            // the cancel above, which is exactly what a host that
1032            // filters opcodes refuses. Gating the proof on it would
1033            // hand those hosts a full-deadline stall and a spurious
1034            // "leaking" warning on every clean shutdown.
1035            let owed = eventfd_armed || slab.entries.iter().flatten().any(|e| e.multishot_active);
1036            if !owed {
1037                break true;
1038            }
1039            if !backoff.wait() {
1040                break false;
1041            }
1042        }
1043    };
1044    if !proven {
1045        // The kernel may still write the pool or the eventfd buffer, or
1046        // read buf_ring entries. Leak all three rather than hand the
1047        // allocator memory the kernel still touches — a bounded one-off
1048        // cost on a thread that is exiting anyway. `mem::forget` on the
1049        // `BufRing` also skips its ring-memory dealloc, which is the
1050        // point.
1051        // Report what was still owed, not `tick_armed` — naming a
1052        // timer that was never part of the proof would point the next
1053        // reader at the wrong operation.
1054        tracing::warn!(
1055            eventfd_armed,
1056            armed_connections = slab
1057                .entries
1058                .iter()
1059                .flatten()
1060                .filter(|e| e.multishot_active)
1061                .count(),
1062            "io_uring teardown drain did not complete; leaking reader buffers"
1063        );
1064        std::mem::forget(buffer_pool);
1065        std::mem::forget(buf_ring);
1066        std::mem::forget(eventfd_buf);
1067    }
1068
1069    // No `close` here. This thread shares the eventfd with
1070    // `UringReaderHandle`, which writes wakeups to it, and closing it from
1071    // this side left that write racing a reused descriptor number. Dropping
1072    // the `Arc` at the end of this scope releases our share; the descriptor
1073    // itself closes when the handle drops too.
1074}
1075
1076/// What to do after processing a RECV CQE.
1077enum Action {
1078    /// Multishot terminated but connection healthy — resubmit RecvMulti.
1079    Resubmit,
1080    /// Connection should be torn down (malformed frame). Handled via
1081    /// `begin_teardown`, which reads what it needs from the slab entry.
1082    Remove,
1083    /// Multishot still active — nothing to do.
1084    None,
1085}
1086
1087// ---------------------------------------------------------------------------
1088// SQE helpers
1089// ---------------------------------------------------------------------------
1090
1091/// Register the provided buffer pool via a legacy ProvideBuffers op —
1092/// the fallback when buf_ring registration is rejected. Submits
1093/// synchronously and panics on failure — called once at startup, and
1094/// only after the preferred path already failed.
1095fn register_buffer_pool(ring: &mut IoUring, pool_ptr: *mut u8) {
1096    let sqe = opcode::ProvideBuffers::new(pool_ptr, BUF_SIZE as i32, NUM_BUFFERS, BUF_GROUP_ID, 0)
1097        .build()
1098        .user_data(PROVIDE_BUFS_TOKEN);
1099
1100    unsafe {
1101        ring.submission()
1102            .push(&sqe)
1103            .expect("io_uring SQ full during buffer pool registration");
1104    }
1105
1106    ring.submit_and_wait(1)
1107        .expect("io_uring submit failed during buffer pool registration");
1108
1109    // Check the completion result.
1110    let cqe = ring
1111        .completion()
1112        .next()
1113        .expect("no CQE after ProvideBuffers");
1114    assert!(cqe.result() >= 0, "ProvideBuffers failed: {}", cqe.result());
1115}
1116
1117/// Re-provide a single consumed buffer back to the pool (legacy fallback
1118/// mode). Pushed to SQ without immediate submission — batched with the
1119/// next submit_and_wait. Safe against SQ overflow only because at most
1120/// `NUM_BUFFERS` (< `RING_SIZE`) recycles can accumulate per drain.
1121fn re_provide_buffer(ring: &mut IoUring, pool_ptr: *mut u8, buf_id: usize) {
1122    let buf_ptr = unsafe { pool_ptr.add(buf_id * BUF_SIZE) };
1123    let sqe = opcode::ProvideBuffers::new(buf_ptr, BUF_SIZE as i32, 1, BUF_GROUP_ID, buf_id as u16)
1124        .build()
1125        .user_data(PROVIDE_BUFS_TOKEN);
1126
1127    unsafe {
1128        ring.submission()
1129            .push(&sqe)
1130            .expect("io_uring SQ full — increase RING_SIZE");
1131    }
1132}
1133
1134/// Return a consumed buffer to the shared pool, in whichever recycle
1135/// mode registration selected.
1136///
1137/// Hard assert, not debug: the bid comes from a kernel CQE, this path
1138/// runs per CQE (off the per-frame budget), and an out-of-range bid
1139/// handed back to the kernel would point a future recv's DMA at
1140/// arbitrary heap — the one failure mode worth an unconditional branch.
1141fn recycle_buffer(
1142    ring: &mut IoUring,
1143    buf_ring: &mut BufRing,
1144    use_buf_ring: bool,
1145    buffer_pool: &[u8],
1146    buf_id: usize,
1147) {
1148    assert!(
1149        buf_id < NUM_BUFFERS as usize,
1150        "kernel returned out-of-pool buffer id {buf_id}"
1151    );
1152    if use_buf_ring {
1153        buf_ring.push(buf_id as u16);
1154    } else {
1155        re_provide_buffer(ring, buffer_pool.as_ptr() as *mut u8, buf_id);
1156    }
1157}
1158
1159/// Begin tearing down a connection the reader decided to drop while its
1160/// multishot RECV may still be armed (malformed frame, idle timeout).
1161///
1162/// The slab index must NOT be freed yet: the kernel can still post CQEs
1163/// carrying it, and the slab's LIFO free list would hand the index to
1164/// the next registration — the old peer's bytes would then be parsed
1165/// under the new connection's identity, key hash, and permissions
1166/// (order-flow injection; audit review F1). Instead: sever the peer,
1167/// mark the entry dying, cancel the armed op, and let the terminal CQE
1168/// free the index (the `dying` branch of the CQE loop). When no op is
1169/// armed there is nothing that can post — free immediately.
1170fn begin_teardown<R>(
1171    ring: &mut IoUring,
1172    slab: &mut ConnectionSlab<R>,
1173    fd_to_slab: &mut HashMap<RawFd, usize>,
1174    control_tx: &mpsc::Sender<ControlEvent>,
1175    idx: usize,
1176) {
1177    let Some(entry) = slab.get_mut(idx) else {
1178        return;
1179    };
1180    if entry.dying {
1181        return;
1182    }
1183    let connection_id = entry.connection_id;
1184    let fd = entry.fd;
1185    // Sever the peer now. The fd itself must stay open until the armed
1186    // op completes (the kernel holds a file reference for it anyway);
1187    // shutdown(2) stops both directions immediately. Best-effort:
1188    // ENOTCONN just means the peer already went away.
1189    unsafe {
1190        libc::shutdown(fd, libc::SHUT_RDWR);
1191    }
1192    fd_to_slab.remove(&fd);
1193    // Dropped error: a dead control channel means the response stage is
1194    // gone and the server is shutting down.
1195    let _ = control_tx.send(ControlEvent::Disconnected { connection_id });
1196
1197    if entry.multishot_active {
1198        entry.dying = true;
1199        let sqe = opcode::AsyncCancel::new(idx as u64)
1200            .build()
1201            .user_data(CANCEL_TOKEN);
1202        unsafe {
1203            ring.submission()
1204                .push(&sqe)
1205                .expect("io_uring SQ full — increase RING_SIZE");
1206        }
1207    } else {
1208        // No armed op ⇒ no future CQEs can carry this index.
1209        slab.remove(idx);
1210    }
1211}
1212
1213/// Push a multishot RECV SQE for a connection. The kernel will produce
1214/// CQEs continuously until EOF, error, or buffer pool exhaustion —
1215/// no resubmission needed unless multishot terminates.
1216fn push_recv_multi<R>(ring: &mut IoUring, slab: &mut ConnectionSlab<R>, idx: usize) {
1217    let entry = match slab.get_mut(idx) {
1218        Some(e) => e,
1219        None => return,
1220    };
1221
1222    if entry.multishot_active {
1223        return;
1224    }
1225
1226    let sqe = opcode::RecvMulti::new(types::Fd(entry.fd), BUF_GROUP_ID)
1227        .build()
1228        .user_data(idx as u64);
1229
1230    unsafe {
1231        ring.submission()
1232            .push(&sqe)
1233            .expect("io_uring SQ full — increase RING_SIZE");
1234    }
1235    entry.multishot_active = true;
1236}
1237
1238/// Push a READ SQE for the eventfd (wakeup notification).
1239fn push_eventfd_read(ring: &mut IoUring, wakeup_fd: RawFd, buf: *mut u8) {
1240    let sqe = opcode::Read::new(types::Fd(wakeup_fd), buf, 8)
1241        .build()
1242        .user_data(EVENTFD_TOKEN);
1243
1244    unsafe {
1245        ring.submission()
1246            .push(&sqe)
1247            .expect("io_uring SQ full — increase RING_SIZE");
1248    }
1249}
1250
1251// ---------------------------------------------------------------------------
1252// Frame parsing
1253// ---------------------------------------------------------------------------
1254
1255/// Extract complete frames from the connection's parse buffer, decode them,
1256/// and publish to the disruptor. Returns `true` if the connection should be
1257/// dropped (e.g., oversized frame).
1258/// Extract complete frames from `conn.parse_buf` and publish them as
1259/// `InputSlot`s. `batch_wall_ns` is the wall-clock timestamp captured
1260/// once per CQE batch by the caller (see `reader_loop`); all non-query
1261/// requests published in this call share it, sparing the reader a
1262/// per-request `clock_gettime(CLOCK_REALTIME)` on the hot path. Returns
1263/// `true` if the connection should be dropped.
1264#[allow(clippy::too_many_arguments)]
1265fn process_frames<A: Application, R>(
1266    conn: &mut ConnectionEntry<R>,
1267    producer: &mut ring::Producer<InputSlot<A::Event>>,
1268    decoder: &dyn RequestDecoder<Event = A::Event>,
1269    halt: &HaltGate,
1270    refusals: &mut RefusalSender<A::Report>,
1271    control_tx: &mpsc::Sender<ControlEvent>,
1272    batch_wall_ns: u64,
1273    recv_ts: melin_transport_core::trace::MonoTraceInstant,
1274    #[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
1275    #[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
1276) -> bool {
1277    use crate::client_frames::{FrameAction, process_client_frames};
1278
1279    let action = process_client_frames::<A>(
1280        &mut conn.parse_buf,
1281        conn.connection_id,
1282        conn.key_hash,
1283        conn.permission,
1284        producer,
1285        decoder,
1286        halt,
1287        refusals,
1288        batch_wall_ns,
1289        recv_ts,
1290        #[cfg(feature = "latency-trace")]
1291        publish_rec,
1292        #[cfg(feature = "tick-to-trade")]
1293        ingest_rec,
1294    );
1295
1296    match action {
1297        FrameAction::Continue => false,
1298        FrameAction::Disconnect => true,
1299        FrameAction::PipelineFull => {
1300            debug!(
1301                connection_id = conn.connection_id,
1302                "pipeline full, routing ServerBusy via response stage"
1303            );
1304            // The response stage owns ALL egress on a client socket. A
1305            // reader-side send here — however non-blocking — races the
1306            // response stage's own writes and can land between the two
1307            // halves of a partially-flushed response frame, permanently
1308            // desyncing the client's length-prefix framing (audit review
1309            // F3). It also kept a client-socket syscall on the ingress
1310            // thread. Routing through the control channel makes the
1311            // notice an ordinary send_buf append over there.
1312            //
1313            // Error deliberately dropped: a dead control channel means
1314            // the response stage is gone and the server is shutting
1315            // down — same reasoning as the Disconnected sends below.
1316            let _ = control_tx.send(ControlEvent::PipelineBusy {
1317                connection_id: conn.connection_id,
1318            });
1319            false
1320        }
1321    }
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326    //! Unit tests for [`process_frames`]. The function has four exit paths
1327    //! (normal end, partial parse-buf, pipeline-full, oversize-frame), each
1328    //! with subtle batch-commit ordering requirements. These tests pin that
1329    //! behaviour against a synthetic decoder so refactors of the batch path
1330    //! (e.g. moving the batch up to span the whole CQE drain) can't silently
1331    //! regress the "earlier frames must be visible before ServerBusy /
1332    //! disconnect" guarantees.
1333    use super::*;
1334    use melin_app::auth::Permission;
1335    use melin_app::decoder::{Decoded, RequestDecoder};
1336    use melin_app::{AppEvent, Application, ApplyCtx, CodecError, RejectReason};
1337    use melin_journal::JournalEvent;
1338    use melin_pipeline::ring::DisruptorBuilder;
1339    use std::io::{ErrorKind, Read};
1340    use std::os::unix::net::UnixStream;
1341    use std::sync::atomic::AtomicU64;
1342    use std::time::Duration;
1343
1344    /// The reader's enter policy. Each case is a syscall-per-drain
1345    /// decision on the hot path, and getting one wrong is either a lost
1346    /// wakeup (skipping with an empty CQ) or unsubmitted SQEs (skipping
1347    /// with a non-empty SQ) — both silent.
1348    mod ring_entry {
1349        use super::super::{RingEntry, ring_entry};
1350
1351        #[test]
1352        fn visible_completions_and_nothing_to_submit_need_no_syscall() {
1353            assert_eq!(ring_entry(0, true), RingEntry::Skip);
1354        }
1355
1356        #[test]
1357        fn queued_sqes_are_always_handed_over() {
1358            // Never skip with work in the SQ: those entries would sit
1359            // there until some later iteration happened to enter.
1360            assert_eq!(ring_entry(1, true), RingEntry::Submit);
1361            assert_eq!(ring_entry(8, false), RingEntry::SubmitAndWait);
1362        }
1363
1364        #[test]
1365        fn an_empty_cq_is_the_only_thing_that_parks_the_thread() {
1366            assert_eq!(ring_entry(0, false), RingEntry::SubmitAndWait);
1367            // ...and visible work never does, whatever the SQ holds.
1368            for sq in [0, 1, 64] {
1369                assert_ne!(ring_entry(sq, true), RingEntry::SubmitAndWait);
1370            }
1371        }
1372    }
1373
1374    /// Minimal `AppEvent` for these tests. `Copy` is required by `AppEvent`;
1375    /// the on-wire codec is unused because [`TagDecoder`] never invokes it
1376    /// (frames are interpreted directly from their tag byte).
1377    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1378    enum TestEvent {
1379        Cmd(u8),
1380        Query,
1381    }
1382
1383    impl AppEvent for TestEvent {
1384        const MAX_ENCODED_SIZE: usize = 2;
1385
1386        fn encoded_size(&self) -> usize {
1387            // Unused — the tests never round-trip through encode/decode.
1388            2
1389        }
1390        fn encode(&self, _buf: &mut [u8]) -> usize {
1391            unreachable!("process_frames does not encode app events")
1392        }
1393        fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
1394            unreachable!("process_frames does not decode app events directly")
1395        }
1396        fn is_query(&self) -> bool {
1397            matches!(self, TestEvent::Query)
1398        }
1399    }
1400
1401    /// Placeholder `Application` impl. `process_frames` calls only
1402    /// `build_reject`, for a write refused while halted; the rest
1403    /// `unreachable!`.
1404    #[derive(Default)]
1405    struct TestApp;
1406
1407    /// What [`TestApp::build_reject`] returns: the refused event and why,
1408    /// so a test can tell refusals apart.
1409    type TestReport = (TestEvent, RejectReason);
1410
1411    impl Application for TestApp {
1412        type Event = TestEvent;
1413        type Report = TestReport;
1414        type QueryResponse = ();
1415        type Sizing = ();
1416        const APP_VERSION: u16 = 0;
1417        fn apply(
1418            &mut self,
1419            _event: TestEvent,
1420            _ctx: &ApplyCtx,
1421            _out: &mut Vec<TestReport>,
1422        ) -> Option<()> {
1423            unreachable!()
1424        }
1425        fn tick(&mut self, _now_ns: u64, _out: &mut Vec<TestReport>) {
1426            unreachable!()
1427        }
1428        fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
1429            unreachable!()
1430        }
1431        fn build_reject(event: &TestEvent, reason: RejectReason) -> TestReport {
1432            (*event, reason)
1433        }
1434        fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1435            unreachable!()
1436        }
1437        fn restore<R: Read>(_r: &mut R) -> std::io::Result<Self> {
1438            unreachable!()
1439        }
1440    }
1441
1442    /// Stateless decoder that maps a frame's single payload byte to a
1443    /// [`Decoded`] outcome. Lets each test feed a precise mix of permitted,
1444    /// filtered, denied, and decode-error frames without standing up the
1445    /// real wire codec.
1446    ///
1447    /// Tag mapping (`0x00..=0xFB` map 1:1 to a Permitted seq, reserving the
1448    /// top four byte values for the non-Permitted outcomes):
1449    ///   * `0xFC` -> `Filter`
1450    ///   * `0xFD` -> `PermissionDenied`
1451    ///   * `0xFE` -> `DecodeError`
1452    ///   * `0xFF` -> `Permitted` with `is_query == true`
1453    ///   * `0x00..=0xFB` -> `Permitted` with `request_seq == byte`
1454    struct TagDecoder;
1455
1456    impl RequestDecoder for TagDecoder {
1457        type Event = TestEvent;
1458        fn decode(&self, bytes: &[u8], _permission: Permission) -> Decoded<TestEvent> {
1459            match bytes.first().copied() {
1460                None => Decoded::DecodeError("empty payload"),
1461                Some(0xFC) => Decoded::Filter,
1462                Some(0xFD) => Decoded::PermissionDenied("denied"),
1463                Some(0xFE) => Decoded::DecodeError("bad"),
1464                Some(0xFF) => Decoded::Permitted {
1465                    request_seq: 0xFF,
1466                    event: TestEvent::Query,
1467                },
1468                Some(b) => Decoded::Permitted {
1469                    request_seq: b as u64,
1470                    event: TestEvent::Cmd(b),
1471                },
1472            }
1473        }
1474    }
1475
1476    /// One-byte payload framed as `[u32 LE length=1][byte]`.
1477    fn frame(byte: u8) -> [u8; 5] {
1478        let mut f = [0u8; 5];
1479        f[..4].copy_from_slice(&1u32.to_le_bytes());
1480        f[4] = byte;
1481        f
1482    }
1483
1484    /// Length prefix announcing an oversize frame. No payload bytes follow —
1485    /// `process_frames` decides on the prefix alone, before waiting for the
1486    /// body.
1487    fn oversize_prefix() -> [u8; 4] {
1488        ((MAX_FRAME_SIZE as u32) + 1).to_le_bytes()
1489    }
1490
1491    /// Test fixture bundle. Grouped into a struct rather than returned as a
1492    /// 4-tuple to keep clippy happy (`type_complexity`) and to give each
1493    /// field a name at call sites.
1494    struct Fixture {
1495        conn: ConnectionEntry<UnixStream>,
1496        producer: ring::Producer<InputSlot<TestEvent>>,
1497        consumer: ring::Consumer<InputSlot<TestEvent>>,
1498        /// Client-side end of the socket pair — read from this to inspect
1499        /// any `ServerBusy` bytes the function under test writes.
1500        peer: UnixStream,
1501    }
1502
1503    /// Build a fresh fixture: a `ConnectionEntry` backed by a `UnixStream`
1504    /// pair plus a single-consumer disruptor of the requested capacity.
1505    fn make_fixture(ring_capacity: usize) -> Fixture {
1506        let (server_side, peer) = UnixStream::pair().expect("UnixStream::pair");
1507        // Short read timeout on the peer so assertions of "no ServerBusy
1508        // written" return promptly instead of hanging the test.
1509        peer.set_read_timeout(Some(Duration::from_millis(50)))
1510            .expect("set_read_timeout");
1511
1512        let entry = ConnectionEntry::<UnixStream> {
1513            connection_id: 7,
1514            addr: "127.0.0.1:1".parse().expect("addr parses"),
1515            permission: Permission::Trader,
1516            key_hash: 0xC0FFEE_u64,
1517            fd: server_side.as_raw_fd(),
1518            _reader: server_side,
1519            parse_buf: Vec::with_capacity(64),
1520            multishot_active: false,
1521            last_activity: Instant::now(),
1522            dying: false,
1523        };
1524
1525        let (producer, mut consumers) =
1526            DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
1527                .add_consumer()
1528                .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
1529        let consumer = consumers.pop().expect("consumer present");
1530
1531        Fixture {
1532            conn: entry,
1533            producer,
1534            consumer,
1535            peer,
1536        }
1537    }
1538
1539    /// Invoke `process_frames::<TestApp, UnixStream>`, threading the
1540    /// feature-gated histogram args when the relevant features are on so
1541    /// the call compiles in every `cargo test` configuration.
1542    ///
1543    /// Returns the disconnect flag plus the control-channel receiver —
1544    /// pipeline-full now surfaces as a `PipelineBusy` event for the
1545    /// response stage rather than reader-side socket bytes, so busy
1546    /// assertions read the channel, not the peer.
1547    fn run_process_frames(
1548        conn: &mut ConnectionEntry<UnixStream>,
1549        producer: &mut ring::Producer<InputSlot<TestEvent>>,
1550    ) -> (bool, mpsc::Receiver<ControlEvent>) {
1551        let (mut refusals, _queue) = refusal_channel();
1552        run_process_frames_with(conn, producer, &gate(None, false), &mut refusals)
1553    }
1554
1555    /// A halt gate: `replicas` connected (`None` for standalone), fenced
1556    /// or not.
1557    fn gate(replicas: Option<u32>, fenced: bool) -> HaltGate {
1558        gate_counting_into(replicas, fenced, Arc::new(AtomicU64::new(0)))
1559    }
1560
1561    /// [`gate`], counting what it refuses into `refused`.
1562    fn gate_counting_into(
1563        replicas: Option<u32>,
1564        fenced: bool,
1565        refused: Arc<AtomicU64>,
1566    ) -> HaltGate {
1567        let fence = Arc::new(melin_transport_core::fence::FenceState::new(0));
1568        if fenced {
1569            fence.fence();
1570        }
1571        HaltGate::new(
1572            replicas.map(|count| Arc::new(std::sync::atomic::AtomicU32::new(count))),
1573            fence,
1574            refused,
1575        )
1576    }
1577
1578    fn refused_count(refused: &AtomicU64) -> u64 {
1579        refused.load(std::sync::atomic::Ordering::Relaxed)
1580    }
1581
1582    /// A refusal queue whose idle bound is never consulted by these tests.
1583    fn refusal_channel() -> (
1584        RefusalSender<TestReport>,
1585        crate::halt::RefusalQueue<TestReport>,
1586    ) {
1587        crate::halt::refusal_channel(Arc::new(melin_pipeline::padding::CachePadded::new(
1588            std::sync::atomic::AtomicU64::new(0),
1589        )))
1590    }
1591
1592    /// [`run_process_frames`] against a given halt gate and refusal queue.
1593    fn run_process_frames_with(
1594        conn: &mut ConnectionEntry<UnixStream>,
1595        producer: &mut ring::Producer<InputSlot<TestEvent>>,
1596        halt: &HaltGate,
1597        refusals: &mut RefusalSender<TestReport>,
1598    ) -> (bool, mpsc::Receiver<ControlEvent>) {
1599        #[cfg(feature = "latency-trace")]
1600        let mut publish_rec = melin_transport_core::trace::register_stage("test: publish");
1601        #[cfg(feature = "tick-to-trade")]
1602        let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest");
1603
1604        #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
1605        let recv_ts = melin_transport_core::trace::mono_trace_ns();
1606
1607        let (control_tx, control_rx) = mpsc::channel();
1608        let disconnect = process_frames::<TestApp, UnixStream>(
1609            conn,
1610            producer,
1611            &TagDecoder,
1612            halt,
1613            refusals,
1614            &control_tx,
1615            0xDEAD_BEEF,
1616            recv_ts,
1617            #[cfg(feature = "latency-trace")]
1618            &mut publish_rec,
1619            #[cfg(feature = "tick-to-trade")]
1620            &mut ingest_rec,
1621        );
1622        (disconnect, control_rx)
1623    }
1624
1625    /// Count `PipelineBusy` events for the fixture connection (id 7)
1626    /// queued on the control channel.
1627    fn busy_events(rx: &mpsc::Receiver<ControlEvent>) -> usize {
1628        let mut n = 0;
1629        while let Ok(event) = rx.try_recv() {
1630            if matches!(event, ControlEvent::PipelineBusy { connection_id } if connection_id == 7) {
1631                n += 1;
1632            }
1633        }
1634        n
1635    }
1636
1637    /// Drain `consumer` into a Vec of `(seq, slot)` until it yields `None`.
1638    /// Used to assert exact event sequences after `process_frames` returns.
1639    fn drain(
1640        consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
1641    ) -> Vec<(u64, InputSlot<TestEvent>)> {
1642        let mut out = Vec::new();
1643        while let Some(pair) = consumer.try_consume() {
1644            out.push(pair);
1645        }
1646        out
1647    }
1648
1649    /// Try to read exactly 5 bytes (the ServerBusy frame size) from the
1650    /// peer. Returns `Some(bytes)` if the read completes within the peer's
1651    /// configured timeout, `None` if it times out (i.e. nothing was sent).
1652    fn read_server_busy(peer: &mut UnixStream) -> Option<[u8; 5]> {
1653        let mut buf = [0u8; 5];
1654        match peer.read_exact(&mut buf) {
1655            Ok(()) => Some(buf),
1656            Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => None,
1657            Err(e) => panic!("unexpected peer read error: {e}"),
1658        }
1659    }
1660
1661    #[test]
1662    fn process_frames_publishes_all_frames_after_single_commit() {
1663        // Capacity > number of frames — every frame must succeed and become
1664        // visible to the consumer after the trailing `batch.commit()`.
1665        let Fixture {
1666            mut conn,
1667            mut producer,
1668            mut consumer,
1669            mut peer,
1670        } = make_fixture(16);
1671        for byte in [0x01, 0x02, 0x03, 0x04, 0x05] {
1672            conn.parse_buf.extend_from_slice(&frame(byte));
1673        }
1674
1675        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
1676        assert!(!disconnect, "no oversize frame ⇒ no disconnect");
1677
1678        let events = drain(&mut consumer);
1679        assert_eq!(events.len(), 5, "all 5 frames must be visible");
1680        for (i, (seq, slot)) in events.iter().enumerate() {
1681            assert_eq!(*seq, i as u64, "seq monotonic from 0");
1682            assert_eq!(slot.connection_id, 7);
1683            assert_eq!(slot.key_hash, 0xC0FFEE_u64);
1684            let byte = (i + 1) as u8;
1685            assert_eq!(slot.request_seq, byte as u64);
1686            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
1687            // Non-query event ⇒ inherits the caller-supplied wall-clock.
1688            assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
1689        }
1690        // Parse buffer fully consumed.
1691        assert!(conn.parse_buf.is_empty());
1692        // No Full happened: no busy event, and the reader never writes
1693        // the client socket (egress belongs to the response stage).
1694        assert_eq!(
1695            busy_events(&control_rx),
1696            0,
1697            "no PipelineBusy on the happy path"
1698        );
1699        assert!(
1700            read_server_busy(&mut peer).is_none(),
1701            "reader must never write the client socket"
1702        );
1703    }
1704
1705    /// A halted reader publishes no write: each is refused, stamped with
1706    /// the input sequence it would have taken, while queries still go
1707    /// through. Nothing refused can reach the journal, so replay cannot
1708    /// apply a write the client was told failed.
1709    #[test]
1710    fn a_halted_reader_refuses_writes_before_publishing_and_still_publishes_queries() {
1711        let Fixture {
1712            mut conn,
1713            mut producer,
1714            mut consumer,
1715            ..
1716        } = make_fixture(16);
1717        // Write, query, write, query.
1718        for byte in [0x01, 0xFF, 0x02, 0xFF] {
1719            conn.parse_buf.extend_from_slice(&frame(byte));
1720        }
1721        let (mut refusals, mut queue) = refusal_channel();
1722        let counted = Arc::new(AtomicU64::new(0));
1723
1724        let (disconnect, control_rx) = run_process_frames_with(
1725            &mut conn,
1726            &mut producer,
1727            &gate_counting_into(Some(0), false, Arc::clone(&counted)),
1728            &mut refusals,
1729        );
1730        assert!(!disconnect);
1731        assert_eq!(busy_events(&control_rx), 0);
1732        assert!(conn.parse_buf.is_empty(), "every frame consumed");
1733        assert_eq!(
1734            refused_count(&counted),
1735            2,
1736            "the two writes are counted, the queries are not"
1737        );
1738
1739        let published: Vec<_> = drain(&mut consumer)
1740            .into_iter()
1741            .map(|(seq, slot)| (seq, slot.event))
1742            .collect();
1743        assert_eq!(
1744            published,
1745            [
1746                (0, JournalEvent::App(TestEvent::Query)),
1747                (1, JournalEvent::App(TestEvent::Query)),
1748            ],
1749            "only the queries enter the pipeline"
1750        );
1751
1752        let mut refused = Vec::new();
1753        assert!(queue.sync(), "the refusals are flushed");
1754        queue.release(u64::MAX, |r| refused.push(r));
1755        let expect = |input_seq, byte| crate::halt::Refusal {
1756            connection_id: 7,
1757            input_seq,
1758            report: (TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected),
1759        };
1760        assert_eq!(
1761            refused,
1762            [expect(0, 0x01), expect(1, 0x02)],
1763            "each write is stamped with the sequence of the query that followed it"
1764        );
1765    }
1766
1767    /// A fenced node closes the connection, whatever its replica count:
1768    /// nothing is published, nothing is refused, nothing is counted.
1769    #[test]
1770    fn a_fenced_reader_closes_the_connection() {
1771        let Fixture {
1772            mut conn,
1773            mut producer,
1774            mut consumer,
1775            ..
1776        } = make_fixture(16);
1777        for byte in [0x01, 0xFF] {
1778            conn.parse_buf.extend_from_slice(&frame(byte));
1779        }
1780        let (mut refusals, mut queue) = refusal_channel();
1781        let counted = Arc::new(AtomicU64::new(0));
1782
1783        let (disconnect, control_rx) = run_process_frames_with(
1784            &mut conn,
1785            &mut producer,
1786            &gate_counting_into(Some(1), true, Arc::clone(&counted)),
1787            &mut refusals,
1788        );
1789
1790        assert!(disconnect, "the connection is dropped");
1791        assert_eq!(busy_events(&control_rx), 0);
1792        assert!(
1793            drain(&mut consumer).is_empty(),
1794            "not even the query is published"
1795        );
1796        assert!(!queue.sync(), "no refusal is queued");
1797        assert_eq!(refused_count(&counted), 0);
1798    }
1799
1800    /// Refusals that cannot be queued shed load the way a full input ring
1801    /// does, and the write is still not published.
1802    #[test]
1803    fn a_full_refusal_queue_is_pipeline_busy() {
1804        let Fixture {
1805            mut conn,
1806            mut producer,
1807            mut consumer,
1808            ..
1809        } = make_fixture(16);
1810        conn.parse_buf.extend_from_slice(&frame(0x01));
1811        let (mut refusals, _queue) = refusal_channel();
1812        let filler = crate::halt::Refusal {
1813            connection_id: 1,
1814            input_seq: 0,
1815            report: (TestEvent::Cmd(0), RejectReason::ReplicaDisconnected),
1816        };
1817        while refusals.try_send(filler).is_ok() {}
1818        let counted = Arc::new(AtomicU64::new(0));
1819
1820        let (disconnect, control_rx) = run_process_frames_with(
1821            &mut conn,
1822            &mut producer,
1823            &gate_counting_into(Some(0), false, Arc::clone(&counted)),
1824            &mut refusals,
1825        );
1826
1827        assert!(!disconnect);
1828        assert_eq!(busy_events(&control_rx), 1);
1829        assert!(drain(&mut consumer).is_empty());
1830        assert_eq!(
1831            refused_count(&counted),
1832            1,
1833            "a shed write still counts as refused by the halt"
1834        );
1835    }
1836
1837    /// The caller stamps `recv_ts` once per recv (at the kernel-return
1838    /// site) and every frame parsed from that buffer must carry that exact
1839    /// value. Guards against a regression to per-frame `recv_ts` capture,
1840    /// which excluded decode from the reader-ingest / server-e2e windows
1841    /// and drifted forward for later frames in a multi-frame recv. Gated on
1842    /// `latency-trace` because `recv_ts` is a real `u64` only then (`()`
1843    /// otherwise, leaving nothing to assert).
1844    #[cfg(feature = "latency-trace")]
1845    #[test]
1846    fn process_frames_stamps_every_slot_with_caller_recv_ts() {
1847        let Fixture {
1848            mut conn,
1849            mut producer,
1850            mut consumer,
1851            ..
1852        } = make_fixture(16);
1853        for byte in [0x01, 0x02, 0x03, 0x04] {
1854            conn.parse_buf.extend_from_slice(&frame(byte));
1855        }
1856
1857        // A recognizable sentinel the caller would have captured at the
1858        // recv site; distinct from the wall-clock stamp (0xDEAD_BEEF).
1859        const RECV_TS: u64 = 0x5EED_5EED;
1860
1861        let mut publish_rec = melin_transport_core::trace::register_stage("test: publish recv_ts");
1862        #[cfg(feature = "tick-to-trade")]
1863        let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest recv_ts");
1864
1865        let (control_tx, _control_rx) = mpsc::channel();
1866        let (mut refusals, _queue) = refusal_channel();
1867        let disconnect = process_frames::<TestApp, UnixStream>(
1868            &mut conn,
1869            &mut producer,
1870            &TagDecoder,
1871            &gate(None, false),
1872            &mut refusals,
1873            &control_tx,
1874            0xDEAD_BEEF,
1875            RECV_TS,
1876            &mut publish_rec,
1877            #[cfg(feature = "tick-to-trade")]
1878            &mut ingest_rec,
1879        );
1880        assert!(!disconnect);
1881
1882        let events = drain(&mut consumer);
1883        assert_eq!(events.len(), 4, "all frames published");
1884        for (_seq, slot) in &events {
1885            assert_eq!(
1886                slot.recv_ts, RECV_TS,
1887                "every frame from one recv shares the single caller-supplied recv_ts"
1888            );
1889        }
1890    }
1891
1892    #[test]
1893    fn process_frames_rotates_batch_at_commit_every_cap() {
1894        // Push more events than `COMMIT_EVERY` (= 16) into a recv-cycle.
1895        // The cap must trigger at least one mid-loop commit so the
1896        // consumer sees the first capacity-many events before the
1897        // remainder lands. Validates the visibility-delay cap from the
1898        // perf branch — without it, all 32 events would commit together
1899        // and the first frame would wait for the 32nd to decode.
1900        //
1901        // Capacity 64 leaves room for the entire input (no Full); ring
1902        // backpressure is exercised separately in
1903        // `process_frames_partial_commit_then_server_busy_when_pipeline_full`.
1904        let Fixture {
1905            mut conn,
1906            mut producer,
1907            mut consumer,
1908            ..
1909        } = make_fixture(64);
1910        const EVENT_COUNT: usize = 32;
1911        for i in 0..EVENT_COUNT {
1912            // Use bytes 1..=32 (each ≤ 0xFB so TagDecoder yields
1913            // `Permitted` with `request_seq == byte`).
1914            conn.parse_buf.extend_from_slice(&frame((i + 1) as u8));
1915        }
1916
1917        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
1918        assert!(!disconnect, "no oversize / no Full ⇒ no disconnect");
1919
1920        let events = drain(&mut consumer);
1921        assert_eq!(events.len(), EVENT_COUNT, "every event visible");
1922        for (i, (seq, slot)) in events.iter().enumerate() {
1923            assert_eq!(*seq, i as u64, "seq contiguous across batch rotations");
1924            let byte = (i + 1) as u8;
1925            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
1926        }
1927        assert!(conn.parse_buf.is_empty());
1928    }
1929
1930    #[test]
1931    fn process_frames_query_event_skips_wall_clock_stamp() {
1932        // `AppEvent::is_query` events bypass the journal stamp — verify
1933        // the timestamp is zeroed even when a non-zero batch_wall_ns was
1934        // supplied.
1935        let Fixture {
1936            mut conn,
1937            mut producer,
1938            mut consumer,
1939            ..
1940        } = make_fixture(8);
1941        conn.parse_buf.extend_from_slice(&frame(0xFF)); // tag → Query
1942
1943        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
1944        assert!(!disconnect);
1945
1946        let events = drain(&mut consumer);
1947        assert_eq!(events.len(), 1);
1948        let (_, slot) = &events[0];
1949        assert_eq!(slot.event, JournalEvent::App(TestEvent::Query));
1950        assert_eq!(
1951            slot.timestamp_ns, 0,
1952            "query events must skip the wall-clock stamp"
1953        );
1954    }
1955
1956    #[test]
1957    fn process_frames_partial_commit_then_server_busy_when_pipeline_full() {
1958        // Ring capacity 4 + 6 frames ⇒ first 4 commit, 5th triggers Full,
1959        // 6th is never reached because the loop breaks on Full. Validates:
1960        //   * `Err(Full)` does not roll back the prior 4 (single commit
1961        //     happens before the ServerBusy write).
1962        //   * The frame that triggered Full is silently dropped — its bytes
1963        //     are compacted out of `parse_buf` along with every earlier
1964        //     frame, mirroring pre-batch behaviour.
1965        //   * Exactly one PipelineBusy event is routed to the response
1966        //     stage, and nothing is written to the socket from here.
1967        let Fixture {
1968            mut conn,
1969            mut producer,
1970            mut consumer,
1971            mut peer,
1972        } = make_fixture(4);
1973        for byte in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] {
1974            conn.parse_buf.extend_from_slice(&frame(byte));
1975        }
1976
1977        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
1978        assert!(!disconnect, "Full does not drop the connection");
1979
1980        let events = drain(&mut consumer);
1981        assert_eq!(
1982            events.len(),
1983            4,
1984            "only the first capacity-many frames are visible"
1985        );
1986        for (i, (_, slot)) in events.iter().enumerate() {
1987            let byte = (i + 1) as u8;
1988            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
1989        }
1990
1991        // The busy notice goes to the response stage — never directly
1992        // onto the socket, where it could tear a partially-flushed
1993        // response frame (audit review F3).
1994        assert_eq!(
1995            busy_events(&control_rx),
1996            1,
1997            "exactly one PipelineBusy routed"
1998        );
1999        assert!(
2000            read_server_busy(&mut peer).is_none(),
2001            "reader must never write the client socket"
2002        );
2003
2004        // The frame that triggered Full (0x05) had its bytes consumed by
2005        // the loop's `cursor +=` before `try_push_with` ran; the 6th frame
2006        // is never inspected because the loop broke. Compaction shifts
2007        // the unprocessed tail (the 6th frame's bytes) to the front.
2008        assert_eq!(
2009            conn.parse_buf,
2010            frame(0x06).to_vec(),
2011            "the 6th frame remains in parse_buf for the next recv-cycle"
2012        );
2013    }
2014
2015    /// Regression for the 2026-08 io_uring audit, finding 2 and review
2016    /// finding F3 (docs/internal/io-uring-audit-2026-08.md): the
2017    /// pipeline-full path must not touch the client socket at all. The
2018    /// original defect was a blocking `write(2)` of ServerBusy that
2019    /// parked the reader thread whenever the offender's socket buffer
2020    /// was also full — stalling ingress for *every* connection exactly
2021    /// on the overload path (against that code this test never
2022    /// returns). The busy notice now routes to the response stage,
2023    /// which also closes the frame-tearing race of any reader-side
2024    /// send. The socket-buffer fill stays: it proves the path is
2025    /// insensitive to socket state.
2026    #[test]
2027    fn pipeline_full_never_touches_the_client_socket() {
2028        let Fixture {
2029            mut conn,
2030            mut producer,
2031            mut consumer,
2032            mut peer,
2033        } = make_fixture(2);
2034
2035        // Fill the server→peer direction until the kernel refuses more —
2036        // the state a non-reading client leaves the connection in.
2037        let junk = [0u8; 4096];
2038        loop {
2039            let n = unsafe {
2040                libc::send(
2041                    conn.fd,
2042                    junk.as_ptr().cast(),
2043                    junk.len(),
2044                    libc::MSG_DONTWAIT,
2045                )
2046            };
2047            if n < 0 {
2048                assert_eq!(
2049                    std::io::Error::last_os_error().raw_os_error(),
2050                    Some(libc::EAGAIN),
2051                    "unexpected errno while filling socket"
2052                );
2053                break;
2054            }
2055        }
2056
2057        // Capacity 2 + 3 frames ⇒ the third triggers Full ⇒ ServerBusy.
2058        conn.parse_buf.extend_from_slice(&frame(0x01));
2059        conn.parse_buf.extend_from_slice(&frame(0x02));
2060        conn.parse_buf.extend_from_slice(&frame(0x03));
2061
2062        let start = std::time::Instant::now();
2063        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2064        // Generous bound to stay unflaky under CI load — the fixed path
2065        // returns in microseconds, the broken one only when the peer
2066        // reads (here: never).
2067        assert!(
2068            start.elapsed() < Duration::from_secs(1),
2069            "pipeline-full path blocked on a full socket"
2070        );
2071        assert!(!disconnect, "Full does not drop the connection");
2072        assert_eq!(
2073            drain(&mut consumer).len(),
2074            2,
2075            "the frames that fit are still published"
2076        );
2077        assert_eq!(
2078            busy_events(&control_rx),
2079            1,
2080            "busy notice routed to response stage"
2081        );
2082        // Nothing beyond the pre-fill junk arrives: drain it, then the
2083        // read must time out rather than yield a reader-written frame.
2084        let mut sink = [0u8; 4096];
2085        loop {
2086            match peer.read(&mut sink) {
2087                Ok(0) => panic!("unexpected EOF"),
2088                Ok(_) => continue,
2089                Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
2090                    break;
2091                }
2092                Err(e) => panic!("unexpected peer read error: {e}"),
2093            }
2094        }
2095    }
2096
2097    #[test]
2098    fn process_frames_oversize_commits_prior_frames_then_signals_disconnect() {
2099        // Two valid frames followed by an oversize length prefix must:
2100        //   * publish the two valid frames (commit-before-break) so the
2101        //     pipeline observes them even though we're about to tear the
2102        //     connection down,
2103        //   * return `true` so the caller drops the connection,
2104        //   * NOT emit PipelineBusy (that is reserved for pipeline-full).
2105        let Fixture {
2106            mut conn,
2107            mut producer,
2108            mut consumer,
2109            mut peer,
2110        } = make_fixture(16);
2111        conn.parse_buf.extend_from_slice(&frame(0x01));
2112        conn.parse_buf.extend_from_slice(&frame(0x02));
2113        conn.parse_buf.extend_from_slice(&oversize_prefix());
2114
2115        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2116        assert!(disconnect, "oversize frame must request disconnect");
2117
2118        let events = drain(&mut consumer);
2119        assert_eq!(
2120            events.len(),
2121            2,
2122            "prior frames are committed before the break"
2123        );
2124        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
2125        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
2126
2127        assert_eq!(
2128            busy_events(&control_rx),
2129            0,
2130            "PipelineBusy is emitted on Full, not on oversize"
2131        );
2132        assert!(
2133            read_server_busy(&mut peer).is_none(),
2134            "reader must never write the client socket"
2135        );
2136    }
2137
2138    #[test]
2139    fn process_frames_filters_denied_and_decode_errors_advance_cursor() {
2140        // Mixed batch: Permitted, Filter, PermissionDenied, DecodeError,
2141        // Permitted. Only the two Permitted frames must reach the
2142        // consumer; all bytes are consumed (parse_buf fully drains).
2143        let Fixture {
2144            mut conn,
2145            mut producer,
2146            mut consumer,
2147            ..
2148        } = make_fixture(16);
2149        for byte in [0x01, 0xFC, 0xFD, 0xFE, 0x02] {
2150            conn.parse_buf.extend_from_slice(&frame(byte));
2151        }
2152
2153        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
2154        assert!(!disconnect);
2155
2156        let events = drain(&mut consumer);
2157        assert_eq!(events.len(), 2);
2158        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
2159        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
2160        assert!(
2161            conn.parse_buf.is_empty(),
2162            "all bytes advanced past compaction"
2163        );
2164    }
2165
2166    #[test]
2167    fn process_frames_preserves_partial_trailing_frame() {
2168        // One complete frame followed by a truncated length prefix. The
2169        // complete frame must publish; the partial bytes must survive
2170        // compaction at the front of `parse_buf` for the next recv-cycle.
2171        let Fixture {
2172            mut conn,
2173            mut producer,
2174            mut consumer,
2175            ..
2176        } = make_fixture(16);
2177        conn.parse_buf.extend_from_slice(&frame(0x42));
2178        // Three of four length-prefix bytes — `cursor + 4 <= len()` is
2179        // false, so the loop breaks before consuming anything from the
2180        // partial.
2181        conn.parse_buf.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
2182
2183        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
2184        assert!(!disconnect);
2185
2186        let events = drain(&mut consumer);
2187        assert_eq!(events.len(), 1);
2188        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x42)));
2189        assert_eq!(
2190            conn.parse_buf,
2191            vec![0xDE, 0xAD, 0xBE],
2192            "partial length prefix preserved for next recv-cycle"
2193        );
2194    }
2195
2196    #[test]
2197    fn process_frames_empty_buffer_is_noop() {
2198        // No bytes in parse_buf ⇒ loop never enters; commit is the
2199        // documented zero-slot no-op; no busy event; no disconnect.
2200        let Fixture {
2201            mut conn,
2202            mut producer,
2203            mut consumer,
2204            mut peer,
2205        } = make_fixture(4);
2206
2207        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2208        assert!(!disconnect);
2209        assert_eq!(drain(&mut consumer).len(), 0);
2210        assert!(conn.parse_buf.is_empty());
2211        assert_eq!(busy_events(&control_rx), 0);
2212        assert!(read_server_busy(&mut peer).is_none());
2213    }
2214
2215    /// End-to-end soak through the real reader loop: 4 connections
2216    /// concurrently write 10 000 frames each, one `write(2)` per frame,
2217    /// so the receive path churns through the shared buffer pool and
2218    /// its recycle machinery under genuine concurrency (buf_ring where
2219    /// the kernel supports it, legacy ProvideBuffers otherwise — this
2220    /// test is the coverage for whichever mode the host engages).
2221    ///
2222    /// The failure signatures of a recycle bug are exactly what is
2223    /// asserted: a torn or misordered frame (a buffer reused while the
2224    /// kernel still owned it), a lost frame, or a spurious disconnect
2225    /// (`ENOBUFS` mishandled as a client error).
2226    #[test]
2227    fn reader_loop_soak_delivers_every_frame_in_order() {
2228        use std::io::Write as _;
2229
2230        const CONNS: u64 = 4;
2231        const FRAMES: u32 = 10_000;
2232
2233        // Capacity ≥ total frames: even if this thread is descheduled
2234        // and stops draining, the ring cannot fill, so the reader never
2235        // sheds load (which would legitimately drop frames and turn a
2236        // CI hiccup into a false failure).
2237        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
2238            .add_consumer()
2239            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2240        let mut consumer = consumers.pop().expect("consumer");
2241        let (control_tx, control_rx) = mpsc::channel();
2242        let shutdown = Arc::new(AtomicBool::new(false));
2243
2244        let mut handle = spawn_reader::<TestApp, UnixStream>(
2245            producer,
2246            Arc::new(TagDecoder),
2247            gate(None, false),
2248            refusal_channel().0,
2249            control_tx,
2250            0,    // "do not pin" sentinel
2251            None, // no idle timeout — a CI stall must not disconnect
2252            None, // no tick generator
2253            Arc::clone(&shutdown),
2254        );
2255
2256        let mut writers = Vec::new();
2257        for id in 0..CONNS {
2258            let (client, server_side) = UnixStream::pair().expect("socketpair");
2259            handle.register(ReaderRegistration {
2260                connection_id: ConnectionId(id),
2261                reader: server_side,
2262                addr: "127.0.0.1:1".parse().expect("addr"),
2263                permission: Permission::Trader,
2264                key_hash: id,
2265            });
2266            writers.push(std::thread::spawn(move || {
2267                let mut client = client;
2268                for i in 0..FRAMES {
2269                    let byte = (i % 200 + 1) as u8;
2270                    client.write_all(&frame(byte)).expect("client write");
2271                    // Brief pauses fragment the stream so the reader
2272                    // sees many small recvs (heavy buffer churn) rather
2273                    // than a few large coalesced ones.
2274                    if i % 512 == 0 {
2275                        std::thread::sleep(Duration::from_micros(200));
2276                    }
2277                }
2278                client // keep the socket alive until after the drain
2279            }));
2280        }
2281
2282        // Drain until every frame arrived or the deadline passes. The
2283        // deadline is generous — the run takes well under a second on
2284        // an idle machine — because a genuine recycle deadlock must
2285        // fail loudly, not hang CI.
2286        let deadline = Instant::now() + Duration::from_secs(60);
2287        let mut per_conn_count = [0u32; CONNS as usize];
2288        let mut total: u64 = 0;
2289        while total < CONNS * FRAMES as u64 {
2290            assert!(
2291                Instant::now() < deadline,
2292                "soak stalled: {total} of {} frames after 60s ({per_conn_count:?})",
2293                CONNS * FRAMES as u64
2294            );
2295            while let Some((_seq, slot)) = consumer.try_consume() {
2296                let conn = slot.connection_id as usize;
2297                assert!(conn < CONNS as usize, "unknown connection id");
2298                let expected = (per_conn_count[conn] % 200 + 1) as u8;
2299                assert_eq!(
2300                    slot.event,
2301                    JournalEvent::App(TestEvent::Cmd(expected)),
2302                    "conn {conn} frame {} out of order or corrupted",
2303                    per_conn_count[conn]
2304                );
2305                per_conn_count[conn] += 1;
2306                total += 1;
2307            }
2308            std::hint::spin_loop();
2309        }
2310
2311        // Any control event at this point is a defect: sockets are
2312        // still alive (held by the writer join handles) and the ring
2313        // never filled. Every arm diverges, so inspecting the first
2314        // queued event suffices.
2315        if let Ok(event) = control_rx.try_recv() {
2316            match event {
2317                ControlEvent::Disconnected { connection_id } => {
2318                    panic!("spurious disconnect of connection {connection_id}")
2319                }
2320                ControlEvent::PipelineBusy { connection_id } => {
2321                    panic!("spurious pipeline-full for connection {connection_id}")
2322                }
2323                ControlEvent::Connected { .. } => {
2324                    unreachable!("reader never sends Connected")
2325                }
2326            }
2327        }
2328
2329        let _clients: Vec<UnixStream> = writers
2330            .into_iter()
2331            .map(|w| w.join().expect("writer thread"))
2332            .collect();
2333        handle.shutdown();
2334        handle.join();
2335    }
2336
2337    /// Shutdown with live, armed connections must quiesce the ring
2338    /// provably and promptly. The armed multishot RECVs sit on sockets
2339    /// whose peers never disconnect, so nothing completes them on its
2340    /// own — the teardown's wakes (per-connection `shutdown(2)`, the
2341    /// eventfd write) are what retire them, including on hosts that
2342    /// filter the AsyncCancel opcode, where the old cancel-then-quiet
2343    /// teardown could conclude "done" with arms still live against the
2344    /// soon-freed pool. A drain that cannot prove quiescence only
2345    /// returns at the full deadline (and leaks the buffers), so
2346    /// returning well inside it is the proof this test can observe.
2347    #[test]
2348    fn shutdown_quiesces_armed_connections_promptly() {
2349        use std::io::Write as _;
2350
2351        const CONNS: u64 = 3;
2352        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(1024)
2353            .add_consumer()
2354            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2355        let mut consumer = consumers.pop().expect("consumer");
2356        let (control_tx, _control_rx) = mpsc::channel();
2357        let shutdown = Arc::new(AtomicBool::new(false));
2358        let mut handle = spawn_reader::<TestApp, UnixStream>(
2359            producer,
2360            Arc::new(TagDecoder),
2361            gate(None, false),
2362            refusal_channel().0,
2363            control_tx,
2364            0,    // "do not pin" sentinel
2365            None, // no idle timeout
2366            None, // no tick generator
2367            Arc::clone(&shutdown),
2368        );
2369
2370        let mut clients = Vec::new();
2371        for id in 0..CONNS {
2372            let (mut client, server_side) = UnixStream::pair().expect("socketpair");
2373            handle.register(ReaderRegistration {
2374                connection_id: ConnectionId(id),
2375                reader: server_side,
2376                addr: "127.0.0.1:1".parse().expect("addr"),
2377                permission: Permission::Trader,
2378                key_hash: id,
2379            });
2380            // One delivered frame per connection proves its multishot
2381            // is armed and active before the shutdown fires.
2382            client.write_all(&frame(7)).expect("client write");
2383            clients.push(client);
2384        }
2385        let deadline = Instant::now() + Duration::from_secs(30);
2386        let mut seen = 0u64;
2387        while seen < CONNS {
2388            assert!(
2389                Instant::now() < deadline,
2390                "frames never arrived ({seen}/{CONNS})"
2391            );
2392            while consumer.try_consume().is_some() {
2393                seen += 1;
2394            }
2395            std::hint::spin_loop();
2396        }
2397
2398        // The clients stay alive across the shutdown: no peer
2399        // disconnect can complete the armed RECVs — only the teardown's
2400        // own wakes can.
2401        let started = Instant::now();
2402        handle.shutdown();
2403        handle.join();
2404        let elapsed = started.elapsed();
2405        assert!(
2406            elapsed < crate::uring_teardown::DRAIN_TIMEOUT / 4,
2407            "reader teardown took {elapsed:?}, close to the drain deadline — \
2408             armed operations were not woken"
2409        );
2410        drop(clients);
2411    }
2412
2413    /// Regression for audit review F1
2414    /// (docs/internal/io-uring-audit-2026-08.md): a connection torn down
2415    /// while its multishot RECV is still armed must not have its slab
2416    /// index reused while the kernel can still post CQEs carrying it.
2417    /// Pre-fix, the LIFO free list handed client A's index straight to
2418    /// client B; A's socket stayed open (the armed op holds a file
2419    /// reference past the fd close), and A's continued bytes were parsed
2420    /// under B's connection id, key hash, and permissions — order-flow
2421    /// injection under someone else's identity. Post-fix, A's entry dies
2422    /// in place until the cancelled op's terminal CQE, so B gets a fresh
2423    /// index and A's post-teardown bytes are discarded.
2424    #[test]
2425    fn torn_down_connection_cannot_inject_frames_into_a_reused_slot() {
2426        use std::io::Write as _;
2427
2428        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
2429            .add_consumer()
2430            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2431        let mut consumer = consumers.pop().expect("consumer");
2432        let (control_tx, control_rx) = mpsc::channel();
2433        let shutdown = Arc::new(AtomicBool::new(false));
2434        let mut handle = spawn_reader::<TestApp, UnixStream>(
2435            producer,
2436            Arc::new(TagDecoder),
2437            gate(None, false),
2438            refusal_channel().0,
2439            control_tx,
2440            0,
2441            None,
2442            None,
2443            Arc::clone(&shutdown),
2444        );
2445
2446        const A_ID: u64 = 100;
2447        const B_ID: u64 = 200;
2448
2449        // Client A: valid frames (0x01..=0x08), an oversize prefix (the
2450        // teardown trigger), then a sustained stream of would-be
2451        // injection frames from A's byte range.
2452        let (a_client, a_server) = UnixStream::pair().expect("socketpair");
2453        handle.register(ReaderRegistration {
2454            connection_id: ConnectionId(A_ID),
2455            reader: a_server,
2456            addr: "127.0.0.1:1".parse().expect("addr"),
2457            permission: Permission::Trader,
2458            key_hash: A_ID,
2459        });
2460        let a_writer = std::thread::spawn(move || {
2461            let mut a = a_client;
2462            for byte in 0x01..=0x08u8 {
2463                let _ = a.write_all(&frame(byte));
2464            }
2465            let _ = a.write_all(&oversize_prefix());
2466            // Keep writing after the reader repudiates us. Write errors
2467            // (EPIPE once teardown's shutdown(2) lands) are the fix
2468            // doing its job — ignore them and keep trying.
2469            let stop = Instant::now() + Duration::from_millis(300);
2470            while Instant::now() < stop {
2471                let _ = a.write_all(&frame(0x05));
2472                std::thread::sleep(Duration::from_micros(50));
2473            }
2474            a
2475        });
2476
2477        // Wait for A's teardown announcement, then register B — the
2478        // instant a buggy free list would hand B the still-armed index.
2479        let deadline = Instant::now() + Duration::from_secs(10);
2480        loop {
2481            match control_rx.try_recv() {
2482                Ok(ControlEvent::Disconnected { connection_id }) if connection_id == A_ID => break,
2483                Ok(_) => {}
2484                Err(_) => {
2485                    assert!(Instant::now() < deadline, "no Disconnected for A");
2486                    std::thread::sleep(Duration::from_millis(1));
2487                }
2488            }
2489        }
2490
2491        const B_FRAMES: u32 = 2_000;
2492        let (b_client, b_server) = UnixStream::pair().expect("socketpair");
2493        handle.register(ReaderRegistration {
2494            connection_id: ConnectionId(B_ID),
2495            reader: b_server,
2496            addr: "127.0.0.1:2".parse().expect("addr"),
2497            permission: Permission::Trader,
2498            key_hash: B_ID,
2499        });
2500        let b_writer = std::thread::spawn(move || {
2501            let mut b = b_client;
2502            for i in 0..B_FRAMES {
2503                let byte = 0x21 + (i % 0x10) as u8; // 0x21..=0x30, disjoint from A
2504                b.write_all(&frame(byte)).expect("B write");
2505                if i % 128 == 0 {
2506                    std::thread::sleep(Duration::from_micros(100));
2507                }
2508            }
2509            b
2510        });
2511
2512        // Drain until all of B's frames arrive. Every B-attributed slot
2513        // must carry B's key hash and a B-range byte; anything else is
2514        // the injection this test exists to prevent.
2515        let deadline = Instant::now() + Duration::from_secs(30);
2516        let mut b_seen = 0u32;
2517        while b_seen < B_FRAMES {
2518            assert!(
2519                Instant::now() < deadline,
2520                "stalled: {b_seen}/{B_FRAMES} B frames"
2521            );
2522            while let Some((_seq, slot)) = consumer.try_consume() {
2523                match slot.event {
2524                    JournalEvent::App(TestEvent::Cmd(byte)) => {
2525                        if slot.connection_id == B_ID {
2526                            assert_eq!(slot.key_hash, B_ID, "B slot with foreign key hash");
2527                            assert!(
2528                                (0x21..=0x30).contains(&byte),
2529                                "byte {byte:#x} injected into B's stream"
2530                            );
2531                            b_seen += 1;
2532                        } else {
2533                            assert_eq!(slot.connection_id, A_ID, "unknown connection id");
2534                            assert!(
2535                                (0x01..=0x08).contains(&byte),
2536                                "unexpected byte {byte:#x} on A"
2537                            );
2538                        }
2539                    }
2540                    ref other => panic!("unexpected event {other:?}"),
2541                }
2542            }
2543            std::hint::spin_loop();
2544        }
2545
2546        let _a = a_writer.join().expect("A writer");
2547        let _b = b_writer.join().expect("B writer");
2548        handle.shutdown();
2549        handle.join();
2550    }
2551}