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::early_reply::EarlyReplySender;
33use crate::halt::HaltGate;
34use crate::policy_control::{AckPolicyPublisher, BootedClaim};
35use melin_app::Application;
36use melin_app::auth::{ClientRole, RoleId};
37use melin_app::decoder::ErasedDecoder;
38
39/// Decoder type alias: the application's request decoder, bound to its
40/// `Event` type, with its role type erased (see [`ErasedDecoder`]).
41/// Mirrors [`crate::response::ResponseEncoderArc`]; hides the
42/// `dyn ErasedDecoder<…>` spelling at call sites that thread the decoder
43/// through several functions.
44pub type RequestDecoderArc<A> = Arc<dyn ErasedDecoder<<A as Application>::Event>>;
45use melin_app::unix_epoch_nanos;
46use melin_pipeline::ring;
47use melin_transport_core::health::IngressMetrics;
48use melin_transport_core::pipeline::InputSlot;
49use melin_transport_core::tick::TickSchedule;
50
51/// Size of each provided buffer. 4 KiB accommodates multiple frames per
52/// recv (frames are typically <100 bytes).
53///
54/// The number of buffers in the shared pool, and the ring's depth, are
55/// not constants: both are derived from `--max-connections` at startup —
56/// see [`RingSizing`]. The pool is a power of two (buf_ring ABI) with two
57/// buffers per connection. On exhaustion the kernel completes the
58/// multishot with `ENOBUFS` (no data consumed) and the loop re-arms it
59/// after the drain's recycles refill the ring. The ring holds a drain's
60/// worth of SQEs: two per connection (a multishot re-arm and a teardown
61/// cancel) plus housekeeping; buffer recycling goes through the buf_ring,
62/// not the SQ.
63const BUF_SIZE: usize = 4096;
64
65/// Buffer group ID for the provided recv buffer pool.
66const BUF_GROUP_ID: u16 = 0;
67
68use crate::client_frames::{ClientIngress, FrameAction, LongParkReport, MAX_FRAME_SIZE};
69use crate::connection_limit::{RingSizing, await_startup, ring_setup_error};
70
71/// User data sentinel for the eventfd read SQE.
72const EVENTFD_TOKEN: u64 = u64::MAX;
73
74/// User data sentinel for legacy ProvideBuffers CQEs (fallback recycle
75/// path only). Best-effort re-provisions: we log errors but don't act
76/// on success.
77const PROVIDE_BUFS_TOKEN: u64 = u64::MAX - 1;
78
79/// User data sentinel for AsyncCancel SQEs (connection teardown and the
80/// shutdown quiesce). The completion carries no actionable information:
81/// `ENOENT`/`EALREADY` just mean the target op already finished.
82const CANCEL_TOKEN: u64 = u64::MAX - 3;
83
84/// User data sentinel for the tick timeout SQE. The reader arms a single
85/// `IORING_OP_TIMEOUT` per cadence so `submit_and_wait` returns at the tick
86/// deadline even when no client traffic is flowing. The CQE itself carries
87/// no information; the loop body checks `Instant::now()` against the next
88/// deadline and emits the actual `JournalEvent::Tick`.
89const TICK_TIMEOUT_TOKEN: u64 = u64::MAX - 2;
90
91/// User data sentinel for the park-retry timeout SQE: armed while any
92/// connection is parked, so the reader comes back round to retry them
93/// even when nothing else wakes it (see [`PARK_RETRY`]). Like the tick's,
94/// its CQE carries no information.
95const PARK_RETRY_TOKEN: u64 = u64::MAX - 4;
96
97/// The longest a parked connection waits for its retry once the
98/// early-reply queue has room again.
99///
100/// Nothing tells the reader that the response stage drained the queue,
101/// so while any connection is parked its wait in the kernel is bounded by
102/// a timeout of this length. A wake from the response stage (the eventfd)
103/// would resume sooner, but needs a fenced park/drain handshake against a
104/// lost wake, and puts a `write` on the response stage's drain, to shorten
105/// a path that is degraded by definition: a park happens only while that
106/// stage is not draining. Any other wake retries parked connections too.
107const PARK_RETRY: Duration = Duration::from_millis(1);
108
109/// The longest a park's `AsyncCancel` may go unanswered before the reader
110/// stops waiting on it and closes the connection.
111///
112/// An honoured cancel's terminal CQE lands within the same submit or the
113/// next, so a cancel still in flight after this long was not honoured:
114/// some hosts filter io_uring opcodes (see the shutdown quiesce), and a
115/// filtered cancel completes with `EINVAL`, leaving the multishot armed.
116/// The connection's bytes would then keep landing while parked, growing
117/// its buffer with nothing pushing back on the client. Closing it is the
118/// behaviour before parking existed; teardown's `shutdown(2)` ends the
119/// multishot whatever the host filters.
120const PARK_CANCEL_DEADLINE: Duration = Duration::from_secs(1);
121
122/// CQE flag: buffer ID is valid in upper 16 bits of flags.
123const IORING_CQE_F_BUFFER: u32 = 1 << 0;
124
125/// CQE flag: more completions coming from this multishot operation.
126const IORING_CQE_F_MORE: u32 = 1 << 1;
127
128/// Bit shift to extract buffer ID from CQE flags.
129const IORING_CQE_BUFFER_SHIFT: u32 = 16;
130
131use melin_wire_protocol::control::ConnectionId;
132
133/// Command sent from the accept loop to a reader thread.
134pub struct ReaderRegistration<R> {
135 pub connection_id: ConnectionId,
136 pub reader: R,
137 pub addr: SocketAddr,
138 /// The client's role, established during the auth handshake.
139 pub role: ClientRole<RoleId>,
140 /// FxHash of the client's Ed25519 public key. Stored per-connection
141 /// and copied into every InputSlot as the submitting key's identity.
142 pub key_hash: u64,
143}
144
145/// Handle for the accept loop to register connections with the io_uring reader.
146pub struct UringReaderHandle<R> {
147 tx: mpsc::Sender<ReaderRegistration<R>>,
148 /// Wakeup eventfd, shared with the reader thread.
149 ///
150 /// `Arc<OwnedFd>` rather than a `RawFd` so the descriptor cannot be
151 /// closed while the other side still holds it: the close happens in
152 /// `OwnedFd`'s `Drop`, which runs when the last holder goes away, and
153 /// no code here is able to call `close` early because none of it owns
154 /// a raw descriptor to close.
155 ///
156 /// The reader thread used to close this itself while this handle was
157 /// still writing wakeups to it — a use-after-close whose real danger
158 /// was descriptor reuse, not `EBADF`: once the number is free, a
159 /// journal segment or an accepted connection can be assigned it, and
160 /// the next wakeup writes eight bytes into that instead. Found by
161 /// ThreadSanitizer.
162 event_fd: Arc<OwnedFd>,
163 join_handle: Option<JoinHandle<()>>,
164 shutdown: Arc<AtomicBool>,
165}
166
167impl<R> UringReaderHandle<R> {
168 /// Register a new connection with the reader thread.
169 ///
170 /// If the reader thread's channel is dead (thread panicked), logs an
171 /// error and signals shutdown so the server can restart cleanly.
172 pub fn register(&mut self, registration: ReaderRegistration<R>) {
173 if self.tx.send(registration).is_ok() {
174 wake_reader(&self.event_fd);
175 } else {
176 error!("reader thread dead, cannot register connection");
177 self.shutdown.store(true, Ordering::Relaxed);
178 }
179 }
180
181 /// Signal the reader thread to shut down and wake it from io_uring_enter.
182 pub fn shutdown(&self) {
183 self.shutdown.store(true, Ordering::Relaxed);
184 wake_reader(&self.event_fd);
185 }
186
187 /// Join the reader thread. Call after `shutdown()`.
188 pub fn join(mut self) {
189 if let Some(handle) = self.join_handle.take()
190 && let Err(panic) = handle.join()
191 {
192 let msg = panic
193 .downcast_ref::<&str>()
194 .copied()
195 .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
196 .unwrap_or("<non-string panic>");
197 error!(message = msg, "reader thread panicked");
198 }
199 }
200}
201
202/// Wake the reader from `io_uring_enter` through its eventfd: the loop
203/// comes back round, and sees whatever it was woken for (a registration,
204/// shutdown, an ack-policy request).
205///
206/// A failed write is not reported: on a nonblocking eventfd it can only be
207/// `EAGAIN`, the counter at its maximum, which means a wakeup is already
208/// pending.
209pub(crate) fn wake_reader(event_fd: &OwnedFd) {
210 let val: u64 = 1;
211 // SAFETY: `event_fd` is a live descriptor (borrowed from its owner), and
212 // the buffer is a live `u64`, of the length passed.
213 unsafe {
214 libc::write(
215 event_fd.as_raw_fd(),
216 &val as *const u64 as *const libc::c_void,
217 8,
218 );
219 }
220}
221
222/// Spawn the io_uring reader thread. Returns a handle for registering
223/// connections.
224///
225/// One reader thread serves every TCP connection on the server. io_uring
226/// with multishot RECV multiplexes thousands of sockets efficiently and the
227/// matching stage is the throughput limit, so adding more reader threads
228/// would not raise throughput — it would only re-introduce contention on
229/// the input ring's multi-producer cursor.
230///
231/// `tick_cadence: Some(d)` makes the reader the clock-tick generator: it
232/// arms an `IORING_OP_TIMEOUT` so `submit_and_wait` returns at the tick
233/// deadline even when no client traffic is flowing, then publishes a
234/// `JournalEvent::Tick { now_ns }` onto the same input ring it uses for
235/// client requests. Pass `None` to disable the tick (useful for benchmarks
236/// that don't exercise time-driven features).
237///
238/// Every request it does not publish — one the application's decoder
239/// filters or refuses, a write refused while `halt` refuses writes, one
240/// shed on a full input ring — the reader answers through
241/// `early_replies` — see [`crate::halt`]. It counts those it sheds,
242/// filters and refuses into `metrics`, with the connections it parks, for
243/// the health endpoint.
244///
245/// The reader also journals the admin `ACK-POLICY` command's requests: it
246/// attaches `ack_policy` with its wakeup eventfd, so a request posted while
247/// it is parked brings it back round its loop, which publishes the entry
248/// (see [`crate::policy_control`]).
249///
250/// `sizing` sets the ring's depth and the provided-buffer pool (see
251/// [`RingSizing`]). Returns once the reader holds its io_uring instance
252/// and buffer pool, or with the error that kept it from getting them —
253/// a startup error, not a dead thread: the kernel charges ring memory
254/// against the locked-memory limit, and running out of it is an
255/// operator-fixable condition the node has to report.
256#[allow(clippy::too_many_arguments)]
257pub fn spawn_reader<A: Application, R: AsRawFd + Send + 'static>(
258 mut producer: ring::Producer<InputSlot<A::Event>>,
259 decoder: RequestDecoderArc<A>,
260 halt: HaltGate,
261 early_replies: EarlyReplySender<A::Report>,
262 metrics: Arc<IngressMetrics>,
263 ack_policy: BootedClaim,
264 control_tx: mpsc::Sender<ControlEvent>,
265 core: usize,
266 connection_timeout: Option<Duration>,
267 tick_cadence: Option<Duration>,
268 sizing: RingSizing,
269 shutdown: Arc<AtomicBool>,
270) -> std::io::Result<UringReaderHandle<R>>
271where
272 A::Event: Send + Sync + 'static,
273 A::Report: Send + 'static,
274{
275 let (tx, rx) = mpsc::channel();
276
277 let raw_event_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
278 if raw_event_fd < 0 {
279 let err = std::io::Error::last_os_error();
280 return Err(std::io::Error::new(
281 err.kind(),
282 format!("uring-reader: cannot create its wakeup eventfd: {err}"),
283 ));
284 }
285
286 // SAFETY: `eventfd` returned a fresh descriptor (checked non-negative
287 // above) that nothing else owns, so transferring ownership to `OwnedFd`
288 // is sound. From here the raw number is never closed by hand — the
289 // descriptor lives exactly as long as the last `Arc` holder.
290 let event_fd = Arc::new(unsafe { OwnedFd::from_raw_fd(raw_event_fd) });
291 let wakeup_fd = Arc::clone(&event_fd);
292 let shutdown_clone = Arc::clone(&shutdown);
293 // Attached here, on this thread, which still owns the producer alone
294 // (a policy staged during bring-up is journaled now), with the
295 // eventfd: if the thread never starts, the publisher is dropped with
296 // its closure and detaches.
297 let ack_policy = ack_policy.attach(&mut producer, &halt, Some(Arc::clone(&event_fd)));
298
299 // The thread's startup report. The ring has to be created on the
300 // thread that submits to it (it is SINGLE_ISSUER: the kernel binds
301 // it to its creator, and a ring moved across threads fails every
302 // submit), so the spawner cannot create it itself and learns the
303 // outcome here instead. Capacity one so the report never blocks the
304 // worker on the spawner reaching `recv`.
305 let (ready_tx, ready_rx) = mpsc::sync_channel::<std::io::Result<()>>(1);
306
307 // `core == 0` is the unpinned sentinel, which puts the thread on the
308 // process's home CPU mask — see the `melin_app::affinity` module docs.
309 let handle = melin_app::affinity::spawn_placed("uring-reader", core, move || {
310 reader_loop::<A, R>(
311 rx,
312 wakeup_fd,
313 producer,
314 &*decoder,
315 &halt,
316 early_replies,
317 &metrics,
318 ack_policy,
319 &control_tx,
320 connection_timeout,
321 tick_cadence,
322 sizing,
323 ready_tx,
324 &shutdown_clone,
325 );
326 })
327 .map_err(|e| {
328 let e = std::io::Error::from(e);
329 std::io::Error::new(
330 e.kind(),
331 format!("uring-reader: cannot start its thread: {e}"),
332 )
333 })?;
334
335 if let Err(e) = await_startup("uring-reader", &ready_rx) {
336 // The thread has returned (or panicked) by now; a panic was
337 // already reported by the panic hook, and the startup error is
338 // what the caller needs, so the join result adds nothing.
339 let _ = handle.join();
340 return Err(e);
341 }
342
343 Ok(UringReaderHandle {
344 tx,
345 event_fd,
346 join_handle: Some(handle),
347 shutdown,
348 })
349}
350
351// ---------------------------------------------------------------------------
352// Slab-based connection storage
353// ---------------------------------------------------------------------------
354
355/// Per-connection state for multishot io_uring recv + incremental frame parsing.
356struct ConnectionEntry<R> {
357 connection_id: u64,
358 addr: SocketAddr,
359 /// The client's role, from the auth handshake. Checked per-request
360 /// by the decoder on the reader thread (cold path), zero cost on the
361 /// matching stage.
362 role: ClientRole<RoleId>,
363 /// FxHash of the client's Ed25519 public key. Copied into every
364 /// InputSlot as the submitting key's identity.
365 key_hash: u64,
366 /// Owned reader — keeps the fd alive. Dropping closes the fd.
367 _reader: R,
368 fd: RawFd,
369 /// Bytes received and not yet processed, and what is left of a shed
370 /// tail: the input state the DPDK poll loop keeps too.
371 ingress: ClientIngress,
372 /// Where the connection's receive stands with the kernel: armed,
373 /// between two arms, parked, or dying.
374 recv: RecvState,
375 /// Last time any data was received on this connection. Used for
376 /// idle timeout detection.
377 last_activity: Instant,
378}
379
380/// Where a connection's receive stands with the kernel.
381///
382/// One enum rather than flags, so a parked connection cannot be re-armed
383/// or freed by the paths written for the others: every change goes
384/// through a method here, each an exhaustive `match`, and the reader acts
385/// on the step it returns.
386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387enum RecvState {
388 /// A multishot receive is armed: the kernel posts CQEs for it until
389 /// one without `IORING_CQE_F_MORE`.
390 Armed,
391 /// No receive armed, none owed: the multishot ended (pool exhaustion,
392 /// or the kernel's own decision) and is re-armed in the same drain.
393 Idle,
394 /// Parked (see `client_frames`): the early-reply queue had no room for
395 /// an answer, so the connection is not read, and its buffer is retried
396 /// on every wake of the reader. `cancelling`: the multishot's
397 /// `AsyncCancel` is in flight, so the kernel still owes its terminal
398 /// CQE, and data CQEs that land before it are kept. `since`: when it
399 /// parked, for the long-park report.
400 Parked { cancelling: bool, since: Instant },
401 /// A retry came back with the buffer processed while the park's cancel
402 /// was still in flight: the receive is re-armed on the cancelled one's
403 /// terminal CQE, never before, so two multishots never feed one buffer.
404 Resuming,
405 /// Torn down while the kernel still owes a terminal CQE. The slab index
406 /// must stay allocated until it arrives: freed early, the LIFO free
407 /// list hands the index to a new registration while the kernel can
408 /// still post CQEs carrying it, and the old peer's bytes would be
409 /// parsed under the new connection's identity and permissions (audit
410 /// review F1). Its CQEs recycle their buffers and are otherwise
411 /// discarded.
412 Dying,
413}
414
415/// What a receive CQE carries, as far as the receive state cares.
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417enum RecvCompletion {
418 /// Bytes, in a provided buffer.
419 Data,
420 /// Nothing, and not the end of the connection: the pool ran dry at
421 /// buffer selection (`ENOBUFS`, nothing read: the bytes wait in the
422 /// socket), the park's cancel landed (`ECANCELED`), or a data CQE
423 /// without a buffer.
424 Empty,
425 /// The peer closed the connection (0), or the receive failed.
426 Closed,
427}
428
429/// What the reader does with a receive CQE ([`RecvState::on_cqe`]). Its
430/// buffer, if it carries one, is recycled whatever the step, once any
431/// bytes are copied out.
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
433enum CqeStep {
434 /// A torn-down connection's CQE: discarded, and the slab index freed
435 /// when it is the terminal one.
436 Discard { free: bool },
437 /// The connection is over: free it and tell the response stage.
438 Remove,
439 /// Append the bytes and process the buffer, then act on
440 /// [`RecvState::after_frames`].
441 Process,
442 /// Append the bytes and nothing more: the connection is parked, and the
443 /// retry processes them.
444 Keep,
445 /// Arm a new multishot receive (the state is `Armed` already).
446 Arm,
447 /// Nothing to do.
448 Nothing,
449}
450
451/// What the reader does once a connection's buffer has been processed
452/// ([`RecvState::after_frames`]).
453#[derive(Clone, Copy, Debug, PartialEq, Eq)]
454enum FramesStep {
455 /// Nothing to do.
456 Nothing,
457 /// Arm a new multishot receive (the state is `Armed` already).
458 Arm,
459 /// Cancel the armed multishot: the connection has parked.
460 Cancel,
461 /// Tear the connection down ([`begin_teardown`]).
462 Teardown,
463}
464
465/// What tearing a connection down takes ([`RecvState::on_teardown`]).
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467enum TeardownStep {
468 /// Already dying: nothing more, no second `Disconnected`.
469 AlreadyDying,
470 /// Nothing can post a CQE for it: free the slab index now.
471 FreeNow,
472 /// Cancel the armed multishot, and free on its terminal CQE.
473 Cancel,
474 /// A cancel is already in flight (the park's): free on its terminal
475 /// CQE, with no second `AsyncCancel`.
476 AwaitCancel,
477}
478
479impl RecvState {
480 /// The state after a receive CQE, and what the reader does with it.
481 /// `terminal`: the CQE ends the armed multishot (no `F_MORE`).
482 fn on_cqe(self, completion: RecvCompletion, terminal: bool) -> (RecvState, CqeStep) {
483 if self == RecvState::Dying {
484 return (self, CqeStep::Discard { free: terminal });
485 }
486 let after = if terminal {
487 self.after_terminal()
488 } else {
489 self
490 };
491 match completion {
492 // Terminal for the armed op, so the index is free to reuse.
493 RecvCompletion::Closed => (after, CqeStep::Remove),
494 RecvCompletion::Empty => match after {
495 RecvState::Idle => (RecvState::Armed, CqeStep::Arm),
496 _ => (after, CqeStep::Nothing),
497 },
498 RecvCompletion::Data => match after {
499 RecvState::Parked { .. } => (after, CqeStep::Keep),
500 _ => (after, CqeStep::Process),
501 },
502 }
503 }
504
505 /// The state once the armed multishot has posted its last CQE.
506 fn after_terminal(self) -> RecvState {
507 match self {
508 RecvState::Armed | RecvState::Resuming => RecvState::Idle,
509 RecvState::Parked { since, .. } => RecvState::Parked {
510 cancelling: false,
511 since,
512 },
513 // Nothing was armed, or the caller frees it.
514 RecvState::Idle | RecvState::Dying => self,
515 }
516 }
517
518 /// The state after the connection's buffer was processed with
519 /// `action` at `now`, and what the reader does next. A parked
520 /// connection that parks again keeps its `since`.
521 fn after_frames(self, action: FrameAction, now: Instant) -> (RecvState, FramesStep) {
522 match action {
523 FrameAction::Disconnect => (self, FramesStep::Teardown),
524 FrameAction::Continue => match self {
525 RecvState::Armed | RecvState::Resuming | RecvState::Dying => {
526 (self, FramesStep::Nothing)
527 }
528 RecvState::Parked {
529 cancelling: true, ..
530 } => (RecvState::Resuming, FramesStep::Nothing),
531 RecvState::Idle
532 | RecvState::Parked {
533 cancelling: false, ..
534 } => (RecvState::Armed, FramesStep::Arm),
535 },
536 FrameAction::Park => match self {
537 RecvState::Armed => (
538 RecvState::Parked {
539 cancelling: true,
540 since: now,
541 },
542 FramesStep::Cancel,
543 ),
544 RecvState::Resuming => (
545 RecvState::Parked {
546 cancelling: true,
547 since: now,
548 },
549 FramesStep::Nothing,
550 ),
551 RecvState::Idle => (
552 RecvState::Parked {
553 cancelling: false,
554 since: now,
555 },
556 FramesStep::Nothing,
557 ),
558 RecvState::Parked { .. } | RecvState::Dying => (self, FramesStep::Nothing),
559 },
560 }
561 }
562
563 /// The state once teardown has begun, and what it takes.
564 fn on_teardown(self) -> (RecvState, TeardownStep) {
565 match self {
566 RecvState::Dying => (self, TeardownStep::AlreadyDying),
567 RecvState::Idle
568 | RecvState::Parked {
569 cancelling: false, ..
570 } => (self, TeardownStep::FreeNow),
571 RecvState::Armed => (RecvState::Dying, TeardownStep::Cancel),
572 RecvState::Resuming
573 | RecvState::Parked {
574 cancelling: true, ..
575 } => (RecvState::Dying, TeardownStep::AwaitCancel),
576 }
577 }
578
579 /// Whether, at `now`, a park's cancel has been in flight for longer
580 /// than [`PARK_CANCEL_DEADLINE`]: the host did not honour it.
581 fn park_cancel_overdue(self, now: Instant) -> bool {
582 match self {
583 RecvState::Parked {
584 cancelling: true,
585 since,
586 } => now.duration_since(since) > PARK_CANCEL_DEADLINE,
587 RecvState::Armed
588 | RecvState::Idle
589 | RecvState::Resuming
590 | RecvState::Dying
591 | RecvState::Parked {
592 cancelling: false, ..
593 } => false,
594 }
595 }
596
597 /// Whether the kernel still owes this connection a CQE: what the
598 /// shutdown quiesce waits out.
599 fn kernel_owes_cqe(self) -> bool {
600 match self {
601 RecvState::Armed
602 | RecvState::Resuming
603 | RecvState::Dying
604 | RecvState::Parked {
605 cancelling: true, ..
606 } => true,
607 RecvState::Idle
608 | RecvState::Parked {
609 cancelling: false, ..
610 } => false,
611 }
612 }
613}
614
615/// Index-stable allocator for connection state. Slab indices are used as
616/// io_uring user_data, avoiding fd-reuse races.
617struct ConnectionSlab<R> {
618 entries: Vec<Option<ConnectionEntry<R>>>,
619 /// Recycled indices for O(1) allocation.
620 free: Vec<usize>,
621}
622
623impl<R> ConnectionSlab<R> {
624 fn new() -> Self {
625 Self {
626 entries: Vec::with_capacity(256),
627 free: Vec::new(),
628 }
629 }
630
631 /// Insert a connection, returning its stable slab index.
632 fn insert(&mut self, entry: ConnectionEntry<R>) -> usize {
633 if let Some(idx) = self.free.pop() {
634 self.entries[idx] = Some(entry);
635 idx
636 } else {
637 let idx = self.entries.len();
638 self.entries.push(Some(entry));
639 idx
640 }
641 }
642
643 fn get_mut(&mut self, idx: usize) -> Option<&mut ConnectionEntry<R>> {
644 self.entries.get_mut(idx).and_then(|e| e.as_mut())
645 }
646
647 /// Remove and return a connection entry, recycling its index.
648 fn remove(&mut self, idx: usize) -> Option<ConnectionEntry<R>> {
649 if let Some(slot) = self.entries.get_mut(idx) {
650 let removed = slot.take();
651 if removed.is_some() {
652 self.free.push(idx);
653 }
654 removed
655 } else {
656 None
657 }
658 }
659}
660
661// ---------------------------------------------------------------------------
662// Main loop
663// ---------------------------------------------------------------------------
664
665/// How the reader should approach the kernel before draining the CQ.
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667enum RingEntry {
668 /// Completions are visible and there is nothing to hand over — drain
669 /// from the mmap'd ring without a syscall.
670 Skip,
671 /// SQEs to submit, but completions are already visible: hand them
672 /// over and come straight back rather than asking to wait.
673 Submit,
674 /// Nothing to process yet — submit whatever is queued and park.
675 SubmitAndWait,
676}
677
678/// The reader's enter policy. Two rules: never park while completions
679/// are already visible, and never skip the syscall while SQEs are
680/// waiting to be handed over.
681#[inline]
682fn ring_entry(sq_pending: usize, cq_ready: bool) -> RingEntry {
683 match (sq_pending > 0, cq_ready) {
684 (false, true) => RingEntry::Skip,
685 (true, true) => RingEntry::Submit,
686 (_, false) => RingEntry::SubmitAndWait,
687 }
688}
689
690/// Main io_uring reader loop. Runs until channel disconnection.
691///
692/// When `tick_cadence` is `Some`, the loop also generates the application's
693/// clock ticks — see [`spawn_reader`] for the rationale.
694#[allow(clippy::too_many_arguments)]
695fn reader_loop<A: Application, R: AsRawFd>(
696 command_rx: mpsc::Receiver<ReaderRegistration<R>>,
697 // Shared with `UringReaderHandle`. Taken by value so this thread keeps
698 // the descriptor alive for as long as it is armed in the ring, and
699 // releases it by dropping the `Arc` rather than by closing the fd.
700 wakeup_fd: Arc<OwnedFd>,
701 mut producer: ring::Producer<InputSlot<A::Event>>,
702 decoder: &dyn ErasedDecoder<A::Event>,
703 halt: &HaltGate,
704 mut early_replies: EarlyReplySender<A::Report>,
705 metrics: &IngressMetrics,
706 mut ack_policy: AckPolicyPublisher,
707 control_tx: &mpsc::Sender<ControlEvent>,
708 connection_timeout: Option<Duration>,
709 tick_cadence: Option<Duration>,
710 sizing: RingSizing,
711 // Startup report to `spawn_reader`: `Ok` once the ring and the buffer
712 // pool are in place, the error otherwise (and the loop never starts).
713 ready: mpsc::SyncSender<std::io::Result<()>>,
714 shutdown: &AtomicBool,
715) {
716 let num_buffers = sizing.provided_buffers();
717 let ring_entries = sizing.ring_entries();
718
719 // Kernel-referenced memory is declared BEFORE the io_uring so it
720 // drops AFTER the ring on every exit path, including panic unwind
721 // (locals drop in reverse declaration order). The kernel holds live
722 // references into all three for as long as the ring fd is open:
723 // armed multishot RECVs select entries from the buf_ring and write
724 // into the pool, and the armed eventfd READ writes its buffer.
725
726 // Eventfd read buffer — boxed for pointer stability across SQE lifetimes.
727 let mut eventfd_buf: Box<[u8; 8]> = Box::new([0u8; 8]);
728
729 // Shared buffer pool for provided buffers. Contiguous allocation of
730 // `num_buffers × BUF_SIZE` bytes. The kernel selects a buffer from
731 // this pool for each recv completion, identified by buffer ID in the
732 // CQE.
733 let mut buffer_pool = vec![0u8; num_buffers as usize * BUF_SIZE].into_boxed_slice();
734
735 // Ring-mapped provided-buffer ring: recycling a consumed buffer is a
736 // shared-memory store, not a ProvideBuffers SQE — see `buf_ring`.
737 // Allocated unconditionally (16 bytes per buffer, page-aligned) so
738 // its declaration precedes the io_uring's even when the legacy
739 // fallback below ends up in use.
740 let mut buf_ring = BufRing::new(num_buffers, buffer_pool.as_mut_ptr(), BUF_SIZE);
741
742 // SINGLE_ISSUER: this thread creates the ring and is the only one
743 // that ever submits — lets the kernel skip SQ locking, and turns any
744 // future cross-thread submission bug into an immediate EEXIST
745 // instead of a silent race. Same rationale as the journal and
746 // replication rings. (COOP_TASKRUN/DEFER_TASKRUN deliberately not
747 // set — see the journal ring's measured rationale in
748 // melin-transport-core::pipeline.)
749 //
750 // A failure here is reported to `spawn_reader` as a startup error
751 // rather than panicking the thread: the usual cause is the
752 // locked-memory limit, which the operator can raise.
753 let mut ring: IoUring = match IoUring::builder().setup_single_issuer().build(ring_entries) {
754 Ok(ring) => ring,
755 Err(e) => {
756 // Dropped send: a spawner that stopped waiting is unwinding,
757 // and the thread returns either way.
758 let _ = ready.send(Err(ring_setup_error("uring-reader", ring_entries, e)));
759 return;
760 }
761 };
762
763 // Prefer the buf_ring; fall back to legacy ProvideBuffers SQEs if
764 // the kernel rejects the registration. Not theoretical: PBUF_RING
765 // needs kernel ≥ 5.19, and some virtualized hosts filter newer
766 // io_uring register opcodes (EINVAL) while reporting a modern
767 // uname. The fallback costs one SQE + one CQE per received chunk —
768 // degraded but fully functional, hence warn (see log conventions).
769 let use_buf_ring = match buf_ring.register(&ring, BUF_GROUP_ID) {
770 Ok(()) => true,
771 Err(e) => {
772 tracing::warn!(
773 error = %e,
774 "buf_ring registration rejected — falling back to legacy \
775 ProvideBuffers recycling (kernel < 5.19, or a hypervisor \
776 filtering io_uring register opcodes)"
777 );
778 if let Err(e) = register_buffer_pool(&mut ring, buffer_pool.as_mut_ptr(), num_buffers) {
779 // Dropped send: as above.
780 let _ = ready.send(Err(std::io::Error::new(
781 e.kind(),
782 format!("uring-reader: cannot provide its receive buffers: {e}"),
783 )));
784 return;
785 }
786 false
787 }
788 };
789
790 // Started. A failed send means `spawn_reader` is no longer waiting —
791 // its thread is unwinding — so nothing would ever register a
792 // connection or stop this loop: return instead of running headless.
793 if ready.send(Ok(())).is_err() {
794 return;
795 }
796
797 let mut slab = ConnectionSlab::<R>::new();
798 // Reverse map for cleanup when a connection's fd needs removal.
799 // HashMap for O(1) lookup by fd. Sized for typical connection counts.
800 let mut fd_to_slab: HashMap<RawFd, usize> = HashMap::with_capacity(256);
801
802 // Pre-allocated CQE collection buffer. We must collect CQEs before
803 // processing because the CQ borrow must end before pushing new SQEs.
804 // Stores (user_data, result, flags) — flags needed for buffer ID and
805 // multishot continuation. Sized to the CQ depth (2× the SQ) so even
806 // a maximal drain never reallocates mid-loop.
807 let mut cqes: Vec<(u64, i32, u32)> = Vec::with_capacity(ring_entries as usize * 2);
808
809 // Submit the initial eventfd read so we wake on first connection.
810 // `eventfd_armed` mirrors whether that READ is pushed/armed — the
811 // kernel owes its CQE and writes `eventfd_buf` when it lands — and
812 // is maintained at every push/consume site so the teardown quiesce
813 // below can prove when the buffer is free of kernel references.
814 push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
815 let mut eventfd_armed = true;
816
817 // Stage histograms via the global registry. `publish` is the
818 // narrow ring-publish call cost (lightweight, gated on
819 // `latency-trace`); `ingest` is the full per-frame reader cost
820 // and feeds the bench's tick-to-trade decomposition (heavier,
821 // gated on `tick-to-trade`).
822 #[cfg(feature = "latency-trace")]
823 let mut publish_rec =
824 melin_transport_core::trace::register_stage("reader: publish (decode → disruptor publish)");
825 #[cfg(feature = "tick-to-trade")]
826 let mut ingest_rec =
827 melin_transport_core::trace::register_stage("reader: ingest (recv_ts → publish complete)");
828 // Paces the recorder flush at the tail of each loop iteration. This
829 // thread parks in `submit_and_wait` when there is no traffic, so
830 // without an explicit flush its samples never reach the registry.
831 #[cfg(feature = "latency-trace")]
832 let mut last_stats_flush = Instant::now();
833
834 // Coarse gate for timeout scanning — avoids scanning on every
835 // submit_and_wait return during high throughput.
836 let mut last_timeout_scan = Instant::now();
837 // Pre-allocated buffer for stale connection indices to avoid
838 // heap allocation inside the hot loop.
839 let mut stale: Vec<usize> = Vec::new();
840
841 // Parking state. `any_parked` is set whenever a connection parks and
842 // recomputed by each retry pass, so it may overstate (a parked
843 // connection removed since) but never understate: an overstatement
844 // costs one pass and one timer. `park_retry_ts` is the timer's length;
845 // like `tick_ts` it must outlive the submit that reads it, so it lives
846 // as long as the loop.
847 let mut any_parked = false;
848 let mut park_retry_armed = false;
849 let park_retry_ts = types::Timespec::new()
850 .sec(PARK_RETRY.as_secs())
851 .nsec(PARK_RETRY.subsec_nanos());
852 let mut long_park_report = LongParkReport::default();
853
854 // Tick generator state. `tick_schedule` holds the monotonic instant the
855 // next `JournalEvent::Tick` should fire and the floor that keeps the
856 // published wall-clock timestamps strictly monotonic (NTP can step the
857 // wall clock backwards) — shared with the DPDK poll thread, see
858 // `TickSchedule`. `tick_armed` tracks whether
859 // an `IORING_OP_TIMEOUT` SQE is currently pending; we keep at most one.
860 //
861 // `tick_ts` lives across loop iterations because the kernel reads its
862 // bytes via the SQE's addr field at submit time, not at push time. If
863 // we declared it inside the `if !tick_armed` arm-timeout block, the
864 // value would be dropped before the `submit_and_wait` below — the
865 // kernel would then read freed stack memory. (See `md-gateway` for the
866 // same pattern: it stores Timespec as a long-lived struct field.)
867 let tick_enabled = tick_cadence.is_some();
868 let cadence = tick_cadence.unwrap_or(Duration::ZERO);
869 let mut tick_schedule = TickSchedule::new(cadence, Instant::now());
870 let mut tick_armed = false;
871 // Arm the very first timeout here, before entering the loop. This both
872 // (a) makes the initial `tick_ts` value actually read by the kernel
873 // (silencing the unused-assignment lint, since rustc cannot see kernel
874 // pointer reads) and (b) ensures the first `submit_and_wait` returns at
875 // the cadence even if no client traffic ever arrives.
876 let mut tick_ts = types::Timespec::new()
877 .sec(cadence.as_secs())
878 .nsec(cadence.subsec_nanos());
879 if tick_enabled {
880 let sqe = opcode::Timeout::new(&tick_ts)
881 .build()
882 .user_data(TICK_TIMEOUT_TOKEN);
883 unsafe {
884 ring.submission()
885 .push(&sqe)
886 .expect("io_uring SQ full while arming initial tick timeout");
887 }
888 tick_armed = true;
889 tracing::info!(
890 cadence_ms = cadence.as_millis() as u64,
891 "tick generator integrated into reader thread"
892 );
893 }
894
895 loop {
896 if shutdown.load(Ordering::Relaxed) {
897 break;
898 }
899
900 // An `ACK-POLICY` request: journaled here, by the ring's one
901 // producer, before parking again. One load when none waits; the
902 // requester's eventfd write is what brought a parked loop here.
903 ack_policy.poll(&mut producer, halt);
904
905 // Tick generator: emit any due tick before sleeping again. Done before
906 // the timeout-arm so that a freshly-emitted tick re-arms a timeout for
907 // the *new* deadline.
908 if tick_enabled {
909 if tick_schedule.publish_if_due(Instant::now(), unix_epoch_nanos, &mut producer) {
910 // The previous timeout (if any) is now stale; let it fire and
911 // be ignored, then arm a new one below.
912 tick_armed = false;
913 }
914
915 if !tick_armed {
916 let remaining = tick_schedule
917 .next_deadline()
918 .saturating_duration_since(Instant::now());
919 // Update the loop-scoped Timespec in place. The kernel reads
920 // it via the SQE's addr pointer on submit_and_wait below, so
921 // the binding must outlive that call (it does — outer scope).
922 tick_ts = types::Timespec::new()
923 .sec(remaining.as_secs())
924 .nsec(remaining.subsec_nanos());
925 let sqe = opcode::Timeout::new(&tick_ts)
926 .build()
927 .user_data(TICK_TIMEOUT_TOKEN);
928 // SAFETY: `tick_ts` is loop-scoped and outlives the
929 // submit that reads it (see its declaration).
930 unsafe { push_sqe(&mut ring, &sqe, "the tick TIMEOUT") };
931 tick_armed = true;
932 }
933 }
934
935 // While a connection is parked, nothing but this timer is sure to
936 // bring the loop back round to retry it.
937 if any_parked && !park_retry_armed {
938 let sqe = opcode::Timeout::new(&park_retry_ts)
939 .build()
940 .user_data(PARK_RETRY_TOKEN);
941 // SAFETY: `park_retry_ts` lives as long as the loop, past the
942 // submit that reads it.
943 unsafe { push_sqe(&mut ring, &sqe, "the park-retry TIMEOUT") };
944 park_retry_armed = true;
945 }
946
947 // Enter the kernel only when there is a reason to.
948 //
949 // Under load the CQ usually already holds completions by the
950 // time we get here: in the ring's default task-work mode the
951 // kernel posts them while this thread is in userspace parsing.
952 // Reading that is pure userspace — the CQ is mmap'd and
953 // `completion()` loads the tail with `Acquire` — so when there
954 // is also nothing to hand over, the `io_uring_enter` is a
955 // ~200 ns mode switch that buys nothing. The replication sender
956 // measured the same skip at ~6 % of its thread's cycles
957 // (`tcp_sender.rs`); this loop makes one enter per drain.
958 //
959 // When there ARE SQEs to submit we still enter, but we do not
960 // ask the kernel to *wait* if completions are already visible.
961 // Only a genuinely empty CQ parks the thread.
962 //
963 // Note this is the half of the io_uring-audit item that pairs
964 // *against* `DEFER_TASKRUN`: deferred task work only runs on an
965 // enter with GETEVENTS, so under that flag the CQ would be empty
966 // here and the skip would simply never fire.
967 let sq_pending = ring.submission().len();
968 let cq_ready = !ring.completion().is_empty();
969 let entered = match ring_entry(sq_pending, cq_ready) {
970 RingEntry::Skip => Ok(0),
971 RingEntry::Submit => ring.submit(),
972 RingEntry::SubmitAndWait => ring.submit_and_wait(1),
973 };
974 match entered {
975 Ok(_) => {}
976 Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
977 Err(e) => {
978 error!(error = %e, "io_uring submit/wait error");
979 break;
980 }
981 }
982
983 // Drain all available CQEs into the pre-allocated buffer.
984 // Must collect before processing because the CQ borrow must end
985 // before we can push new SQEs to the SQ.
986 cqes.clear();
987 cqes.extend(
988 ring.completion()
989 .map(|cqe| (cqe.user_data(), cqe.result(), cqe.flags())),
990 );
991
992 let batch_now = Instant::now();
993 // One wall-clock read per CQE batch instead of per request: at
994 // peak request rates a per-request `unix_epoch_nanos()` (vDSO
995 // `clock_gettime(CLOCK_REALTIME)`) showed up in the primary's
996 // profile. All requests in the same batch share the timestamp —
997 // precision loss is bounded by the CQE-drain cadence. The timestamp
998 // drives the application's clock but orders nothing (the pipeline
999 // orders by sequence), so that resolution is enough.
1000 let batch_wall_ns = unix_epoch_nanos();
1001
1002 for &(token, result, flags) in &cqes {
1003 // ── Tick timeout ──
1004 // The CQE is just a wakeup signal — the actual tick emission
1005 // happens at the top of the next loop iteration via the
1006 // deadline check, so the time the tick is stamped with reflects
1007 // unix_epoch_nanos at fire time, not at submit time.
1008 if token == TICK_TIMEOUT_TOKEN {
1009 tick_armed = false;
1010 continue;
1011 }
1012
1013 // ── Park-retry timeout ──
1014 // A wakeup and nothing more: the retry pass after the drain
1015 // runs on every wake.
1016 if token == PARK_RETRY_TOKEN {
1017 park_retry_armed = false;
1018 continue;
1019 }
1020
1021 // ── Legacy ProvideBuffers completion (fallback mode only) ──
1022 if token == PROVIDE_BUFS_TOKEN {
1023 if result < 0 {
1024 error!(error = result, "ProvideBuffers failed");
1025 }
1026 continue;
1027 }
1028
1029 // ── AsyncCancel completion ──
1030 if token == CANCEL_TOKEN {
1031 // Nothing to do: the cancelled op's own terminal CQE
1032 // drives the state machine; ENOENT/EALREADY just mean
1033 // it already finished on its own. A filtered cancel
1034 // (EINVAL) leaves a parked connection's receive armed;
1035 // the retry pass closes it (`PARK_CANCEL_DEADLINE`).
1036 continue;
1037 }
1038
1039 // ── Eventfd wakeup ──
1040 if token == EVENTFD_TOKEN {
1041 // This CQE is the armed READ completing; the re-arm at
1042 // the end of the branch replaces it before anything can
1043 // observe the gap, so `eventfd_armed` stays `true`
1044 // through the pair. The teardown drain clears it for
1045 // the one case where a completion is never processed
1046 // here (a CQE still queued when the loop breaks).
1047 if result >= 0 {
1048 // Process all pending registrations.
1049 while let Ok(reg) = command_rx.try_recv() {
1050 let fd = reg.reader.as_raw_fd();
1051 let entry = ConnectionEntry {
1052 connection_id: reg.connection_id.0,
1053 addr: reg.addr,
1054 role: reg.role,
1055 key_hash: reg.key_hash,
1056 fd,
1057 _reader: reg.reader,
1058 ingress: ClientIngress::new(Vec::with_capacity(MAX_FRAME_SIZE + 4)),
1059 // Armed just below.
1060 recv: RecvState::Armed,
1061 last_activity: Instant::now(),
1062 };
1063 let idx = slab.insert(entry);
1064 fd_to_slab.insert(fd, idx);
1065
1066 // Submit multishot RECV for this connection.
1067 push_recv_multi(&mut ring, fd, idx);
1068 }
1069 } else {
1070 error!(error = result, "eventfd read error");
1071 }
1072
1073 // Re-submit eventfd read for the next wakeup
1074 // (`eventfd_armed` stays `true` — see above).
1075 push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
1076 continue;
1077 }
1078
1079 // ── Connection multishot RECV completion ──
1080
1081 let slab_idx = token as usize;
1082 // A cleared F_MORE means THIS armed op posts no further CQEs.
1083 let terminal = (flags & IORING_CQE_F_MORE) == 0;
1084
1085 // Extract the buffer (if any) before acting on `result`: a
1086 // buffer can ride ANY recv CQE, and one that is not recycled
1087 // is leaked from the pool forever. On 6.8 the kernel
1088 // recycles internally before posting error/EOF CQEs, but the
1089 // supported floor is 5.19, where that guarantee could not be
1090 // established — the defensive recycle is one branch.
1091 let buf_id = if (flags & IORING_CQE_F_BUFFER) != 0 {
1092 Some((flags >> IORING_CQE_BUFFER_SHIFT) as usize)
1093 } else {
1094 None
1095 };
1096
1097 let Some(entry) = slab.get_mut(slab_idx) else {
1098 // No connection holds the index: an index is freed only
1099 // once nothing armed can post for it, so this does not
1100 // happen; its buffer goes back all the same.
1101 if let Some(bid) = buf_id {
1102 recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
1103 }
1104 continue;
1105 };
1106
1107 // `ENOBUFS`: the pool ran dry at buffer-selection time, and
1108 // the multishot ended WITHOUT reading — the bytes sit in the
1109 // socket buffer. Not a client error, and it must not
1110 // disconnect (a bare `result <= 0 ⇒ remove` here dropped
1111 // innocent clients under burst): the receive is re-armed, and
1112 // recycles from data CQEs earlier in this drain have already
1113 // refilled the buf_ring (publication is immediate), while the
1114 // re-arm SQE submits after the whole drain, so the retry finds
1115 // buffers. `ECANCELED`: a park's cancel landed. A data CQE
1116 // without a buffer cannot happen with buffer-select receives;
1117 // defensively, it is taken as no bytes.
1118 let completion = if result == -libc::ENOBUFS || result == -libc::ECANCELED {
1119 RecvCompletion::Empty
1120 } else if result <= 0 {
1121 RecvCompletion::Closed
1122 } else if buf_id.is_none() {
1123 debug!(slab_idx, "recv CQE without buffer flag");
1124 RecvCompletion::Empty
1125 } else {
1126 RecvCompletion::Data
1127 };
1128 let (recv, step) = entry.recv.on_cqe(completion, terminal);
1129 entry.recv = recv;
1130 let fd = entry.fd;
1131
1132 let (Some(buf_id), CqeStep::Process | CqeStep::Keep) = (buf_id, step) else {
1133 // No bytes to take: a buffer riding the CQE (one can ride
1134 // ANY recv CQE, as above) goes straight back.
1135 if let Some(bid) = buf_id {
1136 recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
1137 }
1138 match step {
1139 CqeStep::Discard { free } => {
1140 // A torn-down connection: its bytes belong to a
1141 // repudiated peer, and its index is freed only at
1142 // the terminal CQE, so it cannot be handed to a
1143 // new registration while the kernel can still post
1144 // CQEs carrying it.
1145 if free && let Some(dead) = slab.remove(slab_idx) {
1146 debug!(
1147 connection_id = dead.connection_id,
1148 "teardown complete, slab index released"
1149 );
1150 }
1151 }
1152 CqeStep::Remove => {
1153 // Disconnect (0) or error (negative errno) —
1154 // terminal for the armed op, so the index is
1155 // immediately safe to free.
1156 if let Some(removed) = slab.remove(slab_idx) {
1157 if result == 0 {
1158 debug!(
1159 connection_id = removed.connection_id,
1160 addr = %removed.addr,
1161 "client disconnected"
1162 );
1163 } else {
1164 debug!(
1165 connection_id = removed.connection_id,
1166 addr = %removed.addr,
1167 error = result,
1168 "recv error"
1169 );
1170 }
1171 fd_to_slab.remove(&removed.fd);
1172 // Dropped error: a dead control channel means
1173 // the response stage is gone and the server is
1174 // shutting down.
1175 let _ = control_tx.send(ControlEvent::Disconnected {
1176 connection_id: removed.connection_id,
1177 });
1178 }
1179 }
1180 CqeStep::Arm => push_recv_multi(&mut ring, fd, slab_idx),
1181 // `Process` and `Keep` without a buffer are `Empty`'s,
1182 // which never yields them.
1183 CqeStep::Nothing | CqeStep::Process | CqeStep::Keep => {}
1184 }
1185 continue;
1186 };
1187
1188 let n = result as usize;
1189
1190 // Trace timestamp: the moment the kernel handed us this recv's
1191 // bytes. Captured once per CQE — not per frame — and stamped
1192 // onto every InputSlot parsed from this buffer below, so the
1193 // reader-ingest and server-e2e stages measure from true wire
1194 // receipt (frame decode included) rather than re-sampling after
1195 // each decode (which excluded decode and drifted forward for
1196 // later frames in a multi-frame recv).
1197 #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
1198 let recv_ts = melin_transport_core::trace::mono_trace_ns();
1199
1200 // Any successful recv resets the idle timeout.
1201 entry.last_activity = batch_now;
1202
1203 // Copy received data from the shared buffer pool into the
1204 // connection's parse buffer: kept even while it is parked (a
1205 // CQE that landed before the park's cancel), for the retry.
1206 let buf_start = buf_id * BUF_SIZE;
1207 entry
1208 .ingress
1209 .extend_from_slice(&buffer_pool[buf_start..buf_start + n]);
1210
1211 // Recycle the consumed buffer. Must happen after the copy-out
1212 // above — from this line on the kernel may write fresh recv
1213 // data into the slot.
1214 recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, buf_id);
1215
1216 if step == CqeStep::Keep {
1217 continue;
1218 }
1219
1220 // Extract and publish complete frames.
1221 let action = process_frames::<A, R>(
1222 entry,
1223 &mut producer,
1224 decoder,
1225 halt,
1226 &mut early_replies,
1227 metrics,
1228 batch_wall_ns,
1229 recv_ts,
1230 #[cfg(feature = "latency-trace")]
1231 &mut publish_rec,
1232 #[cfg(feature = "tick-to-trade")]
1233 &mut ingest_rec,
1234 );
1235 let (recv, step) = entry.recv.after_frames(action, batch_now);
1236 entry.recv = recv;
1237 // A new park: a parked connection's bytes are kept, not
1238 // processed (`CqeStep::Keep`), so none was parked before this.
1239 if matches!(recv, RecvState::Parked { .. }) {
1240 any_parked = true;
1241 metrics.record_park();
1242 debug!(connection_id = entry.connection_id, "connection parked");
1243 }
1244 apply_frames_step(
1245 &mut ring,
1246 &mut slab,
1247 &mut fd_to_slab,
1248 control_tx,
1249 slab_idx,
1250 step,
1251 );
1252 }
1253
1254 // Retry every parked connection's buffer: on the park-retry
1255 // timer, and on any other wake. One that comes back processed is
1256 // read again; one that parks again stays parked. The pass counts
1257 // those still parked for the gauge: it runs whenever one may be
1258 // (see `any_parked`), so a gauge left above zero is corrected by
1259 // the next pass, and one that finds none sets it to zero.
1260 if any_parked {
1261 any_parked = false;
1262 let mut still_parked = 0u64;
1263 let mut long_parked = 0usize;
1264 #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
1265 let recv_ts = melin_transport_core::trace::mono_trace_ns();
1266 for idx in 0..slab.entries.len() {
1267 let Some(entry) = slab.get_mut(idx) else {
1268 continue;
1269 };
1270 let RecvState::Parked { since, .. } = entry.recv else {
1271 continue;
1272 };
1273 if entry.recv.park_cancel_overdue(batch_now) {
1274 tracing::warn!(
1275 connection_id = entry.connection_id,
1276 "a parked connection's receive cancel went unanswered (is io_uring \
1277 AsyncCancel filtered on this host?): closing the connection"
1278 );
1279 begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
1280 continue;
1281 }
1282 let action = process_frames::<A, R>(
1283 entry,
1284 &mut producer,
1285 decoder,
1286 halt,
1287 &mut early_replies,
1288 metrics,
1289 batch_wall_ns,
1290 recv_ts,
1291 #[cfg(feature = "latency-trace")]
1292 &mut publish_rec,
1293 #[cfg(feature = "tick-to-trade")]
1294 &mut ingest_rec,
1295 );
1296 let (recv, step) = entry.recv.after_frames(action, batch_now);
1297 entry.recv = recv;
1298 match action {
1299 // Still parked, since it first parked.
1300 FrameAction::Park => {
1301 any_parked = true;
1302 still_parked += 1;
1303 if LongParkReport::parked_long(since, batch_now) {
1304 long_parked += 1;
1305 }
1306 }
1307 FrameAction::Continue => {
1308 // Not read while parked: its idle clock restarts
1309 // now, not at its last bytes.
1310 entry.last_activity = batch_now;
1311 debug!(connection_id = entry.connection_id, "connection resumed");
1312 }
1313 // Torn down below.
1314 FrameAction::Disconnect => {}
1315 }
1316 apply_frames_step(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx, step);
1317 }
1318 metrics.set_parked(still_parked);
1319 long_park_report.report(long_parked, batch_now);
1320 }
1321
1322 // Scan for idle connections that have exceeded the timeout.
1323 // Coarse gate: only scan once per second to avoid unnecessary
1324 // iteration during high-throughput phases when submit_and_wait
1325 // returns immediately with CQEs.
1326 if let Some(timeout) = connection_timeout {
1327 let now = Instant::now();
1328 if now.duration_since(last_timeout_scan) >= Duration::from_secs(1) {
1329 last_timeout_scan = now;
1330 stale.clear();
1331 for (idx, slot) in slab.entries.iter().enumerate() {
1332 // Dying entries are already mid-teardown: their
1333 // `last_activity` stays stale by design, and a second
1334 // teardown would emit a duplicate Disconnected. A
1335 // parked connection is not idle: the node is not
1336 // reading it.
1337 if let Some(entry) = slot
1338 && !matches!(entry.recv, RecvState::Dying | RecvState::Parked { .. })
1339 && now.duration_since(entry.last_activity) > timeout
1340 {
1341 debug!(
1342 connection_id = entry.connection_id,
1343 addr = %entry.addr,
1344 "connection timed out"
1345 );
1346 stale.push(idx);
1347 }
1348 }
1349 for &idx in &stale {
1350 begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
1351 }
1352 }
1353 }
1354
1355 // Hand buffered latency samples to the stats registry before
1356 // parking in `submit_and_wait` at the top of the next
1357 // iteration — once traffic stops this thread can sleep in the
1358 // kernel indefinitely, which is precisely when the bench
1359 // scrapes /stats-dump. Reuses the per-batch clock read.
1360 #[cfg(feature = "latency-trace")]
1361 if batch_now.duration_since(last_stats_flush)
1362 >= melin_transport_core::trace::IDLE_FLUSH_INTERVAL
1363 {
1364 last_stats_flush = batch_now;
1365 publish_rec.flush();
1366 #[cfg(feature = "tick-to-trade")]
1367 ingest_rec.flush();
1368 }
1369 }
1370
1371 // ── Teardown quiesce ────────────────────────────────────────────
1372 // Armed operations hold kernel pointers into the buffer pool, the
1373 // buf_ring memory, and the eventfd buffer. Closing the ring fd only
1374 // triggers ASYNCHRONOUS cancellation (the kernel's ring-exit work),
1375 // which can still be touching those allocations after this function
1376 // returns and frees them — the same corruption class fixed on both
1377 // replication transports (see `crate::uring_teardown`). Quiesce
1378 // *provably*: wake every armed operation, then reap CQEs until none
1379 // is owed, and leak the buffers if that cannot be shown within the
1380 // deadline. "CQ went quiet" is not proof — a wake that silently
1381 // failed leaves armed ops and a quiet CQ.
1382 //
1383 // The wake cannot rely on the cancel opcode alone: some hosts
1384 // filter io_uring opcodes (the reason the buf_ring path has a
1385 // legacy fallback), and a filtered cancel completes with EINVAL
1386 // while the armed operations live on. So every operation that has
1387 // to be proven down gets a wake it must answer: `shutdown(2)` on
1388 // each connection socket completes its multishot RECV with EOF,
1389 // and a wakeup write completes the armed eventfd READ. The cancel
1390 // is still pushed as an accelerator where the host honours it. The
1391 // tick timeout is not in the proof at all — see `owed` below.
1392 // Panic unwind skips all of this and accepts the (tiny,
1393 // process-is-dying) exit-work window; declaration order still
1394 // closes the ring fd before the frees.
1395 let proven = {
1396 let cancel_all = opcode::AsyncCancel2::new(types::CancelBuilder::any())
1397 .build()
1398 .user_data(CANCEL_TOKEN);
1399 unsafe {
1400 // Ignore a full SQ: the wakes below do not depend on it.
1401 let _ = ring.submission().push(&cancel_all);
1402 }
1403 for entry in slab.entries.iter().flatten() {
1404 crate::uring_teardown::wake_pending_ops(entry.fd);
1405 }
1406 // Completes the armed eventfd READ into `eventfd_buf` (still
1407 // alive). Best-effort: on failure the cancel is the fallback.
1408 let wake: u64 = 1;
1409 unsafe {
1410 libc::write(
1411 wakeup_fd.as_raw_fd(),
1412 &wake as *const u64 as *const libc::c_void,
1413 8,
1414 );
1415 }
1416 // Flush SQEs pushed but never submitted (re-arms from the last
1417 // batch, the cancel above): until they reach the kernel no CQE
1418 // is coming for them, but their armed-state flags are already
1419 // set, so abandoning them in the SQ would stall the proof
1420 // below. On a submit error the flags stay set and the deadline
1421 // routes to the leak path — the conservative outcome.
1422 loop {
1423 match ring.submit() {
1424 Ok(_) => break,
1425 Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1426 Err(_) => break,
1427 }
1428 }
1429 let mut backoff = crate::uring_teardown::DrainBackoff::new();
1430 loop {
1431 for cqe in ring.completion() {
1432 match cqe.user_data() {
1433 EVENTFD_TOKEN => eventfd_armed = false,
1434 // None of these touch the allocations freed below,
1435 // so reaping them settles nothing: cancel results
1436 // carry no information, legacy ProvideBuffers
1437 // completions only registered addresses, and the
1438 // tick timeout reads nothing after submit (see
1439 // `owed`), nor does the park-retry timeout, for the
1440 // same reason.
1441 PROVIDE_BUFS_TOKEN | CANCEL_TOKEN | TICK_TIMEOUT_TOKEN | PARK_RETRY_TOKEN => {}
1442 // Connection tokens are slab indices.
1443 idx => {
1444 // A terminal CQE retires that connection's arm.
1445 // Data CQEs landing mid-teardown are discarded
1446 // and their buffers deliberately not recycled —
1447 // nothing re-arms, so the pool cannot be
1448 // selected from again once every arm is down.
1449 // Whatever state it was in, the kernel owes it
1450 // nothing more: `Idle` says so (the entry is never
1451 // used again).
1452 if (cqe.flags() & IORING_CQE_F_MORE) == 0
1453 && let Some(entry) = slab.get_mut(idx as usize)
1454 {
1455 entry.recv = RecvState::Idle;
1456 }
1457 }
1458 }
1459 }
1460 // `tick_armed` is deliberately absent: an armed
1461 // `IORING_OP_TIMEOUT` holds no pointer into any of the
1462 // allocations below. The kernel copies its `Timespec` out
1463 // of `tick_ts` when the SQE is *submitted* (which is why
1464 // that binding only has to outlive the submit — see its
1465 // declaration), and the ring's own exit work retires the
1466 // timer. Waiting for it would prove nothing and cost
1467 // plenty: nothing here can wake a timeout, so the only
1468 // things that retire one are it firing at its own cadence
1469 // — `--tick-interval-ms`, operator-set and unbounded — or
1470 // the cancel above, which is exactly what a host that
1471 // filters opcodes refuses. Gating the proof on it would
1472 // hand those hosts a full-deadline stall and a spurious
1473 // "leaking" warning on every clean shutdown.
1474 let owed = eventfd_armed
1475 || slab
1476 .entries
1477 .iter()
1478 .flatten()
1479 .any(|e| e.recv.kernel_owes_cqe());
1480 if !owed {
1481 break true;
1482 }
1483 if !backoff.wait() {
1484 break false;
1485 }
1486 }
1487 };
1488 if !proven {
1489 // The kernel may still write the pool or the eventfd buffer, or
1490 // read buf_ring entries. Leak all three rather than hand the
1491 // allocator memory the kernel still touches — a bounded one-off
1492 // cost on a thread that is exiting anyway. `mem::forget` on the
1493 // `BufRing` also skips its ring-memory dealloc, which is the
1494 // point.
1495 // Report what was still owed, not `tick_armed` — naming a
1496 // timer that was never part of the proof would point the next
1497 // reader at the wrong operation.
1498 tracing::warn!(
1499 eventfd_armed,
1500 armed_connections = slab
1501 .entries
1502 .iter()
1503 .flatten()
1504 .filter(|e| e.recv.kernel_owes_cqe())
1505 .count(),
1506 "io_uring teardown drain did not complete; leaking reader buffers"
1507 );
1508 std::mem::forget(buffer_pool);
1509 std::mem::forget(buf_ring);
1510 std::mem::forget(eventfd_buf);
1511 }
1512
1513 // No `close` here. This thread shares the eventfd with
1514 // `UringReaderHandle`, which writes wakeups to it, and closing it from
1515 // this side left that write racing a reused descriptor number. Dropping
1516 // the `Arc` at the end of this scope releases our share; the descriptor
1517 // itself closes when the handle drops too.
1518}
1519
1520// ---------------------------------------------------------------------------
1521// SQE helpers
1522// ---------------------------------------------------------------------------
1523
1524/// Queue an SQE, first handing the already-queued ones to the kernel if
1525/// the submission queue is full.
1526///
1527/// [`RingSizing`] sizes the SQ for a drain's worth of SQEs on the
1528/// buf_ring path — two per connection plus housekeeping — so there the
1529/// flush fires only when the reader transiently tracks more connections
1530/// than the cap: a dying connection stays in the reader until its cancel
1531/// completes, while the response stage has already released its slot and
1532/// the accept loop may have admitted a replacement. The legacy
1533/// ProvideBuffers fallback adds one SQE per
1534/// recycled buffer on top, which the sizing does not count; this is what
1535/// keeps that path from overrunning the queue. Submitting mid-drain is
1536/// harmless: the CQEs being processed were copied out before any SQE was
1537/// pushed, and the queued SQEs only reach the kernel sooner.
1538///
1539/// Panics only if the submit itself fails, which leaves queued SQEs that
1540/// point into live buffers with nowhere to go.
1541///
1542/// # Safety
1543///
1544/// As for [`io_uring::SubmissionQueue::push`]: every buffer the SQE
1545/// references must stay valid until its operation completes.
1546unsafe fn push_sqe(ring: &mut IoUring, sqe: &io_uring::squeue::Entry, what: &str) {
1547 // SAFETY: forwarded from the caller.
1548 if unsafe { ring.submission().push(sqe) }.is_ok() {
1549 return;
1550 }
1551 loop {
1552 match ring.submit() {
1553 Ok(_) => break,
1554 Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1555 Err(e) => panic!("io_uring submit failed while making room for {what}: {e}"),
1556 }
1557 }
1558 // SAFETY: forwarded from the caller.
1559 unsafe { ring.submission().push(sqe) }.unwrap_or_else(|_| {
1560 panic!("io_uring SQ still full for {what} after submitting every queued SQE")
1561 });
1562}
1563
1564/// Register the provided buffer pool via a legacy ProvideBuffers op —
1565/// the fallback when buf_ring registration is rejected. Submits
1566/// synchronously — called once at startup, before the reader reports
1567/// that it started, and only after the preferred path already failed.
1568fn register_buffer_pool(
1569 ring: &mut IoUring,
1570 pool_ptr: *mut u8,
1571 num_buffers: u16,
1572) -> std::io::Result<()> {
1573 let sqe = opcode::ProvideBuffers::new(pool_ptr, BUF_SIZE as i32, num_buffers, BUF_GROUP_ID, 0)
1574 .build()
1575 .user_data(PROVIDE_BUFS_TOKEN);
1576
1577 // SAFETY: the pool outlives the ring (declared before it in
1578 // `reader_loop`), and ProvideBuffers only records its address.
1579 unsafe {
1580 ring.submission().push(&sqe).map_err(|_| {
1581 std::io::Error::other("io_uring SQ full during buffer pool registration")
1582 })?;
1583 }
1584
1585 loop {
1586 match ring.submit_and_wait(1) {
1587 Ok(_) => break,
1588 Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1589 Err(e) => return Err(e),
1590 }
1591 }
1592
1593 // Check the completion result.
1594 let cqe = ring
1595 .completion()
1596 .next()
1597 .ok_or_else(|| std::io::Error::other("no CQE after ProvideBuffers"))?;
1598 if cqe.result() < 0 {
1599 return Err(std::io::Error::from_raw_os_error(-cqe.result()));
1600 }
1601 Ok(())
1602}
1603
1604/// Re-provide a single consumed buffer back to the pool (legacy fallback
1605/// mode). Pushed to SQ without immediate submission — batched with the
1606/// next submit_and_wait. A drain can recycle up to the whole pool on top
1607/// of the SQEs the ring is sized for, so this is the push that relies on
1608/// [`push_sqe`] making room.
1609fn re_provide_buffer(ring: &mut IoUring, pool_ptr: *mut u8, buf_id: usize) {
1610 let buf_ptr = unsafe { pool_ptr.add(buf_id * BUF_SIZE) };
1611 let sqe = opcode::ProvideBuffers::new(buf_ptr, BUF_SIZE as i32, 1, BUF_GROUP_ID, buf_id as u16)
1612 .build()
1613 .user_data(PROVIDE_BUFS_TOKEN);
1614
1615 // SAFETY: the buffer lies inside the pool, which outlives the ring.
1616 unsafe { push_sqe(ring, &sqe, "a legacy ProvideBuffers recycle") };
1617}
1618
1619/// Return a consumed buffer to the shared pool, in whichever recycle
1620/// mode registration selected.
1621///
1622/// Hard assert, not debug: the bid comes from a kernel CQE, this path
1623/// runs per CQE (off the per-frame budget), and an out-of-range bid
1624/// handed back to the kernel would point a future recv's DMA at
1625/// arbitrary heap — the one failure mode worth an unconditional branch.
1626fn recycle_buffer(
1627 ring: &mut IoUring,
1628 buf_ring: &mut BufRing,
1629 use_buf_ring: bool,
1630 buffer_pool: &[u8],
1631 buf_id: usize,
1632) {
1633 assert!(
1634 buf_id < buffer_pool.len() / BUF_SIZE,
1635 "kernel returned out-of-pool buffer id {buf_id}"
1636 );
1637 if use_buf_ring {
1638 buf_ring.push(buf_id as u16);
1639 } else {
1640 re_provide_buffer(ring, buffer_pool.as_ptr() as *mut u8, buf_id);
1641 }
1642}
1643
1644/// Begin tearing down a connection the reader decided to drop while its
1645/// multishot RECV may still be armed (malformed frame, idle timeout, a
1646/// superseded node), parked or not.
1647///
1648/// The slab index must NOT be freed yet: the kernel can still post CQEs
1649/// carrying it, and the slab's LIFO free list would hand the index to
1650/// the next registration — the old peer's bytes would then be parsed
1651/// under the new connection's identity, key hash, and permissions
1652/// (request injection; audit review F1). Instead: sever the peer,
1653/// mark the entry dying, cancel the armed op (unless a park's cancel is
1654/// already in flight), and let the terminal CQE free the index. When
1655/// nothing is owed there is nothing that can post — free immediately.
1656fn begin_teardown<R>(
1657 ring: &mut IoUring,
1658 slab: &mut ConnectionSlab<R>,
1659 fd_to_slab: &mut HashMap<RawFd, usize>,
1660 control_tx: &mpsc::Sender<ControlEvent>,
1661 idx: usize,
1662) {
1663 let Some(entry) = slab.get_mut(idx) else {
1664 return;
1665 };
1666 let (recv, step) = entry.recv.on_teardown();
1667 if step == TeardownStep::AlreadyDying {
1668 return;
1669 }
1670 entry.recv = recv;
1671 let connection_id = entry.connection_id;
1672 let fd = entry.fd;
1673 // Sever the peer now. The fd itself must stay open until the armed
1674 // op completes (the kernel holds a file reference for it anyway);
1675 // shutdown(2) stops both directions immediately. Best-effort:
1676 // ENOTCONN just means the peer already went away.
1677 unsafe {
1678 libc::shutdown(fd, libc::SHUT_RDWR);
1679 }
1680 fd_to_slab.remove(&fd);
1681 // Dropped error: a dead control channel means the response stage is
1682 // gone and the server is shutting down.
1683 let _ = control_tx.send(ControlEvent::Disconnected { connection_id });
1684
1685 match step {
1686 TeardownStep::Cancel => push_recv_cancel(ring, idx, "a connection teardown's AsyncCancel"),
1687 // The park's cancel is in flight: its terminal CQE frees the index.
1688 TeardownStep::AwaitCancel => {}
1689 // Nothing owed ⇒ no future CQEs can carry this index.
1690 TeardownStep::FreeNow => {
1691 slab.remove(idx);
1692 }
1693 // Returned above, before the peer was told anything twice.
1694 TeardownStep::AlreadyDying => {}
1695 }
1696}
1697
1698/// Act on what [`RecvState::after_frames`] decided for connection `idx`.
1699fn apply_frames_step<R>(
1700 ring: &mut IoUring,
1701 slab: &mut ConnectionSlab<R>,
1702 fd_to_slab: &mut HashMap<RawFd, usize>,
1703 control_tx: &mpsc::Sender<ControlEvent>,
1704 idx: usize,
1705 step: FramesStep,
1706) {
1707 match step {
1708 FramesStep::Nothing => {}
1709 FramesStep::Arm => {
1710 if let Some(entry) = slab.get_mut(idx) {
1711 push_recv_multi(ring, entry.fd, idx);
1712 }
1713 }
1714 // With nothing reading the socket its receive buffer fills, and
1715 // TCP's window closes on the client.
1716 FramesStep::Cancel => push_recv_cancel(ring, idx, "a park's AsyncCancel"),
1717 // Deferred teardown, NOT an immediate slab free — the multishot
1718 // may still be armed and its index must not be reused until the
1719 // terminal CQE (audit review F1).
1720 FramesStep::Teardown => begin_teardown(ring, slab, fd_to_slab, control_tx, idx),
1721 }
1722}
1723
1724/// Push a multishot RECV SQE for connection `idx` on `fd`. The kernel will
1725/// produce CQEs continuously until EOF, error, or buffer pool exhaustion —
1726/// no resubmission needed unless multishot terminates. Only on a
1727/// transition of [`RecvState`] to `Armed`.
1728fn push_recv_multi(ring: &mut IoUring, fd: RawFd, idx: usize) {
1729 let sqe = opcode::RecvMulti::new(types::Fd(fd), BUF_GROUP_ID)
1730 .build()
1731 .user_data(idx as u64);
1732
1733 // SAFETY: buffer-select RECV writes only into the provided-buffer
1734 // pool, which outlives the ring.
1735 unsafe { push_sqe(ring, &sqe, "a multishot RECV") };
1736}
1737
1738/// Cancel connection `idx`'s armed multishot RECV (`what` names why, for
1739/// a failed push). Its terminal CQE, `ECANCELED` or whatever completed
1740/// first, drives the state machine; the cancel's own completion carries
1741/// nothing.
1742fn push_recv_cancel(ring: &mut IoUring, idx: usize, what: &str) {
1743 let sqe = opcode::AsyncCancel::new(idx as u64)
1744 .build()
1745 .user_data(CANCEL_TOKEN);
1746 // SAFETY: AsyncCancel references no memory.
1747 unsafe { push_sqe(ring, &sqe, what) };
1748}
1749
1750/// Push a READ SQE for the eventfd (wakeup notification).
1751fn push_eventfd_read(ring: &mut IoUring, wakeup_fd: RawFd, buf: *mut u8) {
1752 let sqe = opcode::Read::new(types::Fd(wakeup_fd), buf, 8)
1753 .build()
1754 .user_data(EVENTFD_TOKEN);
1755
1756 // SAFETY: `buf` is the boxed eventfd buffer, declared before the
1757 // ring in `reader_loop` so it outlives it.
1758 unsafe { push_sqe(ring, &sqe, "the eventfd READ") };
1759}
1760
1761// ---------------------------------------------------------------------------
1762// Frame parsing
1763// ---------------------------------------------------------------------------
1764
1765/// Extract complete frames from `conn.ingress` and publish them as
1766/// `InputSlot`s, answering through `early_replies` the requests not
1767/// published (see [`process_client_frames`](crate::client_frames::process_client_frames)).
1768/// `batch_wall_ns` is the wall-clock timestamp captured once per CQE batch
1769/// by the caller (see `reader_loop`); all non-query requests published in
1770/// this call share it, sparing the reader a per-request
1771/// `clock_gettime(CLOCK_REALTIME)` on the hot path. Returns what to do
1772/// with the connection: keep reading, close it, or park it.
1773#[allow(clippy::too_many_arguments)]
1774fn process_frames<A: Application, R>(
1775 conn: &mut ConnectionEntry<R>,
1776 producer: &mut ring::Producer<InputSlot<A::Event>>,
1777 decoder: &dyn ErasedDecoder<A::Event>,
1778 halt: &HaltGate,
1779 early_replies: &mut EarlyReplySender<A::Report>,
1780 metrics: &IngressMetrics,
1781 batch_wall_ns: u64,
1782 recv_ts: melin_transport_core::trace::MonoTraceInstant,
1783 #[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
1784 #[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
1785) -> FrameAction {
1786 // Every answer the reader decides on, a busy for a full ring among
1787 // them, goes to the response stage through the early-reply queue: the
1788 // response stage owns all egress on a client socket, so no reader-side
1789 // send can land between the two halves of a partially flushed reply.
1790 crate::client_frames::process_client_frames::<A>(
1791 &mut conn.ingress,
1792 conn.connection_id,
1793 conn.key_hash,
1794 conn.role,
1795 producer,
1796 decoder,
1797 halt,
1798 early_replies,
1799 metrics,
1800 batch_wall_ns,
1801 recv_ts,
1802 #[cfg(feature = "latency-trace")]
1803 publish_rec,
1804 #[cfg(feature = "tick-to-trade")]
1805 ingest_rec,
1806 )
1807}
1808
1809#[cfg(test)]
1810mod tests {
1811 //! Unit tests for [`process_frames`]. The function has several exit
1812 //! paths (normal end, partial parse-buf, pipeline-full, oversize or
1813 //! non-request frame, full early-reply queue), each with subtle
1814 //! batch-commit ordering requirements. These tests pin that behaviour
1815 //! against a synthetic decoder so refactors of the batch path (e.g.
1816 //! moving the batch up to span the whole CQE drain) can't silently
1817 //! regress the "earlier frames must be visible before ServerBusy /
1818 //! disconnect" guarantees.
1819 use super::*;
1820 use crate::client_frames::MAX_REQUEST_BODY;
1821 use crate::early_reply::{EarlyKind, EarlyReply};
1822 use melin_app::auth::NoRoles;
1823 use melin_app::decoder::{Decoded, RequestDecoder};
1824 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1825 use melin_journal::JournalEvent;
1826 use melin_pipeline::ring::DisruptorBuilder;
1827 use melin_transport_core::health::IngressTally;
1828 use melin_transport_core::pipeline::RequestId;
1829 use melin_wire_protocol::control::RefusalKind;
1830 use melin_wire_protocol::control_codec::{
1831 TAG_APP, TAG_CHALLENGE_RESPONSE, TAG_LEN, TAG_RESPONSE_HEARTBEAT,
1832 };
1833 use std::io::{ErrorKind, Read};
1834 use std::os::unix::net::UnixStream;
1835 use std::sync::atomic::AtomicU64;
1836 use std::time::Duration;
1837
1838 /// The reader's enter policy. Each case is a syscall-per-drain
1839 /// decision on the hot path, and getting one wrong is either a lost
1840 /// wakeup (skipping with an empty CQ) or unsubmitted SQEs (skipping
1841 /// with a non-empty SQ) — both silent.
1842 mod ring_entry {
1843 use super::super::{RingEntry, ring_entry};
1844
1845 #[test]
1846 fn visible_completions_and_nothing_to_submit_need_no_syscall() {
1847 assert_eq!(ring_entry(0, true), RingEntry::Skip);
1848 }
1849
1850 #[test]
1851 fn queued_sqes_are_always_handed_over() {
1852 // Never skip with work in the SQ: those entries would sit
1853 // there until some later iteration happened to enter.
1854 assert_eq!(ring_entry(1, true), RingEntry::Submit);
1855 assert_eq!(ring_entry(8, false), RingEntry::SubmitAndWait);
1856 }
1857
1858 #[test]
1859 fn an_empty_cq_is_the_only_thing_that_parks_the_thread() {
1860 assert_eq!(ring_entry(0, false), RingEntry::SubmitAndWait);
1861 // ...and visible work never does, whatever the SQ holds.
1862 for sq in [0, 1, 64] {
1863 assert_ne!(ring_entry(sq, true), RingEntry::SubmitAndWait);
1864 }
1865 }
1866 }
1867
1868 /// Minimal `AppEvent` for these tests. `Copy` is required by `AppEvent`;
1869 /// the on-wire codec is unused because [`ByteDecoder`] never invokes it
1870 /// (frames are interpreted directly from their one body byte).
1871 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1872 enum TestEvent {
1873 Cmd(u8),
1874 Query,
1875 }
1876
1877 impl AppEvent for TestEvent {
1878 const MAX_ENCODED_SIZE: usize = 2;
1879
1880 fn encoded_size(&self) -> usize {
1881 // Unused — the tests never round-trip through encode/decode.
1882 2
1883 }
1884 fn encode(&self, _buf: &mut [u8]) -> usize {
1885 unreachable!("process_frames does not encode app events")
1886 }
1887 fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
1888 unreachable!("process_frames does not decode app events directly")
1889 }
1890 fn is_query(&self) -> bool {
1891 matches!(self, TestEvent::Query)
1892 }
1893 }
1894
1895 /// Placeholder `Application` impl. `process_frames` calls only
1896 /// `build_reject`, for a write refused while halted; the rest
1897 /// `unreachable!`.
1898 #[derive(Default)]
1899 struct TestApp;
1900
1901 /// What [`TestApp::build_reject`] returns: the refused event and why,
1902 /// so a test can tell refusals apart.
1903 type TestReport = (TestEvent, RejectReason);
1904
1905 impl Application for TestApp {
1906 type Event = TestEvent;
1907 type Report = TestReport;
1908 type QueryResponse = ();
1909 type Sizing = ();
1910 const APP_VERSION: u16 = 0;
1911 fn apply(&mut self, _event: TestEvent, _ctx: &ApplyCtx, _out: &mut Vec<TestReport>) {
1912 unreachable!()
1913 }
1914 fn query(&self, _event: TestEvent, _ctx: &QueryCtx) -> Option<()> {
1915 unreachable!()
1916 }
1917 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<TestReport>) {
1918 unreachable!()
1919 }
1920 fn build_reject(event: &TestEvent, reason: RejectReason) -> TestReport {
1921 (*event, reason)
1922 }
1923 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1924 unreachable!()
1925 }
1926 fn restore<R: Read>(_r: &mut R) -> std::io::Result<Self> {
1927 unreachable!()
1928 }
1929 }
1930
1931 /// Stateless decoder that maps a request's single body byte to a
1932 /// [`Decoded`] outcome. Lets each test feed a precise mix of permitted,
1933 /// filtered, denied, and decode-error frames without standing up the
1934 /// real wire codec.
1935 ///
1936 /// Body-byte mapping (`0x00..=0xFB` map 1:1 to a Permitted command,
1937 /// reserving the top four byte values for the non-Permitted outcomes):
1938 /// * `0xFC` -> `Filter`
1939 /// * `0xFD` -> `PermissionDenied`
1940 /// * `0xFE` -> `DecodeError`
1941 /// * `0xFF` -> `Permitted` with `is_query == true`
1942 /// * `0x00..=0xFB` -> `Permitted(Cmd(byte))`
1943 struct ByteDecoder;
1944
1945 impl RequestDecoder for ByteDecoder {
1946 type Event = TestEvent;
1947 // The reader never looks at a role: operator keys are enough.
1948 type Role = NoRoles;
1949 fn decode(&self, body: &[u8], _role: ClientRole<NoRoles>) -> Decoded<TestEvent> {
1950 match body.first().copied() {
1951 None => Decoded::DecodeError("empty body"),
1952 Some(0xFC) => Decoded::Filter,
1953 Some(0xFD) => Decoded::PermissionDenied("denied"),
1954 Some(0xFE) => Decoded::DecodeError("bad"),
1955 Some(0xFF) => Decoded::Permitted(TestEvent::Query),
1956 Some(b) => Decoded::Permitted(TestEvent::Cmd(b)),
1957 }
1958 }
1959 }
1960
1961 /// A frame under any tag — `[u32 LE length][tag][rest]` — as no
1962 /// well-behaved client sends after the handshake unless `tag` is the
1963 /// request's and `rest` starts with an ID.
1964 fn tagged_frame(tag: u8, rest: &[u8]) -> Vec<u8> {
1965 let len = (TAG_LEN + rest.len()) as u32;
1966 let mut f = Vec::with_capacity(4 + len as usize);
1967 f.extend_from_slice(&len.to_le_bytes());
1968 f.push(tag);
1969 f.extend_from_slice(rest);
1970 f
1971 }
1972
1973 /// Request `id`'s frame as a client sends it —
1974 /// `[u32 LE length][TAG_REQUEST][id][body]`.
1975 fn request_frame(id: u64, body: &[u8]) -> Vec<u8> {
1976 let mut buf = vec![0u8; melin_wire_protocol::framing::REQUEST_HEADER_LEN + body.len()];
1977 let n = melin_wire_protocol::framing::frame_request(&mut buf, RequestId(id), |out| {
1978 out[..body.len()].copy_from_slice(body);
1979 Ok::<_, melin_wire_protocol::framing::RequestFrameError>(body.len())
1980 })
1981 .expect("a test request fits");
1982 buf.truncate(n);
1983 buf
1984 }
1985
1986 /// The ID [`frame`] gives a request carrying `byte`: distinct per byte,
1987 /// and never the byte itself, so a test cannot pass by confusing the
1988 /// two.
1989 fn id_of(byte: u8) -> RequestId {
1990 RequestId(0x1_0000 + u64::from(byte))
1991 }
1992
1993 /// The frame most tests use: a request whose ID is [`id_of`] `byte`,
1994 /// and `byte` as the one body byte [`ByteDecoder`] keys on.
1995 fn frame(byte: u8) -> Vec<u8> {
1996 request_frame(id_of(byte).0, &[byte])
1997 }
1998
1999 /// Length prefix announcing an oversize frame. No payload bytes follow —
2000 /// `process_frames` decides on the prefix alone, before waiting for the
2001 /// body.
2002 fn oversize_prefix() -> [u8; 4] {
2003 ((MAX_FRAME_SIZE as u32) + 1).to_le_bytes()
2004 }
2005
2006 /// Test fixture bundle. Grouped into a struct rather than returned as a
2007 /// 4-tuple to keep clippy happy (`type_complexity`) and to give each
2008 /// field a name at call sites.
2009 struct Fixture {
2010 conn: ConnectionEntry<UnixStream>,
2011 producer: ring::Producer<InputSlot<TestEvent>>,
2012 consumer: ring::Consumer<InputSlot<TestEvent>>,
2013 /// Client-side end of the socket pair — read from this to inspect
2014 /// any `ServerBusy` bytes the function under test writes.
2015 peer: UnixStream,
2016 }
2017
2018 /// Build a fresh fixture: a `ConnectionEntry` backed by a `UnixStream`
2019 /// pair plus a single-consumer disruptor of the requested capacity.
2020 fn make_fixture(ring_capacity: usize) -> Fixture {
2021 let (server_side, peer) = UnixStream::pair().expect("UnixStream::pair");
2022 // Short read timeout on the peer so assertions of "no ServerBusy
2023 // written" return promptly instead of hanging the test.
2024 peer.set_read_timeout(Some(Duration::from_millis(50)))
2025 .expect("set_read_timeout");
2026
2027 let entry = ConnectionEntry::<UnixStream> {
2028 connection_id: 7,
2029 addr: "127.0.0.1:1".parse().expect("addr parses"),
2030 role: ClientRole::Operator,
2031 key_hash: 0xC0FFEE_u64,
2032 fd: server_side.as_raw_fd(),
2033 _reader: server_side,
2034 ingress: ClientIngress::new(Vec::with_capacity(64)),
2035 recv: RecvState::Idle,
2036 last_activity: Instant::now(),
2037 };
2038
2039 let (producer, mut consumers) =
2040 DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
2041 .add_consumer()
2042 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2043 let consumer = consumers.pop().expect("consumer present");
2044
2045 Fixture {
2046 conn: entry,
2047 producer,
2048 consumer,
2049 peer,
2050 }
2051 }
2052
2053 /// Invoke `process_frames::<TestApp, UnixStream>` on a standalone gate,
2054 /// threading the feature-gated histogram args when the relevant
2055 /// features are on so the call compiles in every `cargo test`
2056 /// configuration.
2057 ///
2058 /// Returns the call's outcome plus every early reply the call queued:
2059 /// a busy goes to the response stage through the early-reply queue,
2060 /// never as reader-side socket bytes, so busy assertions read the
2061 /// queue, not the peer.
2062 fn run_process_frames(
2063 conn: &mut ConnectionEntry<UnixStream>,
2064 producer: &mut ring::Producer<InputSlot<TestEvent>>,
2065 ) -> (FrameAction, Vec<EarlyReply<TestReport>>) {
2066 let (mut early, mut queue) = early_channel();
2067 let action = run_process_frames_with(conn, producer, &gate(None, false), &mut early);
2068 (action, released(&mut queue))
2069 }
2070
2071 /// Every early reply visible on `queue`, oldest first: what the
2072 /// response stage would send, with no flush on the reader's side
2073 /// after the call that queued them.
2074 fn released(
2075 queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
2076 ) -> Vec<EarlyReply<TestReport>> {
2077 let boundary = crate::batch_boundary::BatchBoundary::new();
2078 let at = boundary.at_boundary().expect("between events");
2079 let mut out = Vec::new();
2080 queue.register(|| {}).drain(&at, |reply| out.push(reply));
2081 out
2082 }
2083
2084 /// The early reply the fixture connection (id 7) gets for request
2085 /// `id`.
2086 fn early_reply(id: RequestId, kind: EarlyKind<TestReport>) -> EarlyReply<TestReport> {
2087 EarlyReply {
2088 connection_id: 7,
2089 request_id: id,
2090 kind,
2091 }
2092 }
2093
2094 /// A halt gate: `replicas` connected (`None` for standalone), fenced
2095 /// or not.
2096 fn gate(replicas: Option<u32>, fenced: bool) -> HaltGate {
2097 gate_counting_into(replicas, fenced, Arc::new(AtomicU64::new(0)))
2098 }
2099
2100 /// [`gate`], counting what it refuses into `refused`.
2101 fn gate_counting_into(
2102 replicas: Option<u32>,
2103 fenced: bool,
2104 refused: Arc<AtomicU64>,
2105 ) -> HaltGate {
2106 let fence = Arc::new(melin_transport_core::fence::FenceState::new(0));
2107 if fenced {
2108 fence.fence();
2109 }
2110 HaltGate::new(
2111 replicas.map(crate::halt::test_support::presence),
2112 fence,
2113 refused,
2114 )
2115 }
2116
2117 fn refused_count(refused: &AtomicU64) -> u64 {
2118 refused.load(std::sync::atomic::Ordering::Relaxed)
2119 }
2120
2121 /// The ack-policy mailbox's producer side, for a reader no test sends
2122 /// a request to.
2123 fn policy_claim() -> crate::policy_control::BootedClaim {
2124 crate::policy_control::test_support::booted(
2125 &crate::policy_control::test_support::replicated(crate::ack_policy::AckPolicy::Disk),
2126 )
2127 }
2128
2129 /// A reader parked with nothing to do (no connection, no tick) is
2130 /// woken by an `ACK-POLICY` request and journals it. Halt-plan step 0
2131 /// through the reader: on a halted primary the operator's `disk` is
2132 /// journaled forced, and the halt is lifted before the request returns
2133 /// published, so the reader takes the next write. Once stopped, the
2134 /// reader is detached.
2135 #[test]
2136 fn a_parked_reader_journals_an_ack_policy_request() {
2137 use crate::ack_policy::AckPolicy;
2138 use crate::halt::Verdict;
2139 use crate::halt::test_support::{gate_over, presence};
2140 use crate::policy_control::RequestOutcome;
2141
2142 let replicas = presence(0);
2143 let halt = || gate_over(Some(Arc::clone(&replicas)));
2144 assert_eq!(
2145 halt().verdict(),
2146 Verdict::Refuse(RejectReason::ReplicaDisconnected)
2147 );
2148 let (handle, mut consumer, control) = spawn_parked_reader(halt(), None);
2149 assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
2150 let (_, slot) = consumer.try_consume().expect("the entry is on the ring");
2151 assert_eq!(
2152 slot.event,
2153 JournalEvent::AckPolicyChange(melin_journal::AckPolicyChange {
2154 policy: AckPolicy::Disk.to_byte(),
2155 unwitnessed: true,
2156 })
2157 );
2158 assert_eq!(halt().verdict(), Verdict::Take);
2159 handle.shutdown();
2160 handle.join();
2161 assert_eq!(
2162 control.request(AckPolicy::Ram),
2163 RequestOutcome::Staged,
2164 "a stopped reader is detached"
2165 );
2166 }
2167
2168 /// A policy staged while the primary started is journaled as the
2169 /// reader attaches, before it runs: ahead of a runtime request it
2170 /// then publishes, and never forced.
2171 #[test]
2172 fn a_reader_journals_a_policy_staged_during_bring_up_first() {
2173 use crate::ack_policy::AckPolicy;
2174 use crate::halt::test_support::{gate_over, presence};
2175 use crate::policy_control::RequestOutcome;
2176
2177 let (handle, mut consumer, control) =
2178 spawn_parked_reader(gate_over(Some(presence(0))), Some(AckPolicy::Disk));
2179 assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
2180 handle.shutdown();
2181 handle.join();
2182 let mut changes = Vec::new();
2183 while let Some((_, slot)) = consumer.try_consume() {
2184 if let JournalEvent::AckPolicyChange(change) = slot.event {
2185 changes.push(change.unwitnessed);
2186 }
2187 }
2188 assert_eq!(
2189 changes,
2190 [false, true],
2191 "staged first, then the forced runtime one"
2192 );
2193 }
2194
2195 /// A reader over `halt` with no connection and no tick, parked (in all
2196 /// likelihood: the callers must not depend on it either way), whose
2197 /// mailbox starts at `disk+ram` and had `staged` requested between its
2198 /// claim and the reader's start (a primary still starting); and the
2199 /// consumer of its input ring.
2200 fn spawn_parked_reader(
2201 halt: HaltGate,
2202 staged: Option<crate::ack_policy::AckPolicy>,
2203 ) -> (
2204 UringReaderHandle<UnixStream>,
2205 ring::Consumer<InputSlot<TestEvent>>,
2206 Arc<crate::policy_control::AckPolicyControl>,
2207 ) {
2208 use crate::ack_policy::AckPolicy;
2209 use crate::policy_control::RequestOutcome;
2210 use crate::policy_control::test_support::{control_with_publish_wait, first_change};
2211
2212 let (mut producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
2213 .add_consumer()
2214 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2215 let consumer = consumers.pop().expect("consumer");
2216 // Dropped: the reader's sends to it are best-effort.
2217 let (control_tx, _) = mpsc::channel();
2218 // Long enough that a reader that is never woken fails the request.
2219 let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_secs(30));
2220 // Booted on a lineage already under the claim's policy: no entry.
2221 let claim =
2222 control
2223 .claim()
2224 .journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt);
2225 if let Some(policy) = staged {
2226 assert_eq!(control.request(policy), RequestOutcome::Staged);
2227 }
2228 let handle = spawn_reader::<TestApp, UnixStream>(
2229 producer,
2230 Arc::new(ByteDecoder),
2231 halt,
2232 early_channel().0,
2233 Arc::new(IngressMetrics::default()),
2234 claim,
2235 control_tx,
2236 0,
2237 None,
2238 None, // no tick: nothing but the wakeup brings the loop round
2239 RingSizing::for_max_connections(4).expect("a supported cap"),
2240 Arc::new(AtomicBool::new(false)),
2241 )
2242 .expect("the reader starts");
2243 std::thread::sleep(Duration::from_millis(50));
2244 (handle, consumer, control)
2245 }
2246
2247 /// An early-reply queue, at a node's capacity.
2248 fn early_channel() -> (
2249 EarlyReplySender<TestReport>,
2250 crate::early_reply::EarlyReplyQueue<TestReport>,
2251 ) {
2252 early_channel_of(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY)
2253 }
2254
2255 /// An early-reply queue that fills at `capacity` (a power of two).
2256 fn early_channel_of(
2257 capacity: usize,
2258 ) -> (
2259 EarlyReplySender<TestReport>,
2260 crate::early_reply::EarlyReplyQueue<TestReport>,
2261 ) {
2262 crate::early_reply::early_reply_channel(capacity)
2263 }
2264
2265 /// [`run_process_frames`] against a given halt gate and early-reply
2266 /// queue.
2267 fn run_process_frames_with(
2268 conn: &mut ConnectionEntry<UnixStream>,
2269 producer: &mut ring::Producer<InputSlot<TestEvent>>,
2270 halt: &HaltGate,
2271 early: &mut EarlyReplySender<TestReport>,
2272 ) -> FrameAction {
2273 run_process_frames_counting(conn, producer, halt, early, &IngressMetrics::default())
2274 }
2275
2276 /// [`run_process_frames_with`], counting what it answers early into
2277 /// `metrics`.
2278 fn run_process_frames_counting(
2279 conn: &mut ConnectionEntry<UnixStream>,
2280 producer: &mut ring::Producer<InputSlot<TestEvent>>,
2281 halt: &HaltGate,
2282 early: &mut EarlyReplySender<TestReport>,
2283 metrics: &IngressMetrics,
2284 ) -> FrameAction {
2285 #[cfg(feature = "latency-trace")]
2286 let mut publish_rec = melin_transport_core::trace::register_stage("test: publish");
2287 #[cfg(feature = "tick-to-trade")]
2288 let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest");
2289
2290 #[allow(clippy::let_unit_value)] // ZST when latency-trace is off
2291 let recv_ts = melin_transport_core::trace::mono_trace_ns();
2292
2293 process_frames::<TestApp, UnixStream>(
2294 conn,
2295 producer,
2296 &ByteDecoder,
2297 halt,
2298 early,
2299 metrics,
2300 0xDEAD_BEEF,
2301 recv_ts,
2302 #[cfg(feature = "latency-trace")]
2303 &mut publish_rec,
2304 #[cfg(feature = "tick-to-trade")]
2305 &mut ingest_rec,
2306 )
2307 }
2308
2309 /// The counts `answers` should leave on the ingress metrics: a busy
2310 /// is shed, a filtered request filtered, a refusal refused as its
2311 /// kind. A halt refusal is not among them: the halt gate counts it.
2312 fn ingress_counts_of(answers: &[EarlyReply<TestReport>]) -> IngressTally {
2313 let mut tally = IngressTally::default();
2314 for reply in answers {
2315 match reply.kind {
2316 EarlyKind::Busy => tally.shed += 1,
2317 EarlyKind::Filtered => tally.filtered += 1,
2318 EarlyKind::Refused { kind, .. } => tally.refused(kind),
2319 EarlyKind::Rejected(_) => {}
2320 }
2321 }
2322 tally
2323 }
2324
2325 /// Drain `consumer` into a Vec of `(seq, slot)` until it yields `None`.
2326 /// Used to assert exact event sequences after `process_frames` returns.
2327 fn drain(
2328 consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
2329 ) -> Vec<(u64, InputSlot<TestEvent>)> {
2330 let mut out = Vec::new();
2331 while let Some(pair) = consumer.try_consume() {
2332 out.push(pair);
2333 }
2334 out
2335 }
2336
2337 /// Whether the peer can read anything within its configured timeout:
2338 /// the reader never writes the client socket (egress belongs to the
2339 /// response stage), so it must not.
2340 fn peer_got_bytes(peer: &mut UnixStream) -> bool {
2341 let mut buf = [0u8; 1];
2342 match peer.read(&mut buf) {
2343 Ok(n) => n > 0,
2344 Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => false,
2345 Err(e) => panic!("unexpected peer read error: {e}"),
2346 }
2347 }
2348
2349 #[test]
2350 fn process_frames_publishes_all_frames_after_single_commit() {
2351 // Capacity > number of frames — every frame must succeed and become
2352 // visible to the consumer after the trailing `batch.commit()`.
2353 let Fixture {
2354 mut conn,
2355 mut producer,
2356 mut consumer,
2357 mut peer,
2358 } = make_fixture(16);
2359 for byte in [0x01, 0x02, 0x03, 0x04, 0x05] {
2360 conn.ingress.extend_from_slice(&frame(byte));
2361 }
2362
2363 let (action, early) = run_process_frames(&mut conn, &mut producer);
2364 assert_eq!(
2365 action,
2366 FrameAction::Continue,
2367 "no oversize frame ⇒ no disconnect"
2368 );
2369
2370 let events = drain(&mut consumer);
2371 assert_eq!(events.len(), 5, "all 5 frames must be visible");
2372 for (i, (seq, slot)) in events.iter().enumerate() {
2373 assert_eq!(*seq, i as u64, "seq monotonic from 0");
2374 assert_eq!(slot.connection_id, 7);
2375 assert_eq!(slot.key_hash, 0xC0FFEE_u64);
2376 let byte = (i + 1) as u8;
2377 // The client's ID travels in the slot.
2378 assert_eq!(slot.request_id, id_of(byte));
2379 assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2380 // Non-query event ⇒ inherits the caller-supplied wall-clock.
2381 assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
2382 }
2383 // Parse buffer fully consumed.
2384 assert!(conn.ingress.bytes().is_empty());
2385 // Every request published: nothing answered early, and the
2386 // reader never writes the client socket (egress belongs to the
2387 // response stage).
2388 assert!(early.is_empty(), "{early:?}");
2389 assert!(
2390 !peer_got_bytes(&mut peer),
2391 "reader must never write the client socket"
2392 );
2393 }
2394
2395 /// A ring slot is reused once its consumer has passed it, and a request
2396 /// published into it is built whole: nothing of the previous occupant
2397 /// survives, a request ID and a journal sequence included (a stale
2398 /// sequence would be adopted by the journal stage as a replica's).
2399 #[test]
2400 fn process_frames_rebuilds_every_field_of_a_reused_slot() {
2401 const CAPACITY: usize = 4;
2402 let Fixture {
2403 mut conn,
2404 mut producer,
2405 mut consumer,
2406 ..
2407 } = make_fixture(CAPACITY);
2408 let stale = InputSlot {
2409 connection_id: 99,
2410 request_id: RequestId(0xBAD),
2411 key_hash: 1,
2412 sequence: 77,
2413 timestamp_ns: 5,
2414 event: JournalEvent::App(TestEvent::Cmd(0xEE)),
2415 ..InputSlot::default()
2416 };
2417 for _ in 0..CAPACITY {
2418 producer.publish(stale);
2419 }
2420 assert_eq!(drain(&mut consumer).len(), CAPACITY, "every slot dirtied");
2421
2422 for byte in 1..=CAPACITY as u8 {
2423 conn.ingress.extend_from_slice(&frame(byte));
2424 }
2425 let (action, _early) = run_process_frames(&mut conn, &mut producer);
2426 assert_eq!(action, FrameAction::Continue);
2427
2428 let events = drain(&mut consumer);
2429 assert_eq!(events.len(), CAPACITY, "every dirtied slot reused");
2430 for (i, (_, slot)) in events.iter().enumerate() {
2431 assert_eq!(slot.connection_id, 7);
2432 assert_eq!(slot.request_id, id_of(i as u8 + 1), "the frame's own ID");
2433 assert_eq!(slot.key_hash, 0xC0FFEE_u64);
2434 assert_eq!(slot.sequence, 0, "the journal stage allocates it");
2435 assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
2436 assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(i as u8 + 1)));
2437 }
2438 }
2439
2440 /// A halted reader publishes no write: each is refused under its ID,
2441 /// while queries still go through. Nothing refused can reach the journal, so replay cannot
2442 /// apply a write the client was told failed.
2443 #[test]
2444 fn a_halted_reader_refuses_writes_before_publishing_and_still_publishes_queries() {
2445 let Fixture {
2446 mut conn,
2447 mut producer,
2448 mut consumer,
2449 ..
2450 } = make_fixture(16);
2451 // Write, query, write, query.
2452 for byte in [0x01, 0xFF, 0x02, 0xFF] {
2453 conn.ingress.extend_from_slice(&frame(byte));
2454 }
2455 let (mut early, mut queue) = early_channel();
2456 let counted = Arc::new(AtomicU64::new(0));
2457
2458 let action = run_process_frames_with(
2459 &mut conn,
2460 &mut producer,
2461 &gate_counting_into(Some(0), false, Arc::clone(&counted)),
2462 &mut early,
2463 );
2464 assert_eq!(action, FrameAction::Continue);
2465 assert!(conn.ingress.bytes().is_empty(), "every frame consumed");
2466 assert_eq!(
2467 refused_count(&counted),
2468 2,
2469 "the two writes are counted, the queries are not"
2470 );
2471
2472 let published: Vec<_> = drain(&mut consumer)
2473 .into_iter()
2474 .map(|(seq, slot)| (seq, slot.event))
2475 .collect();
2476 assert_eq!(
2477 published,
2478 [
2479 (0, JournalEvent::App(TestEvent::Query)),
2480 (1, JournalEvent::App(TestEvent::Query)),
2481 ],
2482 "only the queries enter the pipeline"
2483 );
2484
2485 let expect = |byte| {
2486 early_reply(
2487 id_of(byte),
2488 EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
2489 )
2490 };
2491 assert_eq!(
2492 released(&mut queue),
2493 [expect(0x01), expect(0x02)],
2494 "each write is answered under its ID"
2495 );
2496 }
2497
2498 /// A fenced node closes the connection, whatever its replica count:
2499 /// nothing is published, nothing is refused, nothing is counted.
2500 #[test]
2501 fn a_fenced_reader_closes_the_connection() {
2502 let Fixture {
2503 mut conn,
2504 mut producer,
2505 mut consumer,
2506 ..
2507 } = make_fixture(16);
2508 for byte in [0x01, 0xFF] {
2509 conn.ingress.extend_from_slice(&frame(byte));
2510 }
2511 let (mut early, mut queue) = early_channel();
2512 let counted = Arc::new(AtomicU64::new(0));
2513
2514 let action = run_process_frames_with(
2515 &mut conn,
2516 &mut producer,
2517 &gate_counting_into(Some(1), true, Arc::clone(&counted)),
2518 &mut early,
2519 );
2520
2521 assert_eq!(action, FrameAction::Disconnect, "the connection is dropped");
2522 assert!(
2523 drain(&mut consumer).is_empty(),
2524 "not even the query is published"
2525 );
2526 assert!(released(&mut queue).is_empty(), "no early reply is queued");
2527 assert_eq!(refused_count(&counted), 0);
2528 }
2529
2530 /// An answer with nowhere to go parks its connection: the frames
2531 /// before it are committed, nothing from the frame that met the full
2532 /// queue on is published or answered, and that frame and the one behind
2533 /// it stay buffered. Once the queue has room, a call with no new bytes
2534 /// answers or publishes them as it would have. Each kind of early reply
2535 /// meets it the same way: a halt refusal (counted once answered), a
2536 /// filtered and a refused request, and a busy on a full ring, whose
2537 /// shed tail survives the park, so the frame behind it is shed too
2538 /// though the ring has room by then.
2539 #[test]
2540 fn a_full_early_reply_queue_parks_before_the_frame() {
2541 let refused_as =
2542 |byte: u8, kind, reason| early_reply(id_of(byte), EarlyKind::Refused { kind, reason });
2543 let rejected = |byte: u8| {
2544 early_reply(
2545 id_of(byte),
2546 EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
2547 )
2548 };
2549 let cmd_3 = [JournalEvent::App(TestEvent::Cmd(0x03))];
2550 // (slots already taken in a two-slot ring, halted, the frame that
2551 // meets the full queue, what the call after the park answers, and
2552 // publishes)
2553 type Case<'a> = (
2554 usize,
2555 bool,
2556 u8,
2557 Vec<EarlyReply<TestReport>>,
2558 &'a [JournalEvent<TestEvent>],
2559 );
2560 let cases: [Case; 5] = [
2561 (0, true, 0x02, vec![rejected(0x02), rejected(0x03)], &[]),
2562 (
2563 0,
2564 false,
2565 0xFC,
2566 vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2567 &cmd_3,
2568 ),
2569 (
2570 0,
2571 false,
2572 0xFD,
2573 vec![refused_as(0xFD, RefusalKind::PermissionDenied, "denied")],
2574 &cmd_3,
2575 ),
2576 (
2577 0,
2578 false,
2579 0xFE,
2580 vec![refused_as(0xFE, RefusalKind::Malformed, "bad")],
2581 &cmd_3,
2582 ),
2583 // The ring has room for the first frame only: the second is
2584 // shed, and the third with it.
2585 (
2586 1,
2587 false,
2588 0x02,
2589 vec![
2590 early_reply(id_of(0x02), EarlyKind::Busy),
2591 early_reply(id_of(0x03), EarlyKind::Busy),
2592 ],
2593 &[],
2594 ),
2595 ];
2596 for (prefill, halted, byte, answers, publishes) in cases {
2597 let Fixture {
2598 mut conn,
2599 mut producer,
2600 mut consumer,
2601 ..
2602 } = make_fixture(2);
2603 for _ in 0..prefill {
2604 producer.publish(InputSlot::default());
2605 }
2606 // A query first, which is published even while halted.
2607 conn.ingress.extend_from_slice(&frame(0xFF));
2608 conn.ingress.extend_from_slice(&frame(byte));
2609 conn.ingress.extend_from_slice(&frame(0x03));
2610 let (mut early, mut queue) = early_channel_of(4);
2611 let filler = early_reply(RequestId(0), EarlyKind::Busy);
2612 let mut filled = 0;
2613 while early.try_send(filler).is_ok() {
2614 filled += 1;
2615 }
2616
2617 let counted = Arc::new(AtomicU64::new(0));
2618 let halt = gate_counting_into(halted.then_some(0), false, Arc::clone(&counted));
2619 let metrics = IngressMetrics::default();
2620 let action =
2621 run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
2622
2623 assert_eq!(action, FrameAction::Park, "byte {byte:#04x}");
2624 assert_eq!(
2625 refused_count(&counted),
2626 0,
2627 "byte {byte:#04x}: a write parked on is not refused yet"
2628 );
2629 assert_eq!(
2630 metrics.counts(),
2631 IngressTally::default(),
2632 "byte {byte:#04x}: nor is anything else counted"
2633 );
2634 let published: Vec<_> = drain(&mut consumer)
2635 .into_iter()
2636 .skip(prefill)
2637 .map(|(_, slot)| slot.event)
2638 .collect();
2639 assert_eq!(
2640 published,
2641 [JournalEvent::App(TestEvent::Query)],
2642 "byte {byte:#04x}: the frame before is committed, nothing after"
2643 );
2644 assert_eq!(
2645 conn.ingress.bytes(),
2646 [frame(byte), frame(0x03)].concat(),
2647 "byte {byte:#04x}: the frame parked on stays, with the tail"
2648 );
2649 let queued = released(&mut queue);
2650 assert_eq!(
2651 queued.len(),
2652 filled,
2653 "byte {byte:#04x}: nothing more was queued"
2654 );
2655 assert!(queued.iter().all(|reply| *reply == filler));
2656
2657 // The queue has room again (and so does the ring): the retry
2658 // takes no new bytes.
2659 let action =
2660 run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
2661 assert_eq!(action, FrameAction::Continue, "byte {byte:#04x}");
2662 assert_eq!(
2663 metrics.counts(),
2664 ingress_counts_of(&answers),
2665 "byte {byte:#04x}: each answer counted once, as its kind"
2666 );
2667 assert_eq!(released(&mut queue), answers, "byte {byte:#04x}");
2668 let published: Vec<_> = drain(&mut consumer)
2669 .into_iter()
2670 .map(|(_, slot)| slot.event)
2671 .collect();
2672 assert_eq!(published, publishes, "byte {byte:#04x}");
2673 assert!(conn.ingress.bytes().is_empty(), "byte {byte:#04x}");
2674 assert_eq!(conn.ingress.shed_remaining(), 0, "byte {byte:#04x}");
2675 assert_eq!(
2676 refused_count(&counted),
2677 if halted { 2 } else { 0 },
2678 "byte {byte:#04x}: each refused write counted once"
2679 );
2680 }
2681 }
2682
2683 /// Whatever the call returns, every early reply it staged is visible
2684 /// to the response stage by then: the queue is flushed on every way
2685 /// out, and nothing here flushes it after the call (`released` reads
2686 /// it as the stage does). One case per way out: every frame
2687 /// processed, a shed tail answered whole, a close on a frame that is
2688 /// not a request (also in the middle of a shed tail) and on an
2689 /// oversized prefix, and a park (also in the middle of a shed tail),
2690 /// each after an answer was staged. A fenced node stages nothing.
2691 #[test]
2692 fn every_way_out_leaves_the_early_replies_it_staged_visible() {
2693 let refused = |byte: u8| {
2694 early_reply(
2695 id_of(byte),
2696 EarlyKind::Refused {
2697 kind: if byte == 0xFD {
2698 RefusalKind::PermissionDenied
2699 } else {
2700 RefusalKind::Malformed
2701 },
2702 reason: if byte == 0xFD { "denied" } else { "bad" },
2703 },
2704 )
2705 };
2706 let busy = |byte: u8| early_reply(id_of(byte), EarlyKind::Busy);
2707 let not_a_request = tagged_frame(0x00, &[0x03; 9]);
2708 // (case, ring capacity, early-reply queue capacity, frames, the
2709 // outcome, what it stages)
2710 let cases = [
2711 (
2712 "every frame processed",
2713 16,
2714 16,
2715 vec![frame(0xFC), frame(0x01)],
2716 FrameAction::Continue,
2717 vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2718 ),
2719 (
2720 "a full ring",
2721 2,
2722 16,
2723 vec![
2724 frame(0xFD),
2725 frame(0x01),
2726 frame(0x02),
2727 frame(0x03),
2728 frame(0x04),
2729 ],
2730 FrameAction::Continue,
2731 vec![refused(0xFD), busy(0x03), busy(0x04)],
2732 ),
2733 (
2734 "a frame that is not a request",
2735 16,
2736 16,
2737 vec![frame(0xFE), not_a_request.clone()],
2738 FrameAction::Disconnect,
2739 vec![refused(0xFE)],
2740 ),
2741 (
2742 "a frame that is not a request, in a shed tail",
2743 2,
2744 16,
2745 vec![
2746 frame(0x01),
2747 frame(0x02),
2748 frame(0x03),
2749 not_a_request,
2750 frame(0x04),
2751 ],
2752 FrameAction::Disconnect,
2753 vec![busy(0x03)],
2754 ),
2755 (
2756 "an oversized prefix",
2757 16,
2758 16,
2759 vec![frame(0xFC), oversize_prefix().to_vec()],
2760 FrameAction::Disconnect,
2761 vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
2762 ),
2763 (
2764 "a park",
2765 16,
2766 2,
2767 vec![frame(0xFC), frame(0xFE), frame(0xFD)],
2768 FrameAction::Park,
2769 vec![early_reply(id_of(0xFC), EarlyKind::Filtered), refused(0xFE)],
2770 ),
2771 (
2772 "a park in a shed tail",
2773 2,
2774 2,
2775 vec![
2776 frame(0x01),
2777 frame(0x02),
2778 frame(0x03),
2779 frame(0x04),
2780 frame(0x05),
2781 ],
2782 FrameAction::Park,
2783 vec![busy(0x03), busy(0x04)],
2784 ),
2785 ];
2786 for (case, ring_capacity, queue_capacity, frames, outcome, expected) in cases {
2787 let Fixture {
2788 mut conn,
2789 mut producer,
2790 ..
2791 } = make_fixture(ring_capacity);
2792 for frame in frames {
2793 conn.ingress.extend_from_slice(&frame);
2794 }
2795 let (mut early, mut queue) = early_channel_of(queue_capacity);
2796 let action =
2797 run_process_frames_with(&mut conn, &mut producer, &gate(None, false), &mut early);
2798 assert_eq!(action, outcome, "{case}");
2799 assert_eq!(released(&mut queue), expected, "{case}");
2800 }
2801 }
2802
2803 /// The caller stamps `recv_ts` once per recv (at the kernel-return
2804 /// site) and every frame parsed from that buffer must carry that exact
2805 /// value. Guards against a regression to per-frame `recv_ts` capture,
2806 /// which excluded decode from the reader-ingest / server-e2e windows
2807 /// and drifted forward for later frames in a multi-frame recv. Gated on
2808 /// `latency-trace` because `recv_ts` is a real `u64` only then (`()`
2809 /// otherwise, leaving nothing to assert).
2810 #[cfg(feature = "latency-trace")]
2811 #[test]
2812 fn process_frames_stamps_every_slot_with_caller_recv_ts() {
2813 let Fixture {
2814 mut conn,
2815 mut producer,
2816 mut consumer,
2817 ..
2818 } = make_fixture(16);
2819 for byte in [0x01, 0x02, 0x03, 0x04] {
2820 conn.ingress.extend_from_slice(&frame(byte));
2821 }
2822
2823 // A recognizable sentinel the caller would have captured at the
2824 // recv site; distinct from the wall-clock stamp (0xDEAD_BEEF).
2825 const RECV_TS: u64 = 0x5EED_5EED;
2826
2827 let mut publish_rec = melin_transport_core::trace::register_stage("test: publish recv_ts");
2828 #[cfg(feature = "tick-to-trade")]
2829 let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest recv_ts");
2830
2831 let (mut early, _queue) = early_channel();
2832 let action = process_frames::<TestApp, UnixStream>(
2833 &mut conn,
2834 &mut producer,
2835 &ByteDecoder,
2836 &gate(None, false),
2837 &mut early,
2838 &IngressMetrics::default(),
2839 0xDEAD_BEEF,
2840 RECV_TS,
2841 &mut publish_rec,
2842 #[cfg(feature = "tick-to-trade")]
2843 &mut ingest_rec,
2844 );
2845 assert_eq!(action, FrameAction::Continue);
2846
2847 let events = drain(&mut consumer);
2848 assert_eq!(events.len(), 4, "all frames published");
2849 for (_seq, slot) in &events {
2850 assert_eq!(
2851 slot.recv_ts, RECV_TS,
2852 "every frame from one recv shares the single caller-supplied recv_ts"
2853 );
2854 }
2855 }
2856
2857 #[test]
2858 fn process_frames_rotates_batch_at_commit_every_cap() {
2859 // Push more events than `COMMIT_EVERY` (= 16) into a recv-cycle.
2860 // The cap must trigger at least one mid-loop commit so the
2861 // consumer sees the first capacity-many events before the
2862 // remainder lands. Validates the visibility-delay cap from the
2863 // perf branch — without it, all 32 events would commit together
2864 // and the first frame would wait for the 32nd to decode.
2865 //
2866 // Capacity 64 leaves room for the entire input (no Full); ring
2867 // backpressure is exercised separately in
2868 // `a_full_ring_queues_busy_for_the_frame_that_met_it`.
2869 let Fixture {
2870 mut conn,
2871 mut producer,
2872 mut consumer,
2873 ..
2874 } = make_fixture(64);
2875 const EVENT_COUNT: usize = 32;
2876 for i in 0..EVENT_COUNT {
2877 // Use bytes 1..=32 (each ≤ 0xFB so ByteDecoder yields
2878 // `Permitted` with `TestEvent::Cmd(byte)`).
2879 conn.ingress.extend_from_slice(&frame((i + 1) as u8));
2880 }
2881
2882 let (action, _early) = run_process_frames(&mut conn, &mut producer);
2883 assert_eq!(
2884 action,
2885 FrameAction::Continue,
2886 "no oversize / no Full ⇒ no disconnect"
2887 );
2888
2889 let events = drain(&mut consumer);
2890 assert_eq!(events.len(), EVENT_COUNT, "every event visible");
2891 for (i, (seq, slot)) in events.iter().enumerate() {
2892 assert_eq!(*seq, i as u64, "seq contiguous across batch rotations");
2893 let byte = (i + 1) as u8;
2894 assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2895 }
2896 assert!(conn.ingress.bytes().is_empty());
2897 }
2898
2899 #[test]
2900 fn process_frames_query_event_skips_wall_clock_stamp() {
2901 // `AppEvent::is_query` events bypass the journal stamp — verify
2902 // the timestamp is zeroed even when a non-zero batch_wall_ns was
2903 // supplied.
2904 let Fixture {
2905 mut conn,
2906 mut producer,
2907 mut consumer,
2908 ..
2909 } = make_fixture(8);
2910 conn.ingress.extend_from_slice(&frame(0xFF)); // body byte → Query
2911
2912 let (action, _early) = run_process_frames(&mut conn, &mut producer);
2913 assert_eq!(action, FrameAction::Continue);
2914
2915 let events = drain(&mut consumer);
2916 assert_eq!(events.len(), 1);
2917 let (_, slot) = &events[0];
2918 assert_eq!(slot.event, JournalEvent::App(TestEvent::Query));
2919 assert_eq!(
2920 slot.timestamp_ns, 0,
2921 "query events must skip the wall-clock stamp"
2922 );
2923 }
2924
2925 #[test]
2926 fn a_full_ring_sheds_the_frame_that_met_it_and_every_complete_frame_behind() {
2927 // Ring capacity 4 + 7 frames, the last cut short ⇒ first 4 commit,
2928 // the 5th triggers Full, the 6th is shed with it, the partial 7th
2929 // waits. Validates:
2930 // * `Err(Full)` does not roll back the prior 4.
2931 // * The frame that triggered Full, and the complete frame behind
2932 // it, are answered `Busy`, each with its ID, without being
2933 // decoded or published, and the connection is kept.
2934 // * The busy goes to the response stage through the early-reply
2935 // queue, and nothing is written to the socket from here.
2936 // * A trailing partial frame is not part of the tail: it stays,
2937 // and once whole it is judged on its own.
2938 let Fixture {
2939 mut conn,
2940 mut producer,
2941 mut consumer,
2942 mut peer,
2943 } = make_fixture(4);
2944 for byte in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] {
2945 conn.ingress.extend_from_slice(&frame(byte));
2946 }
2947 let partial = frame(0x07);
2948 conn.ingress.extend_from_slice(&partial[..5]);
2949
2950 let (action, early) = run_process_frames(&mut conn, &mut producer);
2951 assert_eq!(
2952 action,
2953 FrameAction::Continue,
2954 "Full does not drop the connection"
2955 );
2956
2957 let events = drain(&mut consumer);
2958 assert_eq!(
2959 events.len(),
2960 4,
2961 "only the first capacity-many frames are visible"
2962 );
2963 for (i, (_, slot)) in events.iter().enumerate() {
2964 let byte = (i + 1) as u8;
2965 assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
2966 }
2967
2968 // The busy goes to the response stage — never directly onto the
2969 // socket, where it could tear a partially-flushed response frame
2970 // (audit review F3).
2971 assert_eq!(
2972 early,
2973 [
2974 early_reply(id_of(0x05), EarlyKind::Busy),
2975 early_reply(id_of(0x06), EarlyKind::Busy),
2976 ],
2977 "a busy for the frame that met the full ring, and for the one behind"
2978 );
2979 assert!(
2980 !peer_got_bytes(&mut peer),
2981 "reader must never write the client socket"
2982 );
2983
2984 // Only the partial frame is left, outside any tail.
2985 assert_eq!(conn.ingress.bytes(), &partial[..5]);
2986 assert_eq!(conn.ingress.shed_remaining(), 0);
2987
2988 // The ring has room again (drained above) and the 7th completes:
2989 // published, as a request on a later receive is.
2990 conn.ingress.extend_from_slice(&partial[5..]);
2991 let (action, early) = run_process_frames(&mut conn, &mut producer);
2992 assert_eq!(action, FrameAction::Continue);
2993 assert!(early.is_empty(), "{early:?}");
2994 let events = drain(&mut consumer);
2995 assert_eq!(events.len(), 1);
2996 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x07)));
2997 }
2998
2999 /// Regression for the 2026-08 io_uring audit, finding 2 and review
3000 /// finding F3 (docs/internal/io-uring-audit-2026-08.md): the
3001 /// pipeline-full path must not touch the client socket at all. The
3002 /// original defect was a blocking `write(2)` of ServerBusy that
3003 /// parked the reader thread whenever the offender's socket buffer
3004 /// was also full — stalling ingress for *every* connection exactly
3005 /// on the overload path (against that code this test never
3006 /// returns). The busy notice now routes to the response stage
3007 /// through the early-reply queue, which also closes the
3008 /// frame-tearing race of any reader-side send. The socket-buffer
3009 /// fill stays: it proves the path is insensitive to socket state.
3010 #[test]
3011 fn pipeline_full_never_touches_the_client_socket() {
3012 let Fixture {
3013 mut conn,
3014 mut producer,
3015 mut consumer,
3016 mut peer,
3017 } = make_fixture(2);
3018
3019 // Fill the server→peer direction until the kernel refuses more —
3020 // the state a non-reading client leaves the connection in.
3021 let junk = [0u8; 4096];
3022 loop {
3023 let n = unsafe {
3024 libc::send(
3025 conn.fd,
3026 junk.as_ptr().cast(),
3027 junk.len(),
3028 libc::MSG_DONTWAIT,
3029 )
3030 };
3031 if n < 0 {
3032 assert_eq!(
3033 std::io::Error::last_os_error().raw_os_error(),
3034 Some(libc::EAGAIN),
3035 "unexpected errno while filling socket"
3036 );
3037 break;
3038 }
3039 }
3040
3041 // Capacity 2 + 3 frames ⇒ the third triggers Full ⇒ ServerBusy.
3042 conn.ingress.extend_from_slice(&frame(0x01));
3043 conn.ingress.extend_from_slice(&frame(0x02));
3044 conn.ingress.extend_from_slice(&frame(0x03));
3045
3046 let start = std::time::Instant::now();
3047 let (action, early) = run_process_frames(&mut conn, &mut producer);
3048 // Generous bound to stay unflaky under CI load — the fixed path
3049 // returns in microseconds, the broken one only when the peer
3050 // reads (here: never).
3051 assert!(
3052 start.elapsed() < Duration::from_secs(1),
3053 "pipeline-full path blocked on a full socket"
3054 );
3055 assert_eq!(
3056 action,
3057 FrameAction::Continue,
3058 "Full does not drop the connection"
3059 );
3060 assert_eq!(
3061 drain(&mut consumer).len(),
3062 2,
3063 "the frames that fit are still published"
3064 );
3065 assert_eq!(
3066 early,
3067 [early_reply(id_of(0x03), EarlyKind::Busy)],
3068 "busy notice routed to response stage"
3069 );
3070 // Nothing beyond the pre-fill junk arrives: drain it, then the
3071 // read must time out rather than yield a reader-written frame.
3072 let mut sink = [0u8; 4096];
3073 loop {
3074 match peer.read(&mut sink) {
3075 Ok(0) => panic!("unexpected EOF"),
3076 Ok(_) => continue,
3077 Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
3078 break;
3079 }
3080 Err(e) => panic!("unexpected peer read error: {e}"),
3081 }
3082 }
3083 }
3084
3085 #[test]
3086 fn process_frames_oversize_commits_prior_frames_then_signals_disconnect() {
3087 // Two valid frames followed by an oversize length prefix must:
3088 // * publish the two valid frames (commit-before-break) so the
3089 // pipeline observes them even though we're about to tear the
3090 // connection down,
3091 // * return `true` so the caller drops the connection,
3092 // * answer nothing early (a busy is for a full ring).
3093 let Fixture {
3094 mut conn,
3095 mut producer,
3096 mut consumer,
3097 mut peer,
3098 } = make_fixture(16);
3099 conn.ingress.extend_from_slice(&frame(0x01));
3100 conn.ingress.extend_from_slice(&frame(0x02));
3101 conn.ingress.extend_from_slice(&oversize_prefix());
3102
3103 let (action, early) = run_process_frames(&mut conn, &mut producer);
3104 assert_eq!(
3105 action,
3106 FrameAction::Disconnect,
3107 "oversize frame must request disconnect"
3108 );
3109
3110 let events = drain(&mut consumer);
3111 assert_eq!(
3112 events.len(),
3113 2,
3114 "prior frames are committed before the break"
3115 );
3116 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3117 assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
3118
3119 assert!(early.is_empty(), "{early:?}");
3120 assert!(
3121 !peer_got_bytes(&mut peer),
3122 "reader must never write the client socket"
3123 );
3124 }
3125
3126 /// Each decoder outcome that publishes nothing yields its early reply,
3127 /// with the frame's ID, in order among the other early replies; all
3128 /// bytes are consumed.
3129 #[test]
3130 fn each_decoder_outcome_yields_its_early_reply_with_the_frames_id() {
3131 // Mixed batch: Permitted, Filter, PermissionDenied, DecodeError,
3132 // Permitted, and a request with an empty body (a decode error).
3133 let Fixture {
3134 mut conn,
3135 mut producer,
3136 mut consumer,
3137 ..
3138 } = make_fixture(16);
3139 for byte in [0x01, 0xFC, 0xFD, 0xFE, 0x02] {
3140 conn.ingress.extend_from_slice(&frame(byte));
3141 }
3142 conn.ingress.extend_from_slice(&request_frame(99, b""));
3143
3144 let (action, early) = run_process_frames(&mut conn, &mut producer);
3145 assert_eq!(
3146 action,
3147 FrameAction::Continue,
3148 "a refused request keeps the connection"
3149 );
3150
3151 let events = drain(&mut consumer);
3152 assert_eq!(events.len(), 2);
3153 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3154 assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
3155 assert_eq!(
3156 early,
3157 [
3158 early_reply(id_of(0xFC), EarlyKind::Filtered),
3159 early_reply(
3160 id_of(0xFD),
3161 EarlyKind::Refused {
3162 kind: RefusalKind::PermissionDenied,
3163 reason: "denied"
3164 }
3165 ),
3166 early_reply(
3167 id_of(0xFE),
3168 EarlyKind::Refused {
3169 kind: RefusalKind::Malformed,
3170 reason: "bad"
3171 }
3172 ),
3173 early_reply(
3174 RequestId(99),
3175 EarlyKind::Refused {
3176 kind: RefusalKind::Malformed,
3177 reason: "empty body"
3178 }
3179 ),
3180 ]
3181 );
3182 assert!(
3183 conn.ingress.bytes().is_empty(),
3184 "all bytes advanced past compaction"
3185 );
3186 }
3187
3188 /// The request tag and ID are the runtime's to read: a frame that is
3189 /// not a request — an empty one, one under any other tag, a request
3190 /// too short for its ID — closes the connection before any decoder
3191 /// sees it, with the frames before it committed and nothing after it
3192 /// read. One such frame per connection. The decoder gets the body
3193 /// alone, so a body byte equal to a protocol tag is just that —
3194 /// `frame(0x01)` carries the heartbeat's value as a command.
3195 #[test]
3196 fn a_frame_that_is_not_a_request_closes_the_connection() {
3197 let mut not_requests = vec![
3198 // An empty frame: no tag at all.
3199 0u32.to_le_bytes().to_vec(),
3200 // A request tag with fewer bytes than an ID behind it.
3201 tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[0x03; 7]),
3202 tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[]),
3203 ];
3204 // A zeroed tag, the protocol's own frames, the application tag of
3205 // a protocol-0 request, and an application tag as a pre-release
3206 // build framed one — each would decode to `Cmd(0x03)` if a
3207 // decoder saw it.
3208 for tag in [
3209 0x00,
3210 TAG_CHALLENGE_RESPONSE,
3211 TAG_RESPONSE_HEARTBEAT,
3212 TAG_APP,
3213 0x10,
3214 ] {
3215 not_requests.push(tagged_frame(tag, &[0x03; 9]));
3216 }
3217 for bad in not_requests {
3218 let Fixture {
3219 mut conn,
3220 mut producer,
3221 mut consumer,
3222 ..
3223 } = make_fixture(16);
3224 conn.ingress.extend_from_slice(&frame(0x01));
3225 conn.ingress.extend_from_slice(&frame(0xFC));
3226 conn.ingress.extend_from_slice(&bad);
3227 conn.ingress.extend_from_slice(&frame(0x02));
3228
3229 let (action, early) = run_process_frames(&mut conn, &mut producer);
3230 assert_eq!(
3231 action,
3232 FrameAction::Disconnect,
3233 "{bad:?} must close the connection"
3234 );
3235
3236 let events = drain(&mut consumer);
3237 assert_eq!(events.len(), 1, "{bad:?}");
3238 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
3239 // The answer decided before it is still sent.
3240 assert_eq!(early, [early_reply(id_of(0xFC), EarlyKind::Filtered)]);
3241 }
3242 }
3243
3244 #[test]
3245 fn process_frames_preserves_partial_trailing_frame() {
3246 // One complete frame followed by a truncated length prefix. The
3247 // complete frame must publish; the partial bytes must survive
3248 // compaction at the front of `parse_buf` for the next recv-cycle.
3249 let Fixture {
3250 mut conn,
3251 mut producer,
3252 mut consumer,
3253 ..
3254 } = make_fixture(16);
3255 conn.ingress.extend_from_slice(&frame(0x42));
3256 // Three of four length-prefix bytes — not even a length to judge
3257 // yet, so the loop breaks before consuming anything from the
3258 // partial.
3259 conn.ingress.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
3260
3261 let (action, _early) = run_process_frames(&mut conn, &mut producer);
3262 assert_eq!(action, FrameAction::Continue);
3263
3264 let events = drain(&mut consumer);
3265 assert_eq!(events.len(), 1);
3266 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x42)));
3267 assert_eq!(
3268 conn.ingress.bytes(),
3269 [0xDE, 0xAD, 0xBE],
3270 "partial length prefix preserved for next recv-cycle"
3271 );
3272 }
3273
3274 /// The cap is inclusive: a frame of exactly `MAX_FRAME_SIZE` payload
3275 /// bytes is waited for while it arrives and then published, and a
3276 /// prefix one past it drops the connection on its own, with no payload
3277 /// behind it and nothing in front.
3278 #[test]
3279 fn process_frames_admits_a_frame_at_the_limit_and_drops_one_past_it() {
3280 let Fixture {
3281 mut conn,
3282 mut producer,
3283 mut consumer,
3284 ..
3285 } = make_fixture(16);
3286 let mut body = vec![0x05; MAX_REQUEST_BODY];
3287 body[0] = 0x2A;
3288 let at_limit = request_frame(5, &body);
3289 assert_eq!(at_limit.len(), 4 + MAX_FRAME_SIZE);
3290
3291 // The prefix and part of the payload: kept, not judged oversized.
3292 conn.ingress.extend_from_slice(&at_limit[..4 + 1]);
3293 let (action, _early) = run_process_frames(&mut conn, &mut producer);
3294 assert_eq!(
3295 action,
3296 FrameAction::Continue,
3297 "a frame at the limit is not oversized"
3298 );
3299 assert_eq!(conn.ingress.bytes(), &at_limit[..4 + 1]);
3300 assert!(drain(&mut consumer).is_empty());
3301
3302 // The rest of it arrives: published, buffer drained.
3303 conn.ingress.extend_from_slice(&at_limit[4 + 1..]);
3304 let (action, _early) = run_process_frames(&mut conn, &mut producer);
3305 assert_eq!(action, FrameAction::Continue);
3306 let events = drain(&mut consumer);
3307 assert_eq!(events.len(), 1);
3308 assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x2A)));
3309 assert_eq!(events[0].1.request_id, RequestId(5));
3310 assert!(conn.ingress.bytes().is_empty());
3311
3312 // One byte more, announced by a prefix alone: dropped at once.
3313 conn.ingress.extend_from_slice(&oversize_prefix());
3314 let (action, _early) = run_process_frames(&mut conn, &mut producer);
3315 assert_eq!(
3316 action,
3317 FrameAction::Disconnect,
3318 "oversize prefix alone must request disconnect"
3319 );
3320 assert!(drain(&mut consumer).is_empty());
3321 }
3322
3323 #[test]
3324 fn process_frames_empty_buffer_is_noop() {
3325 // No bytes in parse_buf ⇒ loop never enters; commit is the
3326 // documented zero-slot no-op; no busy event; no disconnect.
3327 let Fixture {
3328 mut conn,
3329 mut producer,
3330 mut consumer,
3331 mut peer,
3332 } = make_fixture(4);
3333
3334 let (action, early) = run_process_frames(&mut conn, &mut producer);
3335 assert_eq!(action, FrameAction::Continue);
3336 assert_eq!(drain(&mut consumer).len(), 0);
3337 assert!(conn.ingress.bytes().is_empty());
3338 assert!(early.is_empty());
3339 assert!(!peer_got_bytes(&mut peer));
3340 }
3341
3342 /// The shedding and parking rules over random streams of requests of
3343 /// every decoder outcome, split into random receives, through a ring
3344 /// and an early-reply queue of random small sizes that the "pipeline"
3345 /// drains at random. A receive arrives only once the previous call did
3346 /// not park; a park is followed by a retry with no new bytes once the
3347 /// queue was drained, as the reader's retry is.
3348 mod shedding {
3349 use super::*;
3350 use proptest::prelude::*;
3351
3352 /// The body bytes [`ByteDecoder`] maps to each outcome: commands,
3353 /// a filtered one, a denied one, a malformed one, a query.
3354 fn body() -> impl Strategy<Value = u8> {
3355 prop_oneof![1u8..=8, Just(0xFC), Just(0xFD), Just(0xFE), Just(0xFF)]
3356 }
3357
3358 prop_compose! {
3359 fn script()(
3360 bodies in prop::collection::vec(body(), 1..40),
3361 ring_capacity in prop_oneof![Just(2usize), Just(4), Just(8)],
3362 queue_capacity in prop_oneof![Just(2usize), Just(4)],
3363 )(
3364 cuts in prop::collection::vec(0..bodies.len() * 14 + 1, 0..12),
3365 drains in prop::collection::vec((0usize..4, any::<bool>()), 64),
3366 bodies in Just(bodies),
3367 ring_capacity in Just(ring_capacity),
3368 queue_capacity in Just(queue_capacity),
3369 ) -> (Vec<u8>, Vec<usize>, Vec<(usize, bool)>, usize, usize) {
3370 (bodies, cuts, drains, ring_capacity, queue_capacity)
3371 }
3372 }
3373
3374 proptest! {
3375 #![proptest_config(ProptestConfig::with_cases(256))]
3376 #[test]
3377 fn every_request_is_published_or_answered_once_and_a_shed_tail_whole(
3378 (bodies, mut cuts, drains, ring_capacity, queue_capacity) in script()
3379 ) {
3380 // Request `i` carries ID `i`; every frame is 14 bytes.
3381 let frames: Vec<Vec<u8>> = bodies
3382 .iter()
3383 .enumerate()
3384 .map(|(i, body)| request_frame(i as u64, &[*body]))
3385 .collect();
3386 let stream = frames.concat();
3387 cuts.push(0);
3388 cuts.push(stream.len());
3389 cuts.sort_unstable();
3390 cuts.dedup();
3391
3392 let Fixture { mut conn, mut producer, mut consumer, .. } =
3393 make_fixture(ring_capacity);
3394 let (mut early, mut queue) = early_channel_of(queue_capacity);
3395 let halt = gate(None, false);
3396 let metrics = IngressMetrics::default();
3397 let mut published: Vec<u64> = Vec::new();
3398 let mut answered: Vec<EarlyReply<TestReport>> = Vec::new();
3399 // Per receive (an episode: the call after it and the
3400 // retries of its parks), how many frames were complete.
3401 let mut episodes: Vec<usize> = Vec::new();
3402 let mut drains = drains.into_iter().cycle();
3403 let mut take = |consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
3404 queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
3405 published: &mut Vec<u64>,
3406 answered: &mut Vec<EarlyReply<TestReport>>,
3407 all: bool| {
3408 let (events, replies) = drains.next().expect("cycled");
3409 for _ in 0..if all { usize::MAX } else { events } {
3410 let Some((_, slot)) = consumer.try_consume() else { break };
3411 published.push(slot.request_id.0);
3412 }
3413 if all || replies {
3414 answered.extend(released(queue));
3415 }
3416 };
3417
3418 let mut receives = cuts.windows(2).map(|w| &stream[w[0]..w[1]]);
3419 let mut action = FrameAction::Continue;
3420 loop {
3421 if action == FrameAction::Park {
3422 // The response stage drains the queue; the retry
3423 // brings no new bytes.
3424 answered.extend(released(&mut queue));
3425 } else if let Some(bytes) = receives.next() {
3426 conn.ingress.extend_from_slice(bytes);
3427 let received = stream.len() - receives.clone().map(<[u8]>::len).sum::<usize>();
3428 episodes.push(received / frames[0].len());
3429 } else {
3430 break;
3431 }
3432 action = run_process_frames_counting(
3433 &mut conn, &mut producer, &halt, &mut early, &metrics,
3434 );
3435 prop_assert_ne!(action, FrameAction::Disconnect);
3436 take(&mut consumer, &mut queue, &mut published, &mut answered, false);
3437 }
3438 take(&mut consumer, &mut queue, &mut published, &mut answered, true);
3439 prop_assert!(conn.ingress.bytes().is_empty());
3440 prop_assert_eq!(conn.ingress.shed_remaining(), 0);
3441 // Each early answer counted once, as its kind, however
3442 // many parks its frame was retried through.
3443 prop_assert_eq!(metrics.counts(), ingress_counts_of(&answered));
3444
3445 // Every request exactly once: published, or answered as its
3446 // decoder outcome says, or busy.
3447 let mut seen: Vec<u64> = published.clone();
3448 seen.extend(answered.iter().map(|reply| reply.request_id.0));
3449 seen.sort_unstable();
3450 prop_assert_eq!(seen, (0..bodies.len() as u64).collect::<Vec<_>>());
3451 for id in &published {
3452 let body = bodies[*id as usize];
3453 prop_assert!(body <= 8 || body == 0xFF, "published {body:#04x}");
3454 }
3455 let mut busy: Vec<u64> = Vec::new();
3456 for reply in &answered {
3457 let body = bodies[reply.request_id.0 as usize];
3458 match reply.kind {
3459 EarlyKind::Busy => busy.push(reply.request_id.0),
3460 EarlyKind::Filtered => prop_assert_eq!(body, 0xFC),
3461 EarlyKind::Refused { kind: RefusalKind::PermissionDenied, .. } => {
3462 prop_assert_eq!(body, 0xFD)
3463 }
3464 EarlyKind::Refused { kind: RefusalKind::Malformed, .. } => {
3465 prop_assert_eq!(body, 0xFE)
3466 }
3467 EarlyKind::Rejected(_) => prop_assert!(false, "not halted"),
3468 }
3469 }
3470 // A request is answered in the first episode in which it
3471 // was complete (an episode ends only once every complete
3472 // frame is processed), and a shed request takes every
3473 // complete frame behind it then: busy too, never
3474 // published, never decoded.
3475 for &k in &busy {
3476 let complete = episodes
3477 .iter()
3478 .copied()
3479 .find(|&complete| complete > k as usize)
3480 .expect("answered once complete");
3481 for j in k + 1..complete as u64 {
3482 prop_assert!(
3483 busy.contains(&j),
3484 "request {j} was in the tail shed at {k}, \
3485 {complete} complete: {busy:?}"
3486 );
3487 }
3488 }
3489 }
3490 }
3491 }
3492
3493 /// Every way a connection's receive state can change, against the
3494 /// CQEs the kernel can post and the outcomes a buffer can have: a park
3495 /// cancels an armed receive, a parked connection keeps the bytes that
3496 /// land before the cancel and is never re-armed while a cancel is in
3497 /// flight, a teardown issues no second cancel, and a dying connection
3498 /// is freed only on its terminal CQE.
3499 mod recv_state {
3500 use super::super::{
3501 CqeStep, FrameAction, FramesStep, RecvCompletion, RecvState, TeardownStep,
3502 };
3503 use std::time::{Duration, Instant};
3504
3505 const DATA: RecvCompletion = RecvCompletion::Data;
3506 const EMPTY: RecvCompletion = RecvCompletion::Empty;
3507 const CLOSED: RecvCompletion = RecvCompletion::Closed;
3508
3509 fn parked(cancelling: bool, since: Instant) -> RecvState {
3510 RecvState::Parked { cancelling, since }
3511 }
3512
3513 /// The normal path: data on an armed receive is processed; its
3514 /// end, with or without data, re-arms it once processed, and an
3515 /// empty end (a dry pool) re-arms at once.
3516 #[test]
3517 fn an_armed_receive_is_processed_and_re_armed_when_it_ends() {
3518 let now = Instant::now();
3519 assert_eq!(
3520 RecvState::Armed.on_cqe(DATA, false),
3521 (RecvState::Armed, CqeStep::Process)
3522 );
3523 assert_eq!(
3524 RecvState::Armed.on_cqe(DATA, true),
3525 (RecvState::Idle, CqeStep::Process)
3526 );
3527 assert_eq!(
3528 RecvState::Idle.after_frames(FrameAction::Continue, now),
3529 (RecvState::Armed, FramesStep::Arm)
3530 );
3531 assert_eq!(
3532 RecvState::Armed.after_frames(FrameAction::Continue, now),
3533 (RecvState::Armed, FramesStep::Nothing)
3534 );
3535 assert_eq!(
3536 RecvState::Armed.on_cqe(EMPTY, true),
3537 (RecvState::Armed, CqeStep::Arm)
3538 );
3539 assert_eq!(
3540 RecvState::Armed.on_cqe(EMPTY, false),
3541 (RecvState::Armed, CqeStep::Nothing)
3542 );
3543 assert_eq!(
3544 RecvState::Armed.on_cqe(CLOSED, true),
3545 (RecvState::Idle, CqeStep::Remove)
3546 );
3547 // Nothing was armed: an end changes nothing, and one with no
3548 // bytes arms a receive.
3549 assert_eq!(RecvState::Idle.after_terminal(), RecvState::Idle);
3550 assert_eq!(RecvState::Dying.after_terminal(), RecvState::Dying);
3551 assert_eq!(
3552 RecvState::Idle.on_cqe(EMPTY, true),
3553 (RecvState::Armed, CqeStep::Arm)
3554 );
3555 }
3556
3557 /// A park on an armed receive cancels it; bytes landing before the
3558 /// cancel's terminal CQE are kept, not processed; the terminal CQE
3559 /// (the cancel's, or an end of the receive's own) leaves it parked
3560 /// with nothing owed and does not re-arm; a retry that processes
3561 /// the buffer re-arms it.
3562 #[test]
3563 fn a_park_cancels_keeps_late_bytes_and_re_arms_on_resume() {
3564 let since = Instant::now();
3565 let later = since + std::time::Duration::from_secs(5);
3566 assert_eq!(
3567 RecvState::Armed.after_frames(FrameAction::Park, since),
3568 (parked(true, since), FramesStep::Cancel)
3569 );
3570 assert_eq!(
3571 parked(true, since).on_cqe(DATA, false),
3572 (parked(true, since), CqeStep::Keep)
3573 );
3574 assert_eq!(
3575 parked(true, since).on_cqe(DATA, true),
3576 (parked(false, since), CqeStep::Keep)
3577 );
3578 assert_eq!(
3579 parked(true, since).on_cqe(EMPTY, true),
3580 (parked(false, since), CqeStep::Nothing),
3581 "no re-arm while parked"
3582 );
3583 assert_eq!(
3584 parked(true, since).on_cqe(EMPTY, false),
3585 (parked(true, since), CqeStep::Nothing)
3586 );
3587 // Parked again by a retry: still parked since it first did.
3588 assert_eq!(
3589 parked(false, since).after_frames(FrameAction::Park, later),
3590 (parked(false, since), FramesStep::Nothing)
3591 );
3592 assert_eq!(
3593 parked(false, since).after_frames(FrameAction::Continue, later),
3594 (RecvState::Armed, FramesStep::Arm)
3595 );
3596 // A park on a receive already ended needs no cancel.
3597 assert_eq!(
3598 RecvState::Idle.after_frames(FrameAction::Park, since),
3599 (parked(false, since), FramesStep::Nothing)
3600 );
3601 }
3602
3603 /// A retry that processes the buffer before the cancel's terminal
3604 /// CQE arms nothing: the connection resumes on that CQE, and the
3605 /// bytes that land before it are processed, as the connection is
3606 /// no longer parked. Parking again there needs no second cancel.
3607 #[test]
3608 fn a_resume_before_the_cancel_lands_arms_on_its_terminal_cqe() {
3609 let since = Instant::now();
3610 assert_eq!(
3611 parked(true, since).after_frames(FrameAction::Continue, since),
3612 (RecvState::Resuming, FramesStep::Nothing)
3613 );
3614 assert_eq!(
3615 RecvState::Resuming.on_cqe(DATA, false),
3616 (RecvState::Resuming, CqeStep::Process)
3617 );
3618 assert_eq!(
3619 RecvState::Resuming.after_frames(FrameAction::Continue, since),
3620 (RecvState::Resuming, FramesStep::Nothing)
3621 );
3622 assert_eq!(
3623 RecvState::Resuming.on_cqe(EMPTY, true),
3624 (RecvState::Armed, CqeStep::Arm)
3625 );
3626 assert_eq!(
3627 RecvState::Resuming.on_cqe(DATA, true),
3628 (RecvState::Idle, CqeStep::Process)
3629 );
3630 assert_eq!(
3631 RecvState::Resuming.after_frames(FrameAction::Park, since),
3632 (parked(true, since), FramesStep::Nothing)
3633 );
3634 }
3635
3636 /// Teardown by state: an armed receive is cancelled; a cancel in
3637 /// flight (a park's, or a resume's) is awaited, never doubled;
3638 /// nothing owed frees at once; a second teardown does nothing.
3639 /// A dying connection discards its CQEs and is freed on the
3640 /// terminal one only. A connection whose peer closes while parked
3641 /// is removed.
3642 #[test]
3643 fn teardown_cancels_once_and_frees_once() {
3644 let since = Instant::now();
3645 assert_eq!(
3646 RecvState::Armed.on_teardown(),
3647 (RecvState::Dying, TeardownStep::Cancel)
3648 );
3649 for cancelling in [parked(true, since), RecvState::Resuming] {
3650 assert_eq!(
3651 cancelling.on_teardown(),
3652 (RecvState::Dying, TeardownStep::AwaitCancel),
3653 "{cancelling:?}"
3654 );
3655 }
3656 for owed_nothing in [parked(false, since), RecvState::Idle] {
3657 assert_eq!(
3658 owed_nothing.on_teardown(),
3659 (owed_nothing, TeardownStep::FreeNow),
3660 "{owed_nothing:?}"
3661 );
3662 }
3663 assert_eq!(
3664 RecvState::Dying.on_teardown(),
3665 (RecvState::Dying, TeardownStep::AlreadyDying)
3666 );
3667 for completion in [DATA, EMPTY, CLOSED] {
3668 assert_eq!(
3669 RecvState::Dying.on_cqe(completion, false),
3670 (RecvState::Dying, CqeStep::Discard { free: false })
3671 );
3672 assert_eq!(
3673 RecvState::Dying.on_cqe(completion, true),
3674 (RecvState::Dying, CqeStep::Discard { free: true })
3675 );
3676 }
3677 assert_eq!(parked(true, since).on_cqe(CLOSED, true).1, CqeStep::Remove);
3678 // A buffer that closes the connection tears it down, whatever
3679 // the state.
3680 for state in [
3681 RecvState::Armed,
3682 RecvState::Idle,
3683 RecvState::Resuming,
3684 parked(true, since),
3685 ] {
3686 assert_eq!(
3687 state.after_frames(FrameAction::Disconnect, since),
3688 (state, FramesStep::Teardown)
3689 );
3690 }
3691 }
3692
3693 /// Only a park whose cancel is still in flight past the deadline
3694 /// counts as unhonoured: not one at the deadline, not one whose
3695 /// cancel has landed however long it has been parked, and no
3696 /// other state.
3697 #[test]
3698 fn only_a_park_cancel_in_flight_past_the_deadline_is_overdue() {
3699 let since = Instant::now();
3700 let past = since + super::super::PARK_CANCEL_DEADLINE + Duration::from_nanos(1);
3701 let at = since + super::super::PARK_CANCEL_DEADLINE;
3702 assert!(parked(true, since).park_cancel_overdue(past));
3703 assert!(!parked(true, since).park_cancel_overdue(at));
3704 assert!(!parked(true, since).park_cancel_overdue(since));
3705 for state in [
3706 parked(false, since),
3707 RecvState::Armed,
3708 RecvState::Idle,
3709 RecvState::Resuming,
3710 RecvState::Dying,
3711 ] {
3712 assert!(!state.park_cancel_overdue(past), "{state:?}");
3713 }
3714 }
3715
3716 /// What the shutdown quiesce waits for: a CQE the kernel owes.
3717 #[test]
3718 fn the_kernel_owes_a_cqe_while_a_receive_or_a_cancel_is_in_flight() {
3719 let since = Instant::now();
3720 for owed in [
3721 RecvState::Armed,
3722 RecvState::Resuming,
3723 RecvState::Dying,
3724 parked(true, since),
3725 ] {
3726 assert!(owed.kernel_owes_cqe(), "{owed:?}");
3727 }
3728 for free in [RecvState::Idle, parked(false, since)] {
3729 assert!(!free.kernel_owes_cqe(), "{free:?}");
3730 }
3731 }
3732 }
3733
3734 /// End-to-end soak through the real reader loop: 4 connections
3735 /// concurrently write 10 000 frames each, one `write(2)` per frame,
3736 /// so the receive path churns through the shared buffer pool and
3737 /// its recycle machinery under genuine concurrency (buf_ring where
3738 /// the kernel supports it, legacy ProvideBuffers otherwise — this
3739 /// test is the coverage for whichever mode the host engages).
3740 ///
3741 /// The failure signatures of a recycle bug are exactly what is
3742 /// asserted: a torn or misordered frame (a buffer reused while the
3743 /// kernel still owned it), a lost frame, or a spurious disconnect
3744 /// (`ENOBUFS` mishandled as a client error).
3745 #[test]
3746 fn reader_loop_soak_delivers_every_frame_in_order() {
3747 use std::io::Write as _;
3748
3749 const CONNS: u64 = 4;
3750 const FRAMES: u32 = 10_000;
3751
3752 // Capacity ≥ total frames: even if this thread is descheduled
3753 // and stops draining, the ring cannot fill, so the reader never
3754 // sheds load (which would legitimately drop frames and turn a
3755 // CI hiccup into a false failure).
3756 let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
3757 .add_consumer()
3758 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3759 let mut consumer = consumers.pop().expect("consumer");
3760 let (control_tx, control_rx) = mpsc::channel();
3761 let shutdown = Arc::new(AtomicBool::new(false));
3762
3763 let mut handle = spawn_reader::<TestApp, UnixStream>(
3764 producer,
3765 Arc::new(ByteDecoder),
3766 gate(None, false),
3767 early_channel().0,
3768 Arc::new(IngressMetrics::default()),
3769 policy_claim(),
3770 control_tx,
3771 0, // "do not pin" sentinel
3772 None, // no idle timeout — a CI stall must not disconnect
3773 None, // no tick generator
3774 // Sized for exactly these connections: the smallest pool the
3775 // sizing hands out, so the soak runs the pool dry (ENOBUFS,
3776 // re-arm) far more often than a production-sized one would.
3777 RingSizing::for_max_connections(CONNS).expect("a supported cap"),
3778 Arc::clone(&shutdown),
3779 )
3780 .expect("the reader starts");
3781
3782 let mut writers = Vec::new();
3783 for id in 0..CONNS {
3784 let (client, server_side) = UnixStream::pair().expect("socketpair");
3785 handle.register(ReaderRegistration {
3786 connection_id: ConnectionId(id),
3787 reader: server_side,
3788 addr: "127.0.0.1:1".parse().expect("addr"),
3789 role: ClientRole::Operator,
3790 key_hash: id,
3791 });
3792 writers.push(std::thread::spawn(move || {
3793 let mut client = client;
3794 for i in 0..FRAMES {
3795 let byte = (i % 200 + 1) as u8;
3796 client.write_all(&frame(byte)).expect("client write");
3797 // Brief pauses fragment the stream so the reader
3798 // sees many small recvs (heavy buffer churn) rather
3799 // than a few large coalesced ones.
3800 if i % 512 == 0 {
3801 std::thread::sleep(Duration::from_micros(200));
3802 }
3803 }
3804 client // keep the socket alive until after the drain
3805 }));
3806 }
3807
3808 // Drain until every frame arrived or the deadline passes. The
3809 // deadline is generous — the run takes well under a second on
3810 // an idle machine — because a genuine recycle deadlock must
3811 // fail loudly, not hang CI.
3812 let deadline = Instant::now() + Duration::from_secs(60);
3813 let mut per_conn_count = [0u32; CONNS as usize];
3814 let mut total: u64 = 0;
3815 while total < CONNS * FRAMES as u64 {
3816 assert!(
3817 Instant::now() < deadline,
3818 "soak stalled: {total} of {} frames after 60s ({per_conn_count:?})",
3819 CONNS * FRAMES as u64
3820 );
3821 while let Some((_seq, slot)) = consumer.try_consume() {
3822 let conn = slot.connection_id as usize;
3823 assert!(conn < CONNS as usize, "unknown connection id");
3824 let expected = (per_conn_count[conn] % 200 + 1) as u8;
3825 assert_eq!(
3826 slot.event,
3827 JournalEvent::App(TestEvent::Cmd(expected)),
3828 "conn {conn} frame {} out of order or corrupted",
3829 per_conn_count[conn]
3830 );
3831 per_conn_count[conn] += 1;
3832 total += 1;
3833 }
3834 std::hint::spin_loop();
3835 }
3836
3837 // Any control event at this point is a defect: sockets are
3838 // still alive (held by the writer join handles) and the ring
3839 // never filled. Every arm diverges, so inspecting the first
3840 // queued event suffices.
3841 if let Ok(event) = control_rx.try_recv() {
3842 match event {
3843 ControlEvent::Disconnected { connection_id } => {
3844 panic!("spurious disconnect of connection {connection_id}")
3845 }
3846 ControlEvent::Connected { .. } => {
3847 unreachable!("reader never sends Connected")
3848 }
3849 }
3850 }
3851
3852 let _clients: Vec<UnixStream> = writers
3853 .into_iter()
3854 .map(|w| w.join().expect("writer thread"))
3855 .collect();
3856 handle.shutdown();
3857 handle.join();
3858 }
3859
3860 /// Shutdown with live, armed connections must quiesce the ring
3861 /// provably and promptly. The armed multishot RECVs sit on sockets
3862 /// whose peers never disconnect, so nothing completes them on its
3863 /// own — the teardown's wakes (per-connection `shutdown(2)`, the
3864 /// eventfd write) are what retire them, including on hosts that
3865 /// filter the AsyncCancel opcode, where the old cancel-then-quiet
3866 /// teardown could conclude "done" with arms still live against the
3867 /// soon-freed pool. A drain that cannot prove quiescence only
3868 /// returns at the full deadline (and leaks the buffers), so
3869 /// returning well inside it is the proof this test can observe.
3870 #[test]
3871 fn shutdown_quiesces_armed_connections_promptly() {
3872 use std::io::Write as _;
3873
3874 const CONNS: u64 = 3;
3875 let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(1024)
3876 .add_consumer()
3877 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3878 let mut consumer = consumers.pop().expect("consumer");
3879 let (control_tx, _control_rx) = mpsc::channel();
3880 let shutdown = Arc::new(AtomicBool::new(false));
3881 let mut handle = spawn_reader::<TestApp, UnixStream>(
3882 producer,
3883 Arc::new(ByteDecoder),
3884 gate(None, false),
3885 early_channel().0,
3886 Arc::new(IngressMetrics::default()),
3887 policy_claim(),
3888 control_tx,
3889 0, // "do not pin" sentinel
3890 None, // no idle timeout
3891 None, // no tick generator
3892 RingSizing::for_max_connections(CONNS).expect("a supported cap"),
3893 Arc::clone(&shutdown),
3894 )
3895 .expect("the reader starts");
3896
3897 let mut clients = Vec::new();
3898 for id in 0..CONNS {
3899 let (mut client, server_side) = UnixStream::pair().expect("socketpair");
3900 handle.register(ReaderRegistration {
3901 connection_id: ConnectionId(id),
3902 reader: server_side,
3903 addr: "127.0.0.1:1".parse().expect("addr"),
3904 role: ClientRole::Operator,
3905 key_hash: id,
3906 });
3907 // One delivered frame per connection proves its multishot
3908 // is armed and active before the shutdown fires.
3909 client.write_all(&frame(7)).expect("client write");
3910 clients.push(client);
3911 }
3912 let deadline = Instant::now() + Duration::from_secs(30);
3913 let mut seen = 0u64;
3914 while seen < CONNS {
3915 assert!(
3916 Instant::now() < deadline,
3917 "frames never arrived ({seen}/{CONNS})"
3918 );
3919 while consumer.try_consume().is_some() {
3920 seen += 1;
3921 }
3922 std::hint::spin_loop();
3923 }
3924
3925 // The clients stay alive across the shutdown: no peer
3926 // disconnect can complete the armed RECVs — only the teardown's
3927 // own wakes can.
3928 let started = Instant::now();
3929 handle.shutdown();
3930 handle.join();
3931 let elapsed = started.elapsed();
3932 assert!(
3933 elapsed < crate::uring_teardown::DRAIN_TIMEOUT / 4,
3934 "reader teardown took {elapsed:?}, close to the drain deadline — \
3935 armed operations were not woken"
3936 );
3937 drop(clients);
3938 }
3939
3940 /// Regression for audit review F1
3941 /// (docs/internal/io-uring-audit-2026-08.md): a connection torn down
3942 /// while its multishot RECV is still armed must not have its slab
3943 /// index reused while the kernel can still post CQEs carrying it.
3944 /// Pre-fix, the LIFO free list handed client A's index straight to
3945 /// client B; A's socket stayed open (the armed op holds a file
3946 /// reference past the fd close), and A's continued bytes were parsed
3947 /// under B's connection id, key hash, and permissions — request
3948 /// injection under someone else's identity. Post-fix, A's entry dies
3949 /// in place until the cancelled op's terminal CQE, so B gets a fresh
3950 /// index and A's post-teardown bytes are discarded.
3951 #[test]
3952 fn torn_down_connection_cannot_inject_frames_into_a_reused_slot() {
3953 use std::io::Write as _;
3954
3955 let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
3956 .add_consumer()
3957 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
3958 let mut consumer = consumers.pop().expect("consumer");
3959 let (control_tx, control_rx) = mpsc::channel();
3960 let shutdown = Arc::new(AtomicBool::new(false));
3961 let mut handle = spawn_reader::<TestApp, UnixStream>(
3962 producer,
3963 Arc::new(ByteDecoder),
3964 gate(None, false),
3965 early_channel().0,
3966 Arc::new(IngressMetrics::default()),
3967 policy_claim(),
3968 control_tx,
3969 0,
3970 None,
3971 None,
3972 // A and B, plus room for A's entry to linger while dying.
3973 RingSizing::for_max_connections(4).expect("a supported cap"),
3974 Arc::clone(&shutdown),
3975 )
3976 .expect("the reader starts");
3977
3978 const A_ID: u64 = 100;
3979 const B_ID: u64 = 200;
3980
3981 // Client A: valid frames (0x01..=0x08), an oversize prefix (the
3982 // teardown trigger), then a sustained stream of would-be
3983 // injection frames from A's byte range.
3984 let (a_client, a_server) = UnixStream::pair().expect("socketpair");
3985 handle.register(ReaderRegistration {
3986 connection_id: ConnectionId(A_ID),
3987 reader: a_server,
3988 addr: "127.0.0.1:1".parse().expect("addr"),
3989 role: ClientRole::Operator,
3990 key_hash: A_ID,
3991 });
3992 let a_writer = std::thread::spawn(move || {
3993 let mut a = a_client;
3994 for byte in 0x01..=0x08u8 {
3995 let _ = a.write_all(&frame(byte));
3996 }
3997 let _ = a.write_all(&oversize_prefix());
3998 // Keep writing after the reader repudiates us. Write errors
3999 // (EPIPE once teardown's shutdown(2) lands) are the fix
4000 // doing its job — ignore them and keep trying.
4001 let stop = Instant::now() + Duration::from_millis(300);
4002 while Instant::now() < stop {
4003 let _ = a.write_all(&frame(0x05));
4004 std::thread::sleep(Duration::from_micros(50));
4005 }
4006 a
4007 });
4008
4009 // Wait for A's teardown announcement, then register B — the
4010 // instant a buggy free list would hand B the still-armed index.
4011 let deadline = Instant::now() + Duration::from_secs(10);
4012 loop {
4013 match control_rx.try_recv() {
4014 Ok(ControlEvent::Disconnected { connection_id }) if connection_id == A_ID => break,
4015 Ok(_) => {}
4016 Err(_) => {
4017 assert!(Instant::now() < deadline, "no Disconnected for A");
4018 std::thread::sleep(Duration::from_millis(1));
4019 }
4020 }
4021 }
4022
4023 const B_FRAMES: u32 = 2_000;
4024 let (b_client, b_server) = UnixStream::pair().expect("socketpair");
4025 handle.register(ReaderRegistration {
4026 connection_id: ConnectionId(B_ID),
4027 reader: b_server,
4028 addr: "127.0.0.1:2".parse().expect("addr"),
4029 role: ClientRole::Operator,
4030 key_hash: B_ID,
4031 });
4032 let b_writer = std::thread::spawn(move || {
4033 let mut b = b_client;
4034 for i in 0..B_FRAMES {
4035 let byte = 0x21 + (i % 0x10) as u8; // 0x21..=0x30, disjoint from A
4036 b.write_all(&frame(byte)).expect("B write");
4037 if i % 128 == 0 {
4038 std::thread::sleep(Duration::from_micros(100));
4039 }
4040 }
4041 b
4042 });
4043
4044 // Drain until all of B's frames arrive. Every B-attributed slot
4045 // must carry B's key hash and a B-range byte; anything else is
4046 // the injection this test exists to prevent.
4047 let deadline = Instant::now() + Duration::from_secs(30);
4048 let mut b_seen = 0u32;
4049 while b_seen < B_FRAMES {
4050 assert!(
4051 Instant::now() < deadline,
4052 "stalled: {b_seen}/{B_FRAMES} B frames"
4053 );
4054 while let Some((_seq, slot)) = consumer.try_consume() {
4055 match slot.event {
4056 JournalEvent::App(TestEvent::Cmd(byte)) => {
4057 if slot.connection_id == B_ID {
4058 assert_eq!(slot.key_hash, B_ID, "B slot with foreign key hash");
4059 assert!(
4060 (0x21..=0x30).contains(&byte),
4061 "byte {byte:#x} injected into B's stream"
4062 );
4063 b_seen += 1;
4064 } else {
4065 assert_eq!(slot.connection_id, A_ID, "unknown connection id");
4066 assert!(
4067 (0x01..=0x08).contains(&byte),
4068 "unexpected byte {byte:#x} on A"
4069 );
4070 }
4071 }
4072 ref other => panic!("unexpected event {other:?}"),
4073 }
4074 }
4075 std::hint::spin_loop();
4076 }
4077
4078 let _a = a_writer.join().expect("A writer");
4079 let _b = b_writer.join().expect("B writer");
4080 handle.shutdown();
4081 handle.join();
4082 }
4083
4084 /// Parking through the real reader loop, on a real io_uring over
4085 /// socket pairs: a small input ring the test drains (or not), and an
4086 /// early-reply queue of capacity 2 that the test drains in the
4087 /// response stage's place, so it fills on demand. Filtered requests
4088 /// (`0xFC`) are the early replies that fill it.
4089 mod parking {
4090 use super::*;
4091 use crate::early_reply::EarlyReplyQueue;
4092 use melin_transport_core::fence::FenceState;
4093 use std::io::Write as _;
4094
4095 const FILTERED: u8 = 0xFC;
4096
4097 /// A running reader, the ends the test plays the pipeline with,
4098 /// and its client connections.
4099 struct Rig {
4100 handle: UringReaderHandle<UnixStream>,
4101 consumer: ring::Consumer<InputSlot<TestEvent>>,
4102 queue: EarlyReplyQueue<TestReport>,
4103 control_rx: mpsc::Receiver<ControlEvent>,
4104 fence: Arc<FenceState>,
4105 metrics: Arc<IngressMetrics>,
4106 }
4107
4108 impl Rig {
4109 /// A reader over an input ring of `ring_capacity` and a
4110 /// two-entry early-reply queue, with `connection_timeout`.
4111 fn start(ring_capacity: usize, connection_timeout: Option<Duration>) -> Self {
4112 let (producer, mut consumers) =
4113 DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
4114 .add_consumer()
4115 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
4116 let consumer = consumers.pop().expect("consumer");
4117 let (control_tx, control_rx) = mpsc::channel();
4118 let fence = Arc::new(FenceState::new(0));
4119 let halt = HaltGate::new(None, Arc::clone(&fence), Arc::new(AtomicU64::new(0)));
4120 let (early, queue) = early_channel_of(2);
4121 let metrics = Arc::new(IngressMetrics::default());
4122 let handle = spawn_reader::<TestApp, UnixStream>(
4123 producer,
4124 Arc::new(ByteDecoder),
4125 halt,
4126 early,
4127 Arc::clone(&metrics),
4128 policy_claim(),
4129 control_tx,
4130 0,
4131 connection_timeout,
4132 None,
4133 RingSizing::for_max_connections(4).expect("a supported cap"),
4134 Arc::new(AtomicBool::new(false)),
4135 )
4136 .expect("the reader starts");
4137 Rig {
4138 handle,
4139 consumer,
4140 queue,
4141 control_rx,
4142 fence,
4143 metrics,
4144 }
4145 }
4146
4147 /// Wait until the parked gauge reads `parked`; panics after ten
4148 /// seconds. The gauge is set at the end of the reader's retry
4149 /// pass, which may come just after what the test saw.
4150 fn wait_for_parked(&self, parked: u64) {
4151 let deadline = Instant::now() + Duration::from_secs(10);
4152 while self.metrics.parked() != parked {
4153 assert!(
4154 Instant::now() < deadline,
4155 "the parked gauge stayed at {}, not {parked}",
4156 self.metrics.parked()
4157 );
4158 std::thread::sleep(Duration::from_millis(1));
4159 }
4160 }
4161
4162 /// Register connection `id`; returns the client's end and a
4163 /// second handle on the server's end (the response stage's
4164 /// dup, in a node).
4165 fn connect(&mut self, id: u64) -> (UnixStream, UnixStream) {
4166 let (client, server_side) = UnixStream::pair().expect("socketpair");
4167 let dup = server_side.try_clone().expect("dup");
4168 self.handle.register(ReaderRegistration {
4169 connection_id: ConnectionId(id),
4170 reader: server_side,
4171 addr: "127.0.0.1:1".parse().expect("addr"),
4172 role: ClientRole::Operator,
4173 key_hash: id,
4174 });
4175 (client, dup)
4176 }
4177
4178 /// Every early reply queued so far, as the response stage
4179 /// would take them: making room in the queue.
4180 fn take_replies(&mut self) -> Vec<EarlyReply<TestReport>> {
4181 released(&mut self.queue)
4182 }
4183
4184 /// Every request published so far, by ID.
4185 fn take_published(&mut self) -> Vec<RequestId> {
4186 drain(&mut self.consumer)
4187 .into_iter()
4188 .map(|(_, slot)| slot.request_id)
4189 .collect()
4190 }
4191
4192 /// Wait until `done` holds of what was published and answered
4193 /// so far (accumulated into the two vectors), taking both as
4194 /// the pipeline would; panics after ten seconds.
4195 fn wait_for(
4196 &mut self,
4197 published: &mut Vec<RequestId>,
4198 replies: &mut Vec<EarlyReply<TestReport>>,
4199 done: impl Fn(&[RequestId], &[EarlyReply<TestReport>]) -> bool,
4200 ) {
4201 let deadline = Instant::now() + Duration::from_secs(10);
4202 loop {
4203 published.extend(self.take_published());
4204 replies.extend(self.take_replies());
4205 if done(published, replies) {
4206 return;
4207 }
4208 assert!(
4209 Instant::now() < deadline,
4210 "stalled: published {published:?}, answered {replies:?}"
4211 );
4212 std::thread::sleep(Duration::from_millis(1));
4213 }
4214 }
4215
4216 /// The `Disconnected` events sent so far, by connection.
4217 fn disconnects(&self) -> Vec<u64> {
4218 self.control_rx
4219 .try_iter()
4220 .map(|event| match event {
4221 ControlEvent::Disconnected { connection_id } => connection_id,
4222 ControlEvent::Connected { .. } => {
4223 unreachable!("the reader never sends Connected")
4224 }
4225 })
4226 .collect()
4227 }
4228
4229 fn stop(self) {
4230 self.handle.shutdown();
4231 self.handle.join();
4232 }
4233 }
4234
4235 /// What every test's connection writes: three filtered requests,
4236 /// the third of which parks it (two fill the queue), then five
4237 /// writes, in one burst.
4238 fn burst() -> Vec<u8> {
4239 let mut bytes = Vec::new();
4240 for id in 1..=3 {
4241 bytes.extend(request_frame(id, &[FILTERED]));
4242 }
4243 for id in 4..=8 {
4244 bytes.extend(request_frame(id, &[0x01]));
4245 }
4246 bytes
4247 }
4248
4249 fn filtered_ids(replies: &[EarlyReply<TestReport>]) -> Vec<RequestId> {
4250 replies
4251 .iter()
4252 .map(|reply| {
4253 assert_eq!(reply.kind, EarlyKind::Filtered, "{reply:?}");
4254 reply.request_id
4255 })
4256 .collect()
4257 }
4258
4259 /// Park the connection: write the burst, wait until the reader has
4260 /// answered the first two (left in the queue, which they fill) and
4261 /// parked on the third, and check that nothing behind it was
4262 /// published. The parked gauge is set at the end of a retry pass,
4263 /// so by the time it reads one the park's cancel has landed.
4264 fn park(rig: &mut Rig, client: &mut UnixStream) {
4265 client.write_all(&burst()).expect("client write");
4266 rig.wait_for_parked(1);
4267 let published = rig.take_published();
4268 assert!(published.is_empty(), "published past a park: {published:?}");
4269 }
4270
4271 /// The lost-signals scenario: a parked connection resumes without
4272 /// a byte more from its client once the queue has room, and its
4273 /// pipelined writes all complete, in order, each once. Bytes keep
4274 /// coming while it is parked too, and are kept.
4275 #[test]
4276 fn a_parked_connection_resumes_without_new_bytes() {
4277 let mut rig = Rig::start(64, None);
4278 let (mut client, _dup) = rig.connect(1);
4279 park(&mut rig, &mut client);
4280 // While parked, more writes behind: kept for the retry (a
4281 // data CQE landing before the park's cancel, or the socket's
4282 // buffer after it).
4283 for id in 9..=12 {
4284 client
4285 .write_all(&request_frame(id, &[0x02]))
4286 .expect("client write");
4287 }
4288 std::thread::sleep(Duration::from_millis(50));
4289 assert!(
4290 rig.take_published().is_empty(),
4291 "nothing read past the park"
4292 );
4293 // Counted as parked, once, however many retries it has been
4294 // through; the two filtered requests answered so far counted.
4295 assert_eq!(rig.metrics.parked(), 1);
4296 assert_eq!(rig.metrics.parks(), 1);
4297 assert_eq!(rig.metrics.counts().filtered, 2);
4298
4299 let (mut published, mut replies) = (Vec::new(), Vec::new());
4300 rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 9);
4301 assert_eq!(published, (4..=12).map(RequestId).collect::<Vec<_>>());
4302 assert_eq!(
4303 filtered_ids(&replies),
4304 (1..=3).map(RequestId).collect::<Vec<_>>()
4305 );
4306 assert!(rig.disconnects().is_empty(), "the connection is kept");
4307 // Resumed: no longer parked, and still parked only once; the
4308 // request it parked on counted once, when answered.
4309 rig.wait_for_parked(0);
4310 assert_eq!(rig.metrics.parks(), 1);
4311 assert_eq!(rig.metrics.counts().filtered, 3);
4312
4313 // Read again normally once resumed.
4314 client
4315 .write_all(&request_frame(13, &[0x03]))
4316 .expect("client write");
4317 rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 10);
4318 assert_eq!(published.last(), Some(&RequestId(13)));
4319 drop(client);
4320 rig.stop();
4321 }
4322
4323 /// Many parks and resumes in a row, with the client writing one
4324 /// frame per `write` as fast as it can, so data CQEs keep landing
4325 /// around each park's cancel: every request is published or
4326 /// answered exactly once, the writes in order.
4327 #[test]
4328 fn repeated_parks_lose_and_repeat_nothing() {
4329 const REQUESTS: u64 = 600;
4330 let mut rig = Rig::start(1024, None);
4331 let (client, _dup) = rig.connect(1);
4332 let writer = std::thread::spawn(move || {
4333 let mut client = client;
4334 for id in 1..=REQUESTS {
4335 let body = if id % 3 == 0 { 0x01 } else { FILTERED };
4336 client
4337 .write_all(&request_frame(id, &[body]))
4338 .expect("client write");
4339 }
4340 client
4341 });
4342 let (mut published, mut replies) = (Vec::new(), Vec::new());
4343 rig.wait_for(&mut published, &mut replies, |p, r| {
4344 (p.len() + r.len()) as u64 >= REQUESTS
4345 });
4346 let _client = writer.join().expect("writer");
4347 assert_eq!(
4348 published,
4349 (1..=REQUESTS)
4350 .filter(|id| id % 3 == 0)
4351 .map(RequestId)
4352 .collect::<Vec<_>>()
4353 );
4354 assert_eq!(
4355 filtered_ids(&replies),
4356 (1..=REQUESTS)
4357 .filter(|id| id % 3 != 0)
4358 .map(RequestId)
4359 .collect::<Vec<_>>()
4360 );
4361 assert!(rig.disconnects().is_empty());
4362 rig.stop();
4363 }
4364
4365 /// The idle scan skips a parked connection: its client is not
4366 /// idle, the node is not reading it. Resumed, it is judged on its
4367 /// idle clock restarted then.
4368 #[test]
4369 fn a_parked_connection_is_not_reaped_as_idle() {
4370 let mut rig = Rig::start(64, Some(Duration::from_millis(100)));
4371 let (mut client, _dup) = rig.connect(1);
4372 park(&mut rig, &mut client);
4373 // Past several scans (one a second) and many timeouts.
4374 std::thread::sleep(Duration::from_millis(2_500));
4375 assert!(rig.disconnects().is_empty(), "reaped while parked");
4376
4377 let (mut published, mut replies) = (Vec::new(), Vec::new());
4378 rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 5);
4379 // Idle from its resume on, it is reaped as any other. The scan
4380 // runs only when the reader wakes, so a second, busy client
4381 // keeps waking it.
4382 let (mut other, _other_dup) = rig.connect(2);
4383 let deadline = Instant::now() + Duration::from_secs(10);
4384 let mut disconnects = Vec::new();
4385 let mut id = 100;
4386 while disconnects.is_empty() {
4387 assert!(Instant::now() < deadline, "never reaped once resumed");
4388 id += 1;
4389 other.write_all(&request_frame(id, &[0x01])).expect("write");
4390 rig.take_published();
4391 disconnects.extend(rig.disconnects());
4392 std::thread::sleep(Duration::from_millis(10));
4393 }
4394 assert_eq!(disconnects, [1], "the resumed connection, not the busy one");
4395 drop((client, other));
4396 rig.stop();
4397 }
4398
4399 /// A parked connection whose retry closes it — the node fenced
4400 /// while it was parked — is torn down once: one `Disconnected`,
4401 /// and its index is freed for the next connection.
4402 #[test]
4403 fn a_parked_connection_closed_by_its_retry_is_torn_down_once() {
4404 let mut rig = Rig::start(64, None);
4405 let (mut client, _dup) = rig.connect(1);
4406 // Parked, its cancel landed (see `park`): torn down with nothing
4407 // owed.
4408 park(&mut rig, &mut client);
4409 rig.fence.fence();
4410 let deadline = Instant::now() + Duration::from_secs(10);
4411 let mut disconnects = Vec::new();
4412 while disconnects.is_empty() {
4413 assert!(Instant::now() < deadline, "never torn down");
4414 disconnects.extend(rig.disconnects());
4415 std::thread::sleep(Duration::from_millis(1));
4416 }
4417 std::thread::sleep(Duration::from_millis(50));
4418 disconnects.extend(rig.disconnects());
4419 assert_eq!(disconnects, [1], "torn down once");
4420 assert!(rig.take_published().is_empty());
4421 // Gone, so no longer counted as parked.
4422 rig.wait_for_parked(0);
4423 assert_eq!(rig.metrics.parks(), 1);
4424 rig.stop();
4425 }
4426
4427 /// A request that is not a request, in a shed tail a park cut
4428 /// short, closes the connection when the retry reaches it: the
4429 /// tail before it answered busy, nothing past the full ring
4430 /// published, one `Disconnected`.
4431 #[test]
4432 fn a_non_request_in_a_parked_shed_tail_closes_the_connection_once() {
4433 // Two slots, never drained: the third write is shed.
4434 let mut rig = Rig::start(2, None);
4435 let (mut client, _dup) = rig.connect(1);
4436 let mut bytes = Vec::new();
4437 for id in 1..=5 {
4438 bytes.extend(request_frame(id, &[0x01]));
4439 }
4440 bytes.extend(tagged_frame(0x00, &[0x03; 9]));
4441 client.write_all(&bytes).expect("client write");
4442
4443 let (mut published, mut replies) = (Vec::new(), Vec::new());
4444 // Writes 3 and 4 are shed and fill the queue; 5 parks.
4445 let deadline = Instant::now() + Duration::from_secs(10);
4446 while published.len() < 2 {
4447 assert!(Instant::now() < deadline, "the ring never filled");
4448 published.extend(
4449 drain(&mut rig.consumer)
4450 .into_iter()
4451 .map(|(_, s)| s.request_id),
4452 );
4453 std::thread::sleep(Duration::from_millis(1));
4454 }
4455 // Room in the ring now, but 5 is in the tail.
4456 std::thread::sleep(Duration::from_millis(50));
4457 assert!(rig.disconnects().is_empty(), "parked, not closed");
4458 let mut disconnects = Vec::new();
4459 rig.wait_for(&mut published, &mut replies, |_, r| r.len() == 3);
4460 let deadline = Instant::now() + Duration::from_secs(10);
4461 while disconnects.is_empty() {
4462 assert!(Instant::now() < deadline, "never closed");
4463 disconnects.extend(rig.disconnects());
4464 std::thread::sleep(Duration::from_millis(1));
4465 }
4466 std::thread::sleep(Duration::from_millis(50));
4467 disconnects.extend(rig.disconnects());
4468 assert_eq!(disconnects, [1], "torn down once");
4469 assert_eq!(published, [RequestId(1), RequestId(2)]);
4470 assert_eq!(
4471 replies,
4472 (3..=5)
4473 .map(|id| early_reply_for(1, RequestId(id), EarlyKind::Busy))
4474 .collect::<Vec<_>>()
4475 );
4476 rig.stop();
4477 }
4478
4479 /// A parked connection the response stage tears down (a
4480 /// `shutdown(2)` on its own dup, after a send failure) is freed
4481 /// once: the stage drops the connection's early replies, which
4482 /// makes room, the retry re-arms the receive, and the receive
4483 /// reports the close. One `Disconnected`, and the next connection
4484 /// is served.
4485 #[test]
4486 fn a_parked_connection_torn_down_by_the_response_stage_is_freed_once() {
4487 let mut rig = Rig::start(64, None);
4488 let (mut client, dup) = rig.connect(1);
4489 park(&mut rig, &mut client);
4490 // SAFETY: a live descriptor, owned by `dup`.
4491 unsafe { libc::shutdown(dup.as_raw_fd(), libc::SHUT_RDWR) };
4492 let deadline = Instant::now() + Duration::from_secs(10);
4493 let mut disconnects = Vec::new();
4494 while disconnects.is_empty() {
4495 assert!(Instant::now() < deadline, "never freed");
4496 rig.take_replies();
4497 rig.take_published();
4498 disconnects.extend(rig.disconnects());
4499 std::thread::sleep(Duration::from_millis(1));
4500 }
4501 std::thread::sleep(Duration::from_millis(50));
4502 disconnects.extend(rig.disconnects());
4503 assert_eq!(disconnects, [1], "freed once");
4504
4505 let (mut next, _next_dup) = rig.connect(2);
4506 next.write_all(&request_frame(1, &[0x01])).expect("write");
4507 let mut published = Vec::new();
4508 rig.wait_for(&mut published, &mut Vec::new(), |p, _| {
4509 p.contains(&RequestId(1))
4510 });
4511 rig.stop();
4512 }
4513
4514 /// [`early_reply`] for connection `connection_id`.
4515 fn early_reply_for(
4516 connection_id: u64,
4517 request_id: RequestId,
4518 kind: EarlyKind<TestReport>,
4519 ) -> EarlyReply<TestReport> {
4520 EarlyReply {
4521 connection_id,
4522 request_id,
4523 kind,
4524 }
4525 }
4526 }
4527
4528 /// A reader that cannot create its io_uring instance reports it to
4529 /// the spawner as an error naming the thread, instead of panicking
4530 /// on a thread nobody is watching. A zero-entry ring stands in for
4531 /// the locked-memory limit: both fail `io_uring_setup`.
4532 #[test]
4533 fn a_ring_that_cannot_be_created_is_a_startup_error() {
4534 let (producer, _consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
4535 .add_consumer()
4536 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
4537 let (control_tx, _control_rx) = mpsc::channel();
4538 let result = spawn_reader::<TestApp, UnixStream>(
4539 producer,
4540 Arc::new(ByteDecoder),
4541 gate(None, false),
4542 early_channel().0,
4543 Arc::new(IngressMetrics::default()),
4544 policy_claim(),
4545 control_tx,
4546 0,
4547 None,
4548 None,
4549 RingSizing::unchecked(0, 32),
4550 Arc::new(AtomicBool::new(false)),
4551 );
4552 let err = match result {
4553 Ok(_) => panic!("a zero-entry ring cannot be created"),
4554 Err(e) => e,
4555 };
4556 let msg = err.to_string();
4557 assert!(msg.contains("uring-reader"), "{msg}");
4558 assert!(msg.contains("io_uring"), "{msg}");
4559 }
4560
4561 /// `push_sqe` on a full SQ submits what is queued and pushes again,
4562 /// instead of failing: pushing more SQEs than the ring holds must
4563 /// lose none of them.
4564 #[test]
4565 fn push_sqe_makes_room_when_the_sq_is_full() {
4566 const ENTRIES: u32 = 8;
4567 // Three ring-fulls, so the full-SQ branch is taken more than once.
4568 const PUSHES: u64 = 3 * ENTRIES as u64;
4569 // The CQ is twice the SQ by default; size it to hold every
4570 // completion so none is dropped before it is counted.
4571 let mut ring = IoUring::builder()
4572 .setup_cqsize(PUSHES as u32)
4573 .build(ENTRIES)
4574 .expect("create an 8-entry io_uring");
4575 for token in 0..PUSHES {
4576 let nop = opcode::Nop::new().build().user_data(token);
4577 // SAFETY: a NOP references no memory.
4578 unsafe { push_sqe(&mut ring, &nop, "test NOP") };
4579 }
4580 ring.submit_and_wait(PUSHES as usize)
4581 .expect("submit the remaining NOPs");
4582 let mut seen: Vec<u64> = ring.completion().map(|cqe| cqe.user_data()).collect();
4583 seen.sort_unstable();
4584 assert_eq!(seen, (0..PUSHES).collect::<Vec<_>>());
4585 }
4586}