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 as the submitting key's identity.
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 clock-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 stage.
271    permission: Permission,
272    /// FxHash of the client's Ed25519 public key. Copied into every
273    /// InputSlot as the submitting key's identity.
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 application's
378/// clock 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: at
627        // peak request rates a per-request `unix_epoch_nanos()` (vDSO
628        // `clock_gettime(CLOCK_REALTIME)`) showed up in the primary's
629        // profile. All requests in the same batch share the timestamp —
630        // precision loss is bounded by the CQE-drain cadence. The timestamp
631        // drives the application's clock but orders nothing (the pipeline
632        // orders by sequence), so that resolution is enough.
633        let batch_wall_ns = unix_epoch_nanos();
634
635        for &(token, result, flags) in &cqes {
636            // ── Tick timeout ──
637            // The CQE is just a wakeup signal — the actual tick emission
638            // happens at the top of the next loop iteration via the
639            // deadline check, so the time the tick is stamped with reflects
640            // unix_epoch_nanos at fire time, not at submit time.
641            if token == TICK_TIMEOUT_TOKEN {
642                tick_armed = false;
643                continue;
644            }
645
646            // ── Legacy ProvideBuffers completion (fallback mode only) ──
647            if token == PROVIDE_BUFS_TOKEN {
648                if result < 0 {
649                    error!(error = result, "ProvideBuffers failed");
650                }
651                continue;
652            }
653
654            // ── AsyncCancel completion ──
655            if token == CANCEL_TOKEN {
656                // Nothing to do: the cancelled op's own terminal CQE
657                // drives the state machine; ENOENT/EALREADY just mean
658                // it already finished on its own.
659                continue;
660            }
661
662            // ── Eventfd wakeup ──
663            if token == EVENTFD_TOKEN {
664                // This CQE is the armed READ completing; the re-arm at
665                // the end of the branch replaces it before anything can
666                // observe the gap, so `eventfd_armed` stays `true`
667                // through the pair. The teardown drain clears it for
668                // the one case where a completion is never processed
669                // here (a CQE still queued when the loop breaks).
670                if result >= 0 {
671                    // Process all pending registrations.
672                    while let Ok(reg) = command_rx.try_recv() {
673                        let fd = reg.reader.as_raw_fd();
674                        let entry = ConnectionEntry {
675                            connection_id: reg.connection_id.0,
676                            addr: reg.addr,
677                            permission: reg.permission,
678                            key_hash: reg.key_hash,
679                            fd,
680                            _reader: reg.reader,
681                            parse_buf: Vec::with_capacity(MAX_FRAME_SIZE + 4),
682                            multishot_active: false,
683                            last_activity: Instant::now(),
684                            dying: false,
685                        };
686                        let idx = slab.insert(entry);
687                        fd_to_slab.insert(fd, idx);
688
689                        // Submit multishot RECV for this connection.
690                        push_recv_multi(&mut ring, &mut slab, idx);
691                    }
692                } else {
693                    error!(error = result, "eventfd read error");
694                }
695
696                // Re-submit eventfd read for the next wakeup
697                // (`eventfd_armed` stays `true` — see above).
698                push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
699                continue;
700            }
701
702            // ── Connection multishot RECV completion ──
703
704            let slab_idx = token as usize;
705            let has_more = (flags & IORING_CQE_F_MORE) != 0;
706
707            // A cleared F_MORE means THIS armed op posts no further
708            // CQEs — record it before any branch, so no early `continue`
709            // below can wedge the connection with a stale `true`.
710            if !has_more && let Some(entry) = slab.get_mut(slab_idx) {
711                entry.multishot_active = false;
712            }
713
714            // Extract the buffer (if any) before acting on `result`: a
715            // buffer can ride ANY recv CQE, and one that is not recycled
716            // is leaked from the pool forever. On 6.8 the kernel
717            // recycles internally before posting error/EOF CQEs, but the
718            // supported floor is 5.19, where that guarantee could not be
719            // established — the defensive recycle is one branch.
720            let buf_id = if (flags & IORING_CQE_F_BUFFER) != 0 {
721                Some((flags >> IORING_CQE_BUFFER_SHIFT) as usize)
722            } else {
723                None
724            };
725
726            // Dying connection: teardown began while its multishot was
727            // still armed (see `begin_teardown`). Consume its CQEs
728            // without parsing — the bytes belong to a repudiated peer —
729            // but keep recycling their buffers; free the slab index only
730            // at the terminal CQE, so it cannot be handed to a new
731            // registration while the kernel can still post CQEs carrying
732            // it. Checked before the disconnect branch so the cancel's
733            // -ECANCELED completion lands here, not there (which would
734            // emit a second Disconnected event).
735            if slab.get_mut(slab_idx).is_some_and(|e| e.dying) {
736                if let Some(bid) = buf_id {
737                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
738                }
739                if !has_more && let Some(dead) = slab.remove(slab_idx) {
740                    debug!(
741                        connection_id = dead.connection_id,
742                        "teardown complete, slab index released"
743                    );
744                }
745                continue;
746            }
747
748            if result == -libc::ENOBUFS {
749                // Pool exhausted at buffer-selection time: the multishot
750                // terminated WITHOUT reading — no data was lost, it sits
751                // in the socket buffer. This is not a client error and
752                // must not disconnect (a bare `result <= 0 ⇒ remove`
753                // here dropped innocent clients under burst). Re-arm:
754                // recycles from data CQEs earlier in this drain have
755                // already refilled the buf_ring (publication is
756                // immediate), and the re-arm SQE submits after the whole
757                // drain, so the retry finds buffers.
758                if let Some(bid) = buf_id {
759                    // No buffer accompanies a selection failure by
760                    // definition; recycle defensively if one appears.
761                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
762                }
763                if has_more {
764                    // Future-kernel guard: should the kernel ever keep
765                    // the multishot alive across ENOBUFS, arming a second
766                    // one would interleave two delivery streams into the
767                    // same parse buffer.
768                    continue;
769                }
770                push_recv_multi(&mut ring, &mut slab, slab_idx);
771                continue;
772            }
773
774            if result <= 0 {
775                // Disconnect (0) or error (negative errno) — terminal
776                // for the armed op, so the index is immediately safe to
777                // free. Recycle any buffer riding the CQE first.
778                if let Some(bid) = buf_id {
779                    recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
780                }
781                if let Some(removed) = slab.remove(slab_idx) {
782                    if result == 0 {
783                        debug!(
784                            connection_id = removed.connection_id,
785                            addr = %removed.addr,
786                            "client disconnected"
787                        );
788                    } else {
789                        debug!(
790                            connection_id = removed.connection_id,
791                            addr = %removed.addr,
792                            error = result,
793                            "recv error"
794                        );
795                    }
796                    fd_to_slab.remove(&removed.fd);
797                    let _ = control_tx.send(ControlEvent::Disconnected {
798                        connection_id: removed.connection_id,
799                    });
800                }
801                continue;
802            }
803
804            let n = result as usize;
805
806            // Trace timestamp: the moment the kernel handed us this recv's
807            // bytes. Captured once per CQE — not per frame — and stamped
808            // onto every InputSlot parsed from this buffer below, so the
809            // reader-ingest and server-e2e stages measure from true wire
810            // receipt (frame decode included) rather than re-sampling after
811            // each decode (which excluded decode and drifted forward for
812            // later frames in a multi-frame recv).
813            #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
814            let recv_ts = melin_transport_core::trace::mono_trace_ns();
815
816            // Data CQEs always carry a buffer with buffer-select recvs.
817            // Defensive: nothing to copy or recycle without one, but the
818            // multishot bookkeeping already ran above, so a terminal
819            // no-buffer CQE re-arms instead of wedging the connection.
820            let Some(buf_id) = buf_id else {
821                debug!(slab_idx, "recv CQE without buffer flag");
822                if !has_more {
823                    push_recv_multi(&mut ring, &mut slab, slab_idx);
824                }
825                continue;
826            };
827
828            // Feed received bytes into the frame parser from the shared pool.
829            let action = if let Some(entry) = slab.get_mut(slab_idx) {
830                // Any successful recv resets the idle timeout.
831                entry.last_activity = batch_now;
832
833                // Copy received data from the shared buffer pool into the
834                // connection's parse buffer.
835                let buf_start = buf_id * BUF_SIZE;
836                entry
837                    .parse_buf
838                    .extend_from_slice(&buffer_pool[buf_start..buf_start + n]);
839
840                // Extract and publish complete frames.
841                let drop_conn = process_frames::<A, R>(
842                    entry,
843                    &mut producer,
844                    decoder,
845                    halt,
846                    &mut refusals,
847                    control_tx,
848                    batch_wall_ns,
849                    recv_ts,
850                    #[cfg(feature = "latency-trace")]
851                    &mut publish_rec,
852                    #[cfg(feature = "tick-to-trade")]
853                    &mut ingest_rec,
854                );
855                if drop_conn {
856                    Action::Remove
857                } else if !has_more {
858                    // Multishot terminated (buffer pool exhaustion or kernel
859                    // decision) but connection is healthy — resubmit.
860                    Action::Resubmit
861                } else {
862                    Action::None
863                }
864            } else {
865                // Stale CQE for a removed connection — ignore.
866                Action::None
867            };
868
869            // Recycle the consumed buffer. Must happen after the copy-out
870            // above — from this line on the kernel may write fresh recv
871            // data into the slot.
872            recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, buf_id);
873
874            match action {
875                Action::Remove => {
876                    // Deferred teardown, NOT an immediate slab free — the
877                    // multishot may still be armed and its index must not
878                    // be reused until the terminal CQE (audit review F1).
879                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, slab_idx);
880                }
881                Action::Resubmit => {
882                    push_recv_multi(&mut ring, &mut slab, slab_idx);
883                }
884                Action::None => {}
885            }
886        }
887
888        // Scan for idle connections that have exceeded the timeout.
889        // Coarse gate: only scan once per second to avoid unnecessary
890        // iteration during high-throughput phases when submit_and_wait
891        // returns immediately with CQEs.
892        if let Some(timeout) = connection_timeout {
893            let now = Instant::now();
894            if now.duration_since(last_timeout_scan) >= Duration::from_secs(1) {
895                last_timeout_scan = now;
896                stale.clear();
897                for (idx, slot) in slab.entries.iter().enumerate() {
898                    // Dying entries are already mid-teardown: their
899                    // `last_activity` stays stale by design, and a second
900                    // teardown would emit a duplicate Disconnected.
901                    if let Some(entry) = slot
902                        && !entry.dying
903                        && now.duration_since(entry.last_activity) > timeout
904                    {
905                        debug!(
906                            connection_id = entry.connection_id,
907                            addr = %entry.addr,
908                            "connection timed out"
909                        );
910                        stale.push(idx);
911                    }
912                }
913                for &idx in &stale {
914                    begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
915                }
916            }
917        }
918
919        // Hand buffered latency samples to the stats registry before
920        // parking in `submit_and_wait` at the top of the next
921        // iteration — once traffic stops this thread can sleep in the
922        // kernel indefinitely, which is precisely when the bench
923        // scrapes /stats-dump. Reuses the per-batch clock read.
924        #[cfg(feature = "latency-trace")]
925        if batch_now.duration_since(last_stats_flush)
926            >= melin_transport_core::trace::IDLE_FLUSH_INTERVAL
927        {
928            last_stats_flush = batch_now;
929            publish_rec.flush();
930            #[cfg(feature = "tick-to-trade")]
931            ingest_rec.flush();
932        }
933    }
934
935    // ── Teardown quiesce ────────────────────────────────────────────
936    // Armed operations hold kernel pointers into the buffer pool, the
937    // buf_ring memory, and the eventfd buffer. Closing the ring fd only
938    // triggers ASYNCHRONOUS cancellation (the kernel's ring-exit work),
939    // which can still be touching those allocations after this function
940    // returns and frees them — the same corruption class fixed on both
941    // replication transports (see `crate::uring_teardown`). Quiesce
942    // *provably*: wake every armed operation, then reap CQEs until none
943    // is owed, and leak the buffers if that cannot be shown within the
944    // deadline. "CQ went quiet" is not proof — a wake that silently
945    // failed leaves armed ops and a quiet CQ.
946    //
947    // The wake cannot rely on the cancel opcode alone: some hosts
948    // filter io_uring opcodes (the reason the buf_ring path has a
949    // legacy fallback), and a filtered cancel completes with EINVAL
950    // while the armed operations live on. So every operation that has
951    // to be proven down gets a wake it must answer: `shutdown(2)` on
952    // each connection socket completes its multishot RECV with EOF,
953    // and a wakeup write completes the armed eventfd READ. The cancel
954    // is still pushed as an accelerator where the host honours it. The
955    // tick timeout is not in the proof at all — see `owed` below.
956    // Panic unwind skips all of this and accepts the (tiny,
957    // process-is-dying) exit-work window; declaration order still
958    // closes the ring fd before the frees.
959    let proven = {
960        let cancel_all = opcode::AsyncCancel2::new(types::CancelBuilder::any())
961            .build()
962            .user_data(CANCEL_TOKEN);
963        unsafe {
964            // Ignore a full SQ: the wakes below do not depend on it.
965            let _ = ring.submission().push(&cancel_all);
966        }
967        for entry in slab.entries.iter().flatten() {
968            crate::uring_teardown::wake_pending_ops(entry.fd);
969        }
970        // Completes the armed eventfd READ into `eventfd_buf` (still
971        // alive). Best-effort: on failure the cancel is the fallback.
972        let wake: u64 = 1;
973        unsafe {
974            libc::write(
975                wakeup_fd.as_raw_fd(),
976                &wake as *const u64 as *const libc::c_void,
977                8,
978            );
979        }
980        // Flush SQEs pushed but never submitted (re-arms from the last
981        // batch, the cancel above): until they reach the kernel no CQE
982        // is coming for them, but their armed-state flags are already
983        // set, so abandoning them in the SQ would stall the proof
984        // below. On a submit error the flags stay set and the deadline
985        // routes to the leak path — the conservative outcome.
986        loop {
987            match ring.submit() {
988                Ok(_) => break,
989                Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
990                Err(_) => break,
991            }
992        }
993        let mut backoff = crate::uring_teardown::DrainBackoff::new();
994        loop {
995            for cqe in ring.completion() {
996                match cqe.user_data() {
997                    EVENTFD_TOKEN => eventfd_armed = false,
998                    // None of these touch the allocations freed below,
999                    // so reaping them settles nothing: cancel results
1000                    // carry no information, legacy ProvideBuffers
1001                    // completions only registered addresses, and the
1002                    // tick timeout reads nothing after submit (see
1003                    // `owed`).
1004                    PROVIDE_BUFS_TOKEN | CANCEL_TOKEN | TICK_TIMEOUT_TOKEN => {}
1005                    // Connection tokens are slab indices.
1006                    idx => {
1007                        // A terminal CQE retires that connection's arm.
1008                        // Data CQEs landing mid-teardown are discarded
1009                        // and their buffers deliberately not recycled —
1010                        // nothing re-arms, so the pool cannot be
1011                        // selected from again once every arm is down.
1012                        if (cqe.flags() & IORING_CQE_F_MORE) == 0
1013                            && let Some(entry) = slab.get_mut(idx as usize)
1014                        {
1015                            entry.multishot_active = false;
1016                        }
1017                    }
1018                }
1019            }
1020            // `tick_armed` is deliberately absent: an armed
1021            // `IORING_OP_TIMEOUT` holds no pointer into any of the
1022            // allocations below. The kernel copies its `Timespec` out
1023            // of `tick_ts` when the SQE is *submitted* (which is why
1024            // that binding only has to outlive the submit — see its
1025            // declaration), and the ring's own exit work retires the
1026            // timer. Waiting for it would prove nothing and cost
1027            // plenty: nothing here can wake a timeout, so the only
1028            // things that retire one are it firing at its own cadence
1029            // — `--tick-interval-ms`, operator-set and unbounded — or
1030            // the cancel above, which is exactly what a host that
1031            // filters opcodes refuses. Gating the proof on it would
1032            // hand those hosts a full-deadline stall and a spurious
1033            // "leaking" warning on every clean shutdown.
1034            let owed = eventfd_armed || slab.entries.iter().flatten().any(|e| e.multishot_active);
1035            if !owed {
1036                break true;
1037            }
1038            if !backoff.wait() {
1039                break false;
1040            }
1041        }
1042    };
1043    if !proven {
1044        // The kernel may still write the pool or the eventfd buffer, or
1045        // read buf_ring entries. Leak all three rather than hand the
1046        // allocator memory the kernel still touches — a bounded one-off
1047        // cost on a thread that is exiting anyway. `mem::forget` on the
1048        // `BufRing` also skips its ring-memory dealloc, which is the
1049        // point.
1050        // Report what was still owed, not `tick_armed` — naming a
1051        // timer that was never part of the proof would point the next
1052        // reader at the wrong operation.
1053        tracing::warn!(
1054            eventfd_armed,
1055            armed_connections = slab
1056                .entries
1057                .iter()
1058                .flatten()
1059                .filter(|e| e.multishot_active)
1060                .count(),
1061            "io_uring teardown drain did not complete; leaking reader buffers"
1062        );
1063        std::mem::forget(buffer_pool);
1064        std::mem::forget(buf_ring);
1065        std::mem::forget(eventfd_buf);
1066    }
1067
1068    // No `close` here. This thread shares the eventfd with
1069    // `UringReaderHandle`, which writes wakeups to it, and closing it from
1070    // this side left that write racing a reused descriptor number. Dropping
1071    // the `Arc` at the end of this scope releases our share; the descriptor
1072    // itself closes when the handle drops too.
1073}
1074
1075/// What to do after processing a RECV CQE.
1076enum Action {
1077    /// Multishot terminated but connection healthy — resubmit RecvMulti.
1078    Resubmit,
1079    /// Connection should be torn down (malformed frame). Handled via
1080    /// `begin_teardown`, which reads what it needs from the slab entry.
1081    Remove,
1082    /// Multishot still active — nothing to do.
1083    None,
1084}
1085
1086// ---------------------------------------------------------------------------
1087// SQE helpers
1088// ---------------------------------------------------------------------------
1089
1090/// Register the provided buffer pool via a legacy ProvideBuffers op —
1091/// the fallback when buf_ring registration is rejected. Submits
1092/// synchronously and panics on failure — called once at startup, and
1093/// only after the preferred path already failed.
1094fn register_buffer_pool(ring: &mut IoUring, pool_ptr: *mut u8) {
1095    let sqe = opcode::ProvideBuffers::new(pool_ptr, BUF_SIZE as i32, NUM_BUFFERS, BUF_GROUP_ID, 0)
1096        .build()
1097        .user_data(PROVIDE_BUFS_TOKEN);
1098
1099    unsafe {
1100        ring.submission()
1101            .push(&sqe)
1102            .expect("io_uring SQ full during buffer pool registration");
1103    }
1104
1105    ring.submit_and_wait(1)
1106        .expect("io_uring submit failed during buffer pool registration");
1107
1108    // Check the completion result.
1109    let cqe = ring
1110        .completion()
1111        .next()
1112        .expect("no CQE after ProvideBuffers");
1113    assert!(cqe.result() >= 0, "ProvideBuffers failed: {}", cqe.result());
1114}
1115
1116/// Re-provide a single consumed buffer back to the pool (legacy fallback
1117/// mode). Pushed to SQ without immediate submission — batched with the
1118/// next submit_and_wait. Safe against SQ overflow only because at most
1119/// `NUM_BUFFERS` (< `RING_SIZE`) recycles can accumulate per drain.
1120fn re_provide_buffer(ring: &mut IoUring, pool_ptr: *mut u8, buf_id: usize) {
1121    let buf_ptr = unsafe { pool_ptr.add(buf_id * BUF_SIZE) };
1122    let sqe = opcode::ProvideBuffers::new(buf_ptr, BUF_SIZE as i32, 1, BUF_GROUP_ID, buf_id as u16)
1123        .build()
1124        .user_data(PROVIDE_BUFS_TOKEN);
1125
1126    unsafe {
1127        ring.submission()
1128            .push(&sqe)
1129            .expect("io_uring SQ full — increase RING_SIZE");
1130    }
1131}
1132
1133/// Return a consumed buffer to the shared pool, in whichever recycle
1134/// mode registration selected.
1135///
1136/// Hard assert, not debug: the bid comes from a kernel CQE, this path
1137/// runs per CQE (off the per-frame budget), and an out-of-range bid
1138/// handed back to the kernel would point a future recv's DMA at
1139/// arbitrary heap — the one failure mode worth an unconditional branch.
1140fn recycle_buffer(
1141    ring: &mut IoUring,
1142    buf_ring: &mut BufRing,
1143    use_buf_ring: bool,
1144    buffer_pool: &[u8],
1145    buf_id: usize,
1146) {
1147    assert!(
1148        buf_id < NUM_BUFFERS as usize,
1149        "kernel returned out-of-pool buffer id {buf_id}"
1150    );
1151    if use_buf_ring {
1152        buf_ring.push(buf_id as u16);
1153    } else {
1154        re_provide_buffer(ring, buffer_pool.as_ptr() as *mut u8, buf_id);
1155    }
1156}
1157
1158/// Begin tearing down a connection the reader decided to drop while its
1159/// multishot RECV may still be armed (malformed frame, idle timeout).
1160///
1161/// The slab index must NOT be freed yet: the kernel can still post CQEs
1162/// carrying it, and the slab's LIFO free list would hand the index to
1163/// the next registration — the old peer's bytes would then be parsed
1164/// under the new connection's identity, key hash, and permissions
1165/// (request injection; audit review F1). Instead: sever the peer,
1166/// mark the entry dying, cancel the armed op, and let the terminal CQE
1167/// free the index (the `dying` branch of the CQE loop). When no op is
1168/// armed there is nothing that can post — free immediately.
1169fn begin_teardown<R>(
1170    ring: &mut IoUring,
1171    slab: &mut ConnectionSlab<R>,
1172    fd_to_slab: &mut HashMap<RawFd, usize>,
1173    control_tx: &mpsc::Sender<ControlEvent>,
1174    idx: usize,
1175) {
1176    let Some(entry) = slab.get_mut(idx) else {
1177        return;
1178    };
1179    if entry.dying {
1180        return;
1181    }
1182    let connection_id = entry.connection_id;
1183    let fd = entry.fd;
1184    // Sever the peer now. The fd itself must stay open until the armed
1185    // op completes (the kernel holds a file reference for it anyway);
1186    // shutdown(2) stops both directions immediately. Best-effort:
1187    // ENOTCONN just means the peer already went away.
1188    unsafe {
1189        libc::shutdown(fd, libc::SHUT_RDWR);
1190    }
1191    fd_to_slab.remove(&fd);
1192    // Dropped error: a dead control channel means the response stage is
1193    // gone and the server is shutting down.
1194    let _ = control_tx.send(ControlEvent::Disconnected { connection_id });
1195
1196    if entry.multishot_active {
1197        entry.dying = true;
1198        let sqe = opcode::AsyncCancel::new(idx as u64)
1199            .build()
1200            .user_data(CANCEL_TOKEN);
1201        unsafe {
1202            ring.submission()
1203                .push(&sqe)
1204                .expect("io_uring SQ full — increase RING_SIZE");
1205        }
1206    } else {
1207        // No armed op ⇒ no future CQEs can carry this index.
1208        slab.remove(idx);
1209    }
1210}
1211
1212/// Push a multishot RECV SQE for a connection. The kernel will produce
1213/// CQEs continuously until EOF, error, or buffer pool exhaustion —
1214/// no resubmission needed unless multishot terminates.
1215fn push_recv_multi<R>(ring: &mut IoUring, slab: &mut ConnectionSlab<R>, idx: usize) {
1216    let entry = match slab.get_mut(idx) {
1217        Some(e) => e,
1218        None => return,
1219    };
1220
1221    if entry.multishot_active {
1222        return;
1223    }
1224
1225    let sqe = opcode::RecvMulti::new(types::Fd(entry.fd), BUF_GROUP_ID)
1226        .build()
1227        .user_data(idx as u64);
1228
1229    unsafe {
1230        ring.submission()
1231            .push(&sqe)
1232            .expect("io_uring SQ full — increase RING_SIZE");
1233    }
1234    entry.multishot_active = true;
1235}
1236
1237/// Push a READ SQE for the eventfd (wakeup notification).
1238fn push_eventfd_read(ring: &mut IoUring, wakeup_fd: RawFd, buf: *mut u8) {
1239    let sqe = opcode::Read::new(types::Fd(wakeup_fd), buf, 8)
1240        .build()
1241        .user_data(EVENTFD_TOKEN);
1242
1243    unsafe {
1244        ring.submission()
1245            .push(&sqe)
1246            .expect("io_uring SQ full — increase RING_SIZE");
1247    }
1248}
1249
1250// ---------------------------------------------------------------------------
1251// Frame parsing
1252// ---------------------------------------------------------------------------
1253
1254/// Extract complete frames from the connection's parse buffer, decode them,
1255/// and publish to the disruptor. Returns `true` if the connection should be
1256/// dropped (e.g., oversized frame).
1257/// Extract complete frames from `conn.parse_buf` and publish them as
1258/// `InputSlot`s. `batch_wall_ns` is the wall-clock timestamp captured
1259/// once per CQE batch by the caller (see `reader_loop`); all non-query
1260/// requests published in this call share it, sparing the reader a
1261/// per-request `clock_gettime(CLOCK_REALTIME)` on the hot path. Returns
1262/// `true` if the connection should be dropped.
1263#[allow(clippy::too_many_arguments)]
1264fn process_frames<A: Application, R>(
1265    conn: &mut ConnectionEntry<R>,
1266    producer: &mut ring::Producer<InputSlot<A::Event>>,
1267    decoder: &dyn RequestDecoder<Event = A::Event>,
1268    halt: &HaltGate,
1269    refusals: &mut RefusalSender<A::Report>,
1270    control_tx: &mpsc::Sender<ControlEvent>,
1271    batch_wall_ns: u64,
1272    recv_ts: melin_transport_core::trace::MonoTraceInstant,
1273    #[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
1274    #[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
1275) -> bool {
1276    use crate::client_frames::{FrameAction, process_client_frames};
1277
1278    let action = process_client_frames::<A>(
1279        &mut conn.parse_buf,
1280        conn.connection_id,
1281        conn.key_hash,
1282        conn.permission,
1283        producer,
1284        decoder,
1285        halt,
1286        refusals,
1287        batch_wall_ns,
1288        recv_ts,
1289        #[cfg(feature = "latency-trace")]
1290        publish_rec,
1291        #[cfg(feature = "tick-to-trade")]
1292        ingest_rec,
1293    );
1294
1295    match action {
1296        FrameAction::Continue => false,
1297        FrameAction::Disconnect => true,
1298        FrameAction::PipelineFull => {
1299            debug!(
1300                connection_id = conn.connection_id,
1301                "pipeline full, routing ServerBusy via response stage"
1302            );
1303            // The response stage owns ALL egress on a client socket. A
1304            // reader-side send here — however non-blocking — races the
1305            // response stage's own writes and can land between the two
1306            // halves of a partially-flushed response frame, permanently
1307            // desyncing the client's length-prefix framing (audit review
1308            // F3). It also kept a client-socket syscall on the ingress
1309            // thread. Routing through the control channel makes the
1310            // notice an ordinary send_buf append over there.
1311            //
1312            // Error deliberately dropped: a dead control channel means
1313            // the response stage is gone and the server is shutting
1314            // down — same reasoning as the Disconnected sends below.
1315            let _ = control_tx.send(ControlEvent::PipelineBusy {
1316                connection_id: conn.connection_id,
1317            });
1318            false
1319        }
1320    }
1321}
1322
1323#[cfg(test)]
1324mod tests {
1325    //! Unit tests for [`process_frames`]. The function has four exit paths
1326    //! (normal end, partial parse-buf, pipeline-full, oversize-frame), each
1327    //! with subtle batch-commit ordering requirements. These tests pin that
1328    //! behaviour against a synthetic decoder so refactors of the batch path
1329    //! (e.g. moving the batch up to span the whole CQE drain) can't silently
1330    //! regress the "earlier frames must be visible before ServerBusy /
1331    //! disconnect" guarantees.
1332    use super::*;
1333    use melin_app::auth::Permission;
1334    use melin_app::decoder::{Decoded, RequestDecoder};
1335    use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1336    use melin_journal::JournalEvent;
1337    use melin_pipeline::ring::DisruptorBuilder;
1338    use melin_wire_protocol::control_codec::{
1339        TAG_APP, TAG_CHALLENGE_RESPONSE, TAG_LEN, TAG_RESPONSE_HEARTBEAT,
1340    };
1341    use std::io::{ErrorKind, Read};
1342    use std::os::unix::net::UnixStream;
1343    use std::sync::atomic::AtomicU64;
1344    use std::time::Duration;
1345
1346    /// The reader's enter policy. Each case is a syscall-per-drain
1347    /// decision on the hot path, and getting one wrong is either a lost
1348    /// wakeup (skipping with an empty CQ) or unsubmitted SQEs (skipping
1349    /// with a non-empty SQ) — both silent.
1350    mod ring_entry {
1351        use super::super::{RingEntry, ring_entry};
1352
1353        #[test]
1354        fn visible_completions_and_nothing_to_submit_need_no_syscall() {
1355            assert_eq!(ring_entry(0, true), RingEntry::Skip);
1356        }
1357
1358        #[test]
1359        fn queued_sqes_are_always_handed_over() {
1360            // Never skip with work in the SQ: those entries would sit
1361            // there until some later iteration happened to enter.
1362            assert_eq!(ring_entry(1, true), RingEntry::Submit);
1363            assert_eq!(ring_entry(8, false), RingEntry::SubmitAndWait);
1364        }
1365
1366        #[test]
1367        fn an_empty_cq_is_the_only_thing_that_parks_the_thread() {
1368            assert_eq!(ring_entry(0, false), RingEntry::SubmitAndWait);
1369            // ...and visible work never does, whatever the SQ holds.
1370            for sq in [0, 1, 64] {
1371                assert_ne!(ring_entry(sq, true), RingEntry::SubmitAndWait);
1372            }
1373        }
1374    }
1375
1376    /// Minimal `AppEvent` for these tests. `Copy` is required by `AppEvent`;
1377    /// the on-wire codec is unused because [`ByteDecoder`] never invokes it
1378    /// (frames are interpreted directly from their one body byte).
1379    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1380    enum TestEvent {
1381        Cmd(u8),
1382        Query,
1383    }
1384
1385    impl AppEvent for TestEvent {
1386        const MAX_ENCODED_SIZE: usize = 2;
1387
1388        fn encoded_size(&self) -> usize {
1389            // Unused — the tests never round-trip through encode/decode.
1390            2
1391        }
1392        fn encode(&self, _buf: &mut [u8]) -> usize {
1393            unreachable!("process_frames does not encode app events")
1394        }
1395        fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
1396            unreachable!("process_frames does not decode app events directly")
1397        }
1398        fn is_query(&self) -> bool {
1399            matches!(self, TestEvent::Query)
1400        }
1401    }
1402
1403    /// Placeholder `Application` impl. `process_frames` calls only
1404    /// `build_reject`, for a write refused while halted; the rest
1405    /// `unreachable!`.
1406    #[derive(Default)]
1407    struct TestApp;
1408
1409    /// What [`TestApp::build_reject`] returns: the refused event and why,
1410    /// so a test can tell refusals apart.
1411    type TestReport = (TestEvent, RejectReason);
1412
1413    impl Application for TestApp {
1414        type Event = TestEvent;
1415        type Report = TestReport;
1416        type QueryResponse = ();
1417        type Sizing = ();
1418        const APP_VERSION: u16 = 0;
1419        fn apply(&mut self, _event: TestEvent, _ctx: &ApplyCtx, _out: &mut Vec<TestReport>) {
1420            unreachable!()
1421        }
1422        fn query(&self, _event: TestEvent, _ctx: &QueryCtx) -> Option<()> {
1423            unreachable!()
1424        }
1425        fn tick(&mut self, _now_ns: u64, _out: &mut Vec<TestReport>) {
1426            unreachable!()
1427        }
1428        fn build_reject(event: &TestEvent, reason: RejectReason) -> TestReport {
1429            (*event, reason)
1430        }
1431        fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1432            unreachable!()
1433        }
1434        fn restore<R: Read>(_r: &mut R) -> std::io::Result<Self> {
1435            unreachable!()
1436        }
1437    }
1438
1439    /// Stateless decoder that maps a request's single body byte to a
1440    /// [`Decoded`] outcome. Lets each test feed a precise mix of permitted,
1441    /// filtered, denied, and decode-error frames without standing up the
1442    /// real wire codec.
1443    ///
1444    /// Body-byte mapping (`0x00..=0xFB` map 1:1 to a Permitted command,
1445    /// reserving the top four byte values for the non-Permitted outcomes):
1446    ///   * `0xFC` -> `Filter`
1447    ///   * `0xFD` -> `PermissionDenied`
1448    ///   * `0xFE` -> `DecodeError`
1449    ///   * `0xFF` -> `Permitted` with `is_query == true`
1450    ///   * `0x00..=0xFB` -> `Permitted(Cmd(byte))`
1451    struct ByteDecoder;
1452
1453    impl RequestDecoder for ByteDecoder {
1454        type Event = TestEvent;
1455        fn decode(&self, body: &[u8], _permission: Permission) -> Decoded<TestEvent> {
1456            match body.first().copied() {
1457                None => Decoded::DecodeError("empty body"),
1458                Some(0xFC) => Decoded::Filter,
1459                Some(0xFD) => Decoded::PermissionDenied("denied"),
1460                Some(0xFE) => Decoded::DecodeError("bad"),
1461                Some(0xFF) => Decoded::Permitted(TestEvent::Query),
1462                Some(b) => Decoded::Permitted(TestEvent::Cmd(b)),
1463            }
1464        }
1465    }
1466
1467    /// A request frame as a client sends it — `[u32 LE length][tag][body]`
1468    /// — with the tag chosen by the caller.
1469    fn request_frame(tag: u8, body: &[u8]) -> Vec<u8> {
1470        let len = (TAG_LEN + body.len()) as u32;
1471        let mut f = Vec::with_capacity(4 + len as usize);
1472        f.extend_from_slice(&len.to_le_bytes());
1473        f.push(tag);
1474        f.extend_from_slice(body);
1475        f
1476    }
1477
1478    /// The frame most tests use: an application frame, and `byte` as the
1479    /// one body byte [`ByteDecoder`] keys on.
1480    fn frame(byte: u8) -> Vec<u8> {
1481        request_frame(TAG_APP, &[byte])
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            &ByteDecoder,
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.event, JournalEvent::App(TestEvent::Cmd(byte)));
1686            // Non-query event ⇒ inherits the caller-supplied wall-clock.
1687            assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
1688        }
1689        // Parse buffer fully consumed.
1690        assert!(conn.parse_buf.is_empty());
1691        // No Full happened: no busy event, and the reader never writes
1692        // the client socket (egress belongs to the response stage).
1693        assert_eq!(
1694            busy_events(&control_rx),
1695            0,
1696            "no PipelineBusy on the happy path"
1697        );
1698        assert!(
1699            read_server_busy(&mut peer).is_none(),
1700            "reader must never write the client socket"
1701        );
1702    }
1703
1704    /// A halted reader publishes no write: each is refused, stamped with
1705    /// the input sequence it would have taken, while queries still go
1706    /// through. Nothing refused can reach the journal, so replay cannot
1707    /// apply a write the client was told failed.
1708    #[test]
1709    fn a_halted_reader_refuses_writes_before_publishing_and_still_publishes_queries() {
1710        let Fixture {
1711            mut conn,
1712            mut producer,
1713            mut consumer,
1714            ..
1715        } = make_fixture(16);
1716        // Write, query, write, query.
1717        for byte in [0x01, 0xFF, 0x02, 0xFF] {
1718            conn.parse_buf.extend_from_slice(&frame(byte));
1719        }
1720        let (mut refusals, mut queue) = refusal_channel();
1721        let counted = Arc::new(AtomicU64::new(0));
1722
1723        let (disconnect, control_rx) = run_process_frames_with(
1724            &mut conn,
1725            &mut producer,
1726            &gate_counting_into(Some(0), false, Arc::clone(&counted)),
1727            &mut refusals,
1728        );
1729        assert!(!disconnect);
1730        assert_eq!(busy_events(&control_rx), 0);
1731        assert!(conn.parse_buf.is_empty(), "every frame consumed");
1732        assert_eq!(
1733            refused_count(&counted),
1734            2,
1735            "the two writes are counted, the queries are not"
1736        );
1737
1738        let published: Vec<_> = drain(&mut consumer)
1739            .into_iter()
1740            .map(|(seq, slot)| (seq, slot.event))
1741            .collect();
1742        assert_eq!(
1743            published,
1744            [
1745                (0, JournalEvent::App(TestEvent::Query)),
1746                (1, JournalEvent::App(TestEvent::Query)),
1747            ],
1748            "only the queries enter the pipeline"
1749        );
1750
1751        let mut refused = Vec::new();
1752        assert!(queue.sync(), "the refusals are flushed");
1753        queue.release(u64::MAX, |r| refused.push(r));
1754        let expect = |input_seq, byte| crate::halt::Refusal {
1755            connection_id: 7,
1756            input_seq,
1757            report: (TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected),
1758        };
1759        assert_eq!(
1760            refused,
1761            [expect(0, 0x01), expect(1, 0x02)],
1762            "each write is stamped with the sequence of the query that followed it"
1763        );
1764    }
1765
1766    /// A fenced node closes the connection, whatever its replica count:
1767    /// nothing is published, nothing is refused, nothing is counted.
1768    #[test]
1769    fn a_fenced_reader_closes_the_connection() {
1770        let Fixture {
1771            mut conn,
1772            mut producer,
1773            mut consumer,
1774            ..
1775        } = make_fixture(16);
1776        for byte in [0x01, 0xFF] {
1777            conn.parse_buf.extend_from_slice(&frame(byte));
1778        }
1779        let (mut refusals, mut queue) = refusal_channel();
1780        let counted = Arc::new(AtomicU64::new(0));
1781
1782        let (disconnect, control_rx) = run_process_frames_with(
1783            &mut conn,
1784            &mut producer,
1785            &gate_counting_into(Some(1), true, Arc::clone(&counted)),
1786            &mut refusals,
1787        );
1788
1789        assert!(disconnect, "the connection is dropped");
1790        assert_eq!(busy_events(&control_rx), 0);
1791        assert!(
1792            drain(&mut consumer).is_empty(),
1793            "not even the query is published"
1794        );
1795        assert!(!queue.sync(), "no refusal is queued");
1796        assert_eq!(refused_count(&counted), 0);
1797    }
1798
1799    /// Refusals that cannot be queued shed load the way a full input ring
1800    /// does, and the write is still not published.
1801    #[test]
1802    fn a_full_refusal_queue_is_pipeline_busy() {
1803        let Fixture {
1804            mut conn,
1805            mut producer,
1806            mut consumer,
1807            ..
1808        } = make_fixture(16);
1809        conn.parse_buf.extend_from_slice(&frame(0x01));
1810        let (mut refusals, _queue) = refusal_channel();
1811        let filler = crate::halt::Refusal {
1812            connection_id: 1,
1813            input_seq: 0,
1814            report: (TestEvent::Cmd(0), RejectReason::ReplicaDisconnected),
1815        };
1816        while refusals.try_send(filler).is_ok() {}
1817        let counted = Arc::new(AtomicU64::new(0));
1818
1819        let (disconnect, control_rx) = run_process_frames_with(
1820            &mut conn,
1821            &mut producer,
1822            &gate_counting_into(Some(0), false, Arc::clone(&counted)),
1823            &mut refusals,
1824        );
1825
1826        assert!(!disconnect);
1827        assert_eq!(busy_events(&control_rx), 1);
1828        assert!(drain(&mut consumer).is_empty());
1829        assert_eq!(
1830            refused_count(&counted),
1831            1,
1832            "a shed write still counts as refused by the halt"
1833        );
1834    }
1835
1836    /// The caller stamps `recv_ts` once per recv (at the kernel-return
1837    /// site) and every frame parsed from that buffer must carry that exact
1838    /// value. Guards against a regression to per-frame `recv_ts` capture,
1839    /// which excluded decode from the reader-ingest / server-e2e windows
1840    /// and drifted forward for later frames in a multi-frame recv. Gated on
1841    /// `latency-trace` because `recv_ts` is a real `u64` only then (`()`
1842    /// otherwise, leaving nothing to assert).
1843    #[cfg(feature = "latency-trace")]
1844    #[test]
1845    fn process_frames_stamps_every_slot_with_caller_recv_ts() {
1846        let Fixture {
1847            mut conn,
1848            mut producer,
1849            mut consumer,
1850            ..
1851        } = make_fixture(16);
1852        for byte in [0x01, 0x02, 0x03, 0x04] {
1853            conn.parse_buf.extend_from_slice(&frame(byte));
1854        }
1855
1856        // A recognizable sentinel the caller would have captured at the
1857        // recv site; distinct from the wall-clock stamp (0xDEAD_BEEF).
1858        const RECV_TS: u64 = 0x5EED_5EED;
1859
1860        let mut publish_rec = melin_transport_core::trace::register_stage("test: publish recv_ts");
1861        #[cfg(feature = "tick-to-trade")]
1862        let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest recv_ts");
1863
1864        let (control_tx, _control_rx) = mpsc::channel();
1865        let (mut refusals, _queue) = refusal_channel();
1866        let disconnect = process_frames::<TestApp, UnixStream>(
1867            &mut conn,
1868            &mut producer,
1869            &ByteDecoder,
1870            &gate(None, false),
1871            &mut refusals,
1872            &control_tx,
1873            0xDEAD_BEEF,
1874            RECV_TS,
1875            &mut publish_rec,
1876            #[cfg(feature = "tick-to-trade")]
1877            &mut ingest_rec,
1878        );
1879        assert!(!disconnect);
1880
1881        let events = drain(&mut consumer);
1882        assert_eq!(events.len(), 4, "all frames published");
1883        for (_seq, slot) in &events {
1884            assert_eq!(
1885                slot.recv_ts, RECV_TS,
1886                "every frame from one recv shares the single caller-supplied recv_ts"
1887            );
1888        }
1889    }
1890
1891    #[test]
1892    fn process_frames_rotates_batch_at_commit_every_cap() {
1893        // Push more events than `COMMIT_EVERY` (= 16) into a recv-cycle.
1894        // The cap must trigger at least one mid-loop commit so the
1895        // consumer sees the first capacity-many events before the
1896        // remainder lands. Validates the visibility-delay cap from the
1897        // perf branch — without it, all 32 events would commit together
1898        // and the first frame would wait for the 32nd to decode.
1899        //
1900        // Capacity 64 leaves room for the entire input (no Full); ring
1901        // backpressure is exercised separately in
1902        // `process_frames_partial_commit_then_server_busy_when_pipeline_full`.
1903        let Fixture {
1904            mut conn,
1905            mut producer,
1906            mut consumer,
1907            ..
1908        } = make_fixture(64);
1909        const EVENT_COUNT: usize = 32;
1910        for i in 0..EVENT_COUNT {
1911            // Use bytes 1..=32 (each ≤ 0xFB so ByteDecoder yields
1912            // `Permitted` with `TestEvent::Cmd(byte)`).
1913            conn.parse_buf.extend_from_slice(&frame((i + 1) as u8));
1914        }
1915
1916        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
1917        assert!(!disconnect, "no oversize / no Full ⇒ no disconnect");
1918
1919        let events = drain(&mut consumer);
1920        assert_eq!(events.len(), EVENT_COUNT, "every event visible");
1921        for (i, (seq, slot)) in events.iter().enumerate() {
1922            assert_eq!(*seq, i as u64, "seq contiguous across batch rotations");
1923            let byte = (i + 1) as u8;
1924            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
1925        }
1926        assert!(conn.parse_buf.is_empty());
1927    }
1928
1929    #[test]
1930    fn process_frames_query_event_skips_wall_clock_stamp() {
1931        // `AppEvent::is_query` events bypass the journal stamp — verify
1932        // the timestamp is zeroed even when a non-zero batch_wall_ns was
1933        // supplied.
1934        let Fixture {
1935            mut conn,
1936            mut producer,
1937            mut consumer,
1938            ..
1939        } = make_fixture(8);
1940        conn.parse_buf.extend_from_slice(&frame(0xFF)); // body byte → Query
1941
1942        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
1943        assert!(!disconnect);
1944
1945        let events = drain(&mut consumer);
1946        assert_eq!(events.len(), 1);
1947        let (_, slot) = &events[0];
1948        assert_eq!(slot.event, JournalEvent::App(TestEvent::Query));
1949        assert_eq!(
1950            slot.timestamp_ns, 0,
1951            "query events must skip the wall-clock stamp"
1952        );
1953    }
1954
1955    #[test]
1956    fn process_frames_partial_commit_then_server_busy_when_pipeline_full() {
1957        // Ring capacity 4 + 6 frames ⇒ first 4 commit, 5th triggers Full,
1958        // 6th is never reached because the loop breaks on Full. Validates:
1959        //   * `Err(Full)` does not roll back the prior 4 (single commit
1960        //     happens before the ServerBusy write).
1961        //   * The frame that triggered Full is silently dropped — its bytes
1962        //     are compacted out of `parse_buf` along with every earlier
1963        //     frame, mirroring pre-batch behaviour.
1964        //   * Exactly one PipelineBusy event is routed to the response
1965        //     stage, and nothing is written to the socket from here.
1966        let Fixture {
1967            mut conn,
1968            mut producer,
1969            mut consumer,
1970            mut peer,
1971        } = make_fixture(4);
1972        for byte in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] {
1973            conn.parse_buf.extend_from_slice(&frame(byte));
1974        }
1975
1976        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
1977        assert!(!disconnect, "Full does not drop the connection");
1978
1979        let events = drain(&mut consumer);
1980        assert_eq!(
1981            events.len(),
1982            4,
1983            "only the first capacity-many frames are visible"
1984        );
1985        for (i, (_, slot)) in events.iter().enumerate() {
1986            let byte = (i + 1) as u8;
1987            assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
1988        }
1989
1990        // The busy notice goes to the response stage — never directly
1991        // onto the socket, where it could tear a partially-flushed
1992        // response frame (audit review F3).
1993        assert_eq!(
1994            busy_events(&control_rx),
1995            1,
1996            "exactly one PipelineBusy routed"
1997        );
1998        assert!(
1999            read_server_busy(&mut peer).is_none(),
2000            "reader must never write the client socket"
2001        );
2002
2003        // The frame that triggered Full (0x05) had its bytes consumed by
2004        // the loop's `cursor +=` before `try_push_with` ran; the 6th frame
2005        // is never inspected because the loop broke. Compaction shifts
2006        // the unprocessed tail (the 6th frame's bytes) to the front.
2007        assert_eq!(
2008            conn.parse_buf,
2009            frame(0x06),
2010            "the 6th frame remains in parse_buf for the next recv-cycle"
2011        );
2012    }
2013
2014    /// Regression for the 2026-08 io_uring audit, finding 2 and review
2015    /// finding F3 (docs/internal/io-uring-audit-2026-08.md): the
2016    /// pipeline-full path must not touch the client socket at all. The
2017    /// original defect was a blocking `write(2)` of ServerBusy that
2018    /// parked the reader thread whenever the offender's socket buffer
2019    /// was also full — stalling ingress for *every* connection exactly
2020    /// on the overload path (against that code this test never
2021    /// returns). The busy notice now routes to the response stage,
2022    /// which also closes the frame-tearing race of any reader-side
2023    /// send. The socket-buffer fill stays: it proves the path is
2024    /// insensitive to socket state.
2025    #[test]
2026    fn pipeline_full_never_touches_the_client_socket() {
2027        let Fixture {
2028            mut conn,
2029            mut producer,
2030            mut consumer,
2031            mut peer,
2032        } = make_fixture(2);
2033
2034        // Fill the server→peer direction until the kernel refuses more —
2035        // the state a non-reading client leaves the connection in.
2036        let junk = [0u8; 4096];
2037        loop {
2038            let n = unsafe {
2039                libc::send(
2040                    conn.fd,
2041                    junk.as_ptr().cast(),
2042                    junk.len(),
2043                    libc::MSG_DONTWAIT,
2044                )
2045            };
2046            if n < 0 {
2047                assert_eq!(
2048                    std::io::Error::last_os_error().raw_os_error(),
2049                    Some(libc::EAGAIN),
2050                    "unexpected errno while filling socket"
2051                );
2052                break;
2053            }
2054        }
2055
2056        // Capacity 2 + 3 frames ⇒ the third triggers Full ⇒ ServerBusy.
2057        conn.parse_buf.extend_from_slice(&frame(0x01));
2058        conn.parse_buf.extend_from_slice(&frame(0x02));
2059        conn.parse_buf.extend_from_slice(&frame(0x03));
2060
2061        let start = std::time::Instant::now();
2062        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2063        // Generous bound to stay unflaky under CI load — the fixed path
2064        // returns in microseconds, the broken one only when the peer
2065        // reads (here: never).
2066        assert!(
2067            start.elapsed() < Duration::from_secs(1),
2068            "pipeline-full path blocked on a full socket"
2069        );
2070        assert!(!disconnect, "Full does not drop the connection");
2071        assert_eq!(
2072            drain(&mut consumer).len(),
2073            2,
2074            "the frames that fit are still published"
2075        );
2076        assert_eq!(
2077            busy_events(&control_rx),
2078            1,
2079            "busy notice routed to response stage"
2080        );
2081        // Nothing beyond the pre-fill junk arrives: drain it, then the
2082        // read must time out rather than yield a reader-written frame.
2083        let mut sink = [0u8; 4096];
2084        loop {
2085            match peer.read(&mut sink) {
2086                Ok(0) => panic!("unexpected EOF"),
2087                Ok(_) => continue,
2088                Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
2089                    break;
2090                }
2091                Err(e) => panic!("unexpected peer read error: {e}"),
2092            }
2093        }
2094    }
2095
2096    #[test]
2097    fn process_frames_oversize_commits_prior_frames_then_signals_disconnect() {
2098        // Two valid frames followed by an oversize length prefix must:
2099        //   * publish the two valid frames (commit-before-break) so the
2100        //     pipeline observes them even though we're about to tear the
2101        //     connection down,
2102        //   * return `true` so the caller drops the connection,
2103        //   * NOT emit PipelineBusy (that is reserved for pipeline-full).
2104        let Fixture {
2105            mut conn,
2106            mut producer,
2107            mut consumer,
2108            mut peer,
2109        } = make_fixture(16);
2110        conn.parse_buf.extend_from_slice(&frame(0x01));
2111        conn.parse_buf.extend_from_slice(&frame(0x02));
2112        conn.parse_buf.extend_from_slice(&oversize_prefix());
2113
2114        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2115        assert!(disconnect, "oversize frame must request disconnect");
2116
2117        let events = drain(&mut consumer);
2118        assert_eq!(
2119            events.len(),
2120            2,
2121            "prior frames are committed before the break"
2122        );
2123        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
2124        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
2125
2126        assert_eq!(
2127            busy_events(&control_rx),
2128            0,
2129            "PipelineBusy is emitted on Full, not on oversize"
2130        );
2131        assert!(
2132            read_server_busy(&mut peer).is_none(),
2133            "reader must never write the client socket"
2134        );
2135    }
2136
2137    #[test]
2138    fn process_frames_filters_denied_and_decode_errors_advance_cursor() {
2139        // Mixed batch: Permitted, Filter, PermissionDenied, DecodeError,
2140        // Permitted. Only the two Permitted frames must reach the
2141        // consumer; all bytes are consumed (parse_buf fully drains).
2142        let Fixture {
2143            mut conn,
2144            mut producer,
2145            mut consumer,
2146            ..
2147        } = make_fixture(16);
2148        for byte in [0x01, 0xFC, 0xFD, 0xFE, 0x02] {
2149            conn.parse_buf.extend_from_slice(&frame(byte));
2150        }
2151
2152        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
2153        assert!(!disconnect);
2154
2155        let events = drain(&mut consumer);
2156        assert_eq!(events.len(), 2);
2157        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
2158        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
2159        assert!(
2160            conn.parse_buf.is_empty(),
2161            "all bytes advanced past compaction"
2162        );
2163    }
2164
2165    /// The request tag is the runtime's to read: an empty frame, or one
2166    /// whose tag is not the application's, is dropped before any decoder
2167    /// sees it, and the frames around it still publish. The decoder gets
2168    /// the body alone, so a body byte equal to a protocol tag is just
2169    /// that — `frame(0x01)` carries the heartbeat's value as a command.
2170    #[test]
2171    fn process_frames_reads_the_request_tag_itself() {
2172        let Fixture {
2173            mut conn,
2174            mut producer,
2175            mut consumer,
2176            ..
2177        } = make_fixture(16);
2178        conn.parse_buf.extend_from_slice(&frame(0x01));
2179        // An empty frame: no tag at all.
2180        conn.parse_buf.extend_from_slice(&0u32.to_le_bytes());
2181        // A zeroed tag, the protocol's own frames, and a request framed
2182        // under an application tag, as a pre-release build did — each
2183        // would decode to `Cmd(0x03)` if a decoder saw it.
2184        for tag in [0x00, TAG_CHALLENGE_RESPONSE, TAG_RESPONSE_HEARTBEAT, 0x10] {
2185            conn.parse_buf
2186                .extend_from_slice(&request_frame(tag, &[0x03]));
2187        }
2188        conn.parse_buf.extend_from_slice(&frame(0x02));
2189
2190        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
2191        assert!(!disconnect, "malformed requests do not drop the connection");
2192
2193        let events = drain(&mut consumer);
2194        assert_eq!(events.len(), 2);
2195        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
2196        assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
2197        assert!(conn.parse_buf.is_empty(), "every frame was consumed");
2198    }
2199
2200    #[test]
2201    fn process_frames_preserves_partial_trailing_frame() {
2202        // One complete frame followed by a truncated length prefix. The
2203        // complete frame must publish; the partial bytes must survive
2204        // compaction at the front of `parse_buf` for the next recv-cycle.
2205        let Fixture {
2206            mut conn,
2207            mut producer,
2208            mut consumer,
2209            ..
2210        } = make_fixture(16);
2211        conn.parse_buf.extend_from_slice(&frame(0x42));
2212        // Three of four length-prefix bytes — `cursor + 4 <= len()` is
2213        // false, so the loop breaks before consuming anything from the
2214        // partial.
2215        conn.parse_buf.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
2216
2217        let (disconnect, _control_rx) = run_process_frames(&mut conn, &mut producer);
2218        assert!(!disconnect);
2219
2220        let events = drain(&mut consumer);
2221        assert_eq!(events.len(), 1);
2222        assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x42)));
2223        assert_eq!(
2224            conn.parse_buf,
2225            vec![0xDE, 0xAD, 0xBE],
2226            "partial length prefix preserved for next recv-cycle"
2227        );
2228    }
2229
2230    #[test]
2231    fn process_frames_empty_buffer_is_noop() {
2232        // No bytes in parse_buf ⇒ loop never enters; commit is the
2233        // documented zero-slot no-op; no busy event; no disconnect.
2234        let Fixture {
2235            mut conn,
2236            mut producer,
2237            mut consumer,
2238            mut peer,
2239        } = make_fixture(4);
2240
2241        let (disconnect, control_rx) = run_process_frames(&mut conn, &mut producer);
2242        assert!(!disconnect);
2243        assert_eq!(drain(&mut consumer).len(), 0);
2244        assert!(conn.parse_buf.is_empty());
2245        assert_eq!(busy_events(&control_rx), 0);
2246        assert!(read_server_busy(&mut peer).is_none());
2247    }
2248
2249    /// End-to-end soak through the real reader loop: 4 connections
2250    /// concurrently write 10 000 frames each, one `write(2)` per frame,
2251    /// so the receive path churns through the shared buffer pool and
2252    /// its recycle machinery under genuine concurrency (buf_ring where
2253    /// the kernel supports it, legacy ProvideBuffers otherwise — this
2254    /// test is the coverage for whichever mode the host engages).
2255    ///
2256    /// The failure signatures of a recycle bug are exactly what is
2257    /// asserted: a torn or misordered frame (a buffer reused while the
2258    /// kernel still owned it), a lost frame, or a spurious disconnect
2259    /// (`ENOBUFS` mishandled as a client error).
2260    #[test]
2261    fn reader_loop_soak_delivers_every_frame_in_order() {
2262        use std::io::Write as _;
2263
2264        const CONNS: u64 = 4;
2265        const FRAMES: u32 = 10_000;
2266
2267        // Capacity ≥ total frames: even if this thread is descheduled
2268        // and stops draining, the ring cannot fill, so the reader never
2269        // sheds load (which would legitimately drop frames and turn a
2270        // CI hiccup into a false failure).
2271        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
2272            .add_consumer()
2273            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2274        let mut consumer = consumers.pop().expect("consumer");
2275        let (control_tx, control_rx) = mpsc::channel();
2276        let shutdown = Arc::new(AtomicBool::new(false));
2277
2278        let mut handle = spawn_reader::<TestApp, UnixStream>(
2279            producer,
2280            Arc::new(ByteDecoder),
2281            gate(None, false),
2282            refusal_channel().0,
2283            control_tx,
2284            0,    // "do not pin" sentinel
2285            None, // no idle timeout — a CI stall must not disconnect
2286            None, // no tick generator
2287            Arc::clone(&shutdown),
2288        );
2289
2290        let mut writers = Vec::new();
2291        for id in 0..CONNS {
2292            let (client, server_side) = UnixStream::pair().expect("socketpair");
2293            handle.register(ReaderRegistration {
2294                connection_id: ConnectionId(id),
2295                reader: server_side,
2296                addr: "127.0.0.1:1".parse().expect("addr"),
2297                permission: Permission::Trader,
2298                key_hash: id,
2299            });
2300            writers.push(std::thread::spawn(move || {
2301                let mut client = client;
2302                for i in 0..FRAMES {
2303                    let byte = (i % 200 + 1) as u8;
2304                    client.write_all(&frame(byte)).expect("client write");
2305                    // Brief pauses fragment the stream so the reader
2306                    // sees many small recvs (heavy buffer churn) rather
2307                    // than a few large coalesced ones.
2308                    if i % 512 == 0 {
2309                        std::thread::sleep(Duration::from_micros(200));
2310                    }
2311                }
2312                client // keep the socket alive until after the drain
2313            }));
2314        }
2315
2316        // Drain until every frame arrived or the deadline passes. The
2317        // deadline is generous — the run takes well under a second on
2318        // an idle machine — because a genuine recycle deadlock must
2319        // fail loudly, not hang CI.
2320        let deadline = Instant::now() + Duration::from_secs(60);
2321        let mut per_conn_count = [0u32; CONNS as usize];
2322        let mut total: u64 = 0;
2323        while total < CONNS * FRAMES as u64 {
2324            assert!(
2325                Instant::now() < deadline,
2326                "soak stalled: {total} of {} frames after 60s ({per_conn_count:?})",
2327                CONNS * FRAMES as u64
2328            );
2329            while let Some((_seq, slot)) = consumer.try_consume() {
2330                let conn = slot.connection_id as usize;
2331                assert!(conn < CONNS as usize, "unknown connection id");
2332                let expected = (per_conn_count[conn] % 200 + 1) as u8;
2333                assert_eq!(
2334                    slot.event,
2335                    JournalEvent::App(TestEvent::Cmd(expected)),
2336                    "conn {conn} frame {} out of order or corrupted",
2337                    per_conn_count[conn]
2338                );
2339                per_conn_count[conn] += 1;
2340                total += 1;
2341            }
2342            std::hint::spin_loop();
2343        }
2344
2345        // Any control event at this point is a defect: sockets are
2346        // still alive (held by the writer join handles) and the ring
2347        // never filled. Every arm diverges, so inspecting the first
2348        // queued event suffices.
2349        if let Ok(event) = control_rx.try_recv() {
2350            match event {
2351                ControlEvent::Disconnected { connection_id } => {
2352                    panic!("spurious disconnect of connection {connection_id}")
2353                }
2354                ControlEvent::PipelineBusy { connection_id } => {
2355                    panic!("spurious pipeline-full for connection {connection_id}")
2356                }
2357                ControlEvent::Connected { .. } => {
2358                    unreachable!("reader never sends Connected")
2359                }
2360            }
2361        }
2362
2363        let _clients: Vec<UnixStream> = writers
2364            .into_iter()
2365            .map(|w| w.join().expect("writer thread"))
2366            .collect();
2367        handle.shutdown();
2368        handle.join();
2369    }
2370
2371    /// Shutdown with live, armed connections must quiesce the ring
2372    /// provably and promptly. The armed multishot RECVs sit on sockets
2373    /// whose peers never disconnect, so nothing completes them on its
2374    /// own — the teardown's wakes (per-connection `shutdown(2)`, the
2375    /// eventfd write) are what retire them, including on hosts that
2376    /// filter the AsyncCancel opcode, where the old cancel-then-quiet
2377    /// teardown could conclude "done" with arms still live against the
2378    /// soon-freed pool. A drain that cannot prove quiescence only
2379    /// returns at the full deadline (and leaks the buffers), so
2380    /// returning well inside it is the proof this test can observe.
2381    #[test]
2382    fn shutdown_quiesces_armed_connections_promptly() {
2383        use std::io::Write as _;
2384
2385        const CONNS: u64 = 3;
2386        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(1024)
2387            .add_consumer()
2388            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2389        let mut consumer = consumers.pop().expect("consumer");
2390        let (control_tx, _control_rx) = mpsc::channel();
2391        let shutdown = Arc::new(AtomicBool::new(false));
2392        let mut handle = spawn_reader::<TestApp, UnixStream>(
2393            producer,
2394            Arc::new(ByteDecoder),
2395            gate(None, false),
2396            refusal_channel().0,
2397            control_tx,
2398            0,    // "do not pin" sentinel
2399            None, // no idle timeout
2400            None, // no tick generator
2401            Arc::clone(&shutdown),
2402        );
2403
2404        let mut clients = Vec::new();
2405        for id in 0..CONNS {
2406            let (mut client, server_side) = UnixStream::pair().expect("socketpair");
2407            handle.register(ReaderRegistration {
2408                connection_id: ConnectionId(id),
2409                reader: server_side,
2410                addr: "127.0.0.1:1".parse().expect("addr"),
2411                permission: Permission::Trader,
2412                key_hash: id,
2413            });
2414            // One delivered frame per connection proves its multishot
2415            // is armed and active before the shutdown fires.
2416            client.write_all(&frame(7)).expect("client write");
2417            clients.push(client);
2418        }
2419        let deadline = Instant::now() + Duration::from_secs(30);
2420        let mut seen = 0u64;
2421        while seen < CONNS {
2422            assert!(
2423                Instant::now() < deadline,
2424                "frames never arrived ({seen}/{CONNS})"
2425            );
2426            while consumer.try_consume().is_some() {
2427                seen += 1;
2428            }
2429            std::hint::spin_loop();
2430        }
2431
2432        // The clients stay alive across the shutdown: no peer
2433        // disconnect can complete the armed RECVs — only the teardown's
2434        // own wakes can.
2435        let started = Instant::now();
2436        handle.shutdown();
2437        handle.join();
2438        let elapsed = started.elapsed();
2439        assert!(
2440            elapsed < crate::uring_teardown::DRAIN_TIMEOUT / 4,
2441            "reader teardown took {elapsed:?}, close to the drain deadline — \
2442             armed operations were not woken"
2443        );
2444        drop(clients);
2445    }
2446
2447    /// Regression for audit review F1
2448    /// (docs/internal/io-uring-audit-2026-08.md): a connection torn down
2449    /// while its multishot RECV is still armed must not have its slab
2450    /// index reused while the kernel can still post CQEs carrying it.
2451    /// Pre-fix, the LIFO free list handed client A's index straight to
2452    /// client B; A's socket stayed open (the armed op holds a file
2453    /// reference past the fd close), and A's continued bytes were parsed
2454    /// under B's connection id, key hash, and permissions — request
2455    /// injection under someone else's identity. Post-fix, A's entry dies
2456    /// in place until the cancelled op's terminal CQE, so B gets a fresh
2457    /// index and A's post-teardown bytes are discarded.
2458    #[test]
2459    fn torn_down_connection_cannot_inject_frames_into_a_reused_slot() {
2460        use std::io::Write as _;
2461
2462        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
2463            .add_consumer()
2464            .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2465        let mut consumer = consumers.pop().expect("consumer");
2466        let (control_tx, control_rx) = mpsc::channel();
2467        let shutdown = Arc::new(AtomicBool::new(false));
2468        let mut handle = spawn_reader::<TestApp, UnixStream>(
2469            producer,
2470            Arc::new(ByteDecoder),
2471            gate(None, false),
2472            refusal_channel().0,
2473            control_tx,
2474            0,
2475            None,
2476            None,
2477            Arc::clone(&shutdown),
2478        );
2479
2480        const A_ID: u64 = 100;
2481        const B_ID: u64 = 200;
2482
2483        // Client A: valid frames (0x01..=0x08), an oversize prefix (the
2484        // teardown trigger), then a sustained stream of would-be
2485        // injection frames from A's byte range.
2486        let (a_client, a_server) = UnixStream::pair().expect("socketpair");
2487        handle.register(ReaderRegistration {
2488            connection_id: ConnectionId(A_ID),
2489            reader: a_server,
2490            addr: "127.0.0.1:1".parse().expect("addr"),
2491            permission: Permission::Trader,
2492            key_hash: A_ID,
2493        });
2494        let a_writer = std::thread::spawn(move || {
2495            let mut a = a_client;
2496            for byte in 0x01..=0x08u8 {
2497                let _ = a.write_all(&frame(byte));
2498            }
2499            let _ = a.write_all(&oversize_prefix());
2500            // Keep writing after the reader repudiates us. Write errors
2501            // (EPIPE once teardown's shutdown(2) lands) are the fix
2502            // doing its job — ignore them and keep trying.
2503            let stop = Instant::now() + Duration::from_millis(300);
2504            while Instant::now() < stop {
2505                let _ = a.write_all(&frame(0x05));
2506                std::thread::sleep(Duration::from_micros(50));
2507            }
2508            a
2509        });
2510
2511        // Wait for A's teardown announcement, then register B — the
2512        // instant a buggy free list would hand B the still-armed index.
2513        let deadline = Instant::now() + Duration::from_secs(10);
2514        loop {
2515            match control_rx.try_recv() {
2516                Ok(ControlEvent::Disconnected { connection_id }) if connection_id == A_ID => break,
2517                Ok(_) => {}
2518                Err(_) => {
2519                    assert!(Instant::now() < deadline, "no Disconnected for A");
2520                    std::thread::sleep(Duration::from_millis(1));
2521                }
2522            }
2523        }
2524
2525        const B_FRAMES: u32 = 2_000;
2526        let (b_client, b_server) = UnixStream::pair().expect("socketpair");
2527        handle.register(ReaderRegistration {
2528            connection_id: ConnectionId(B_ID),
2529            reader: b_server,
2530            addr: "127.0.0.1:2".parse().expect("addr"),
2531            permission: Permission::Trader,
2532            key_hash: B_ID,
2533        });
2534        let b_writer = std::thread::spawn(move || {
2535            let mut b = b_client;
2536            for i in 0..B_FRAMES {
2537                let byte = 0x21 + (i % 0x10) as u8; // 0x21..=0x30, disjoint from A
2538                b.write_all(&frame(byte)).expect("B write");
2539                if i % 128 == 0 {
2540                    std::thread::sleep(Duration::from_micros(100));
2541                }
2542            }
2543            b
2544        });
2545
2546        // Drain until all of B's frames arrive. Every B-attributed slot
2547        // must carry B's key hash and a B-range byte; anything else is
2548        // the injection this test exists to prevent.
2549        let deadline = Instant::now() + Duration::from_secs(30);
2550        let mut b_seen = 0u32;
2551        while b_seen < B_FRAMES {
2552            assert!(
2553                Instant::now() < deadline,
2554                "stalled: {b_seen}/{B_FRAMES} B frames"
2555            );
2556            while let Some((_seq, slot)) = consumer.try_consume() {
2557                match slot.event {
2558                    JournalEvent::App(TestEvent::Cmd(byte)) => {
2559                        if slot.connection_id == B_ID {
2560                            assert_eq!(slot.key_hash, B_ID, "B slot with foreign key hash");
2561                            assert!(
2562                                (0x21..=0x30).contains(&byte),
2563                                "byte {byte:#x} injected into B's stream"
2564                            );
2565                            b_seen += 1;
2566                        } else {
2567                            assert_eq!(slot.connection_id, A_ID, "unknown connection id");
2568                            assert!(
2569                                (0x01..=0x08).contains(&byte),
2570                                "unexpected byte {byte:#x} on A"
2571                            );
2572                        }
2573                    }
2574                    ref other => panic!("unexpected event {other:?}"),
2575                }
2576            }
2577            std::hint::spin_loop();
2578        }
2579
2580        let _a = a_writer.join().expect("A writer");
2581        let _b = b_writer.join().expect("B writer");
2582        handle.shutdown();
2583        handle.join();
2584    }
2585}