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