Skip to main content

melin_server_runtime/
reader.rs

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