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