Skip to main content

melin_server_runtime/
response.rs

1//! io_uring-based response stage — routes matching output to connections via
2//! `IORING_OP_SEND`.
3//!
4//! Replaces the blocking `write(2)` + `BufWriter` flush path with batched
5//! io_uring sends. Instead of N `write(2)` syscalls (one per dirty connection
6//! on flush), we submit N SEND SQEs in a single `io_uring_enter` call.
7//!
8//! Same SPSC consumption and journal cursor gating as `response.rs`.
9//! Runs on a dedicated OS thread.
10
11use std::os::unix::io::RawFd;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::sync::mpsc;
15use std::time::{Duration, Instant};
16
17use io_uring::{IoUring, opcode, types};
18use rustc_hash::FxHashMap;
19use tracing::{debug, error};
20
21use melin_pipeline::ring;
22use melin_pipeline::wait::WaitStrategy;
23
24use crate::ack_policy::{Blocker, CursorView, EvalStatus, MAX_CLUSTER_SIZE, Policy};
25use crate::batch_boundary::{AtBoundary, BatchBoundary};
26use crate::durability_gate::{Admission, Backing, BatchEnds, DurabilityGate, GateInputs, GateWait};
27pub use crate::durability_gate::{GatePolicy, GateStart};
28use crate::early_reply::{EarlyReply, EarlyReplyQueue, Registered};
29use crate::replication::ReplicationMetrics;
30use melin_app::Application;
31use melin_app::amortized_timer::AmortizedTimer;
32use melin_transport_core::pipeline::{OutputSlot, StageUtilization};
33#[cfg(feature = "latency-trace")]
34use melin_transport_core::trace;
35use melin_transport_core::{DurableWireSeqCursor, WireSeq};
36
37use melin_wire_protocol::control::TransportResponse;
38use melin_wire_protocol::control_codec;
39
40use crate::connection_limit::{RingSizing, ring_setup_error};
41use crate::response_frame::{
42    EarlyReplyBuf, EncodeBuf, MAX_APP_FRAME, MAX_EARLY_REPLY, encode_early_reply,
43    encode_slot_payload,
44};
45
46/// Maximum number of output slots consumed per batch.
47const MAX_BATCH: usize = 1024;
48
49/// Maximum accumulated send buffer per connection (64 KiB). If a client
50/// falls behind and the buffer exceeds this, the connection is dropped.
51/// 64 KiB holds ~500 response frames — well beyond any reasonable lag.
52const MAX_SEND_BUF: usize = 64 * 1024;
53
54/// Minimum interval between SEND retries to a connection whose socket
55/// buffer is full (`MSG_DONTWAIT` completed with `EAGAIN`). The stage
56/// busy-spins, so without pacing a blocked connection would cost one
57/// futile `io_uring_enter` per loop iteration. 100 µs adds no meaningful
58/// delivery delay: a full socket buffer drains at the client's read
59/// cadence (millisecond scale), and healthy connections are never paced.
60const BLOCKED_RETRY_INTERVAL: Duration = Duration::from_micros(100);
61
62/// Byte-threshold flush trigger for the consumed (busy) path — the
63/// still-open half of finding 4 in
64/// `docs/internal/latency-audit-2026-07.md`. Under sustained load with
65/// an open durability gate, neither the idle-path flush (SPSC never
66/// empties) nor the gate-path flush (no wait) runs, so without this
67/// trigger responses sit in `send_buf` until `MAX_SEND_BUF` *drops* the
68/// connection instead of serving it. Roughly one TCP MSS (1460 for
69/// standard Ethernet, minus margin): once a connection has a full
70/// segment buffered, flushing early costs nothing in wire efficiency,
71/// and the bound keeps `send_buf` ~45 flushes away from the disconnect
72/// cap instead of zero.
73const FLUSH_BYTES_THRESHOLD: usize = 1400;
74
75/// Slot-count flush trigger: how many appended slots may pass between
76/// flushes before one is forced, regardless of how few bytes any single
77/// connection has buffered.
78///
79/// [`FLUSH_BYTES_THRESHOLD`] bounds a *connection's* buffered bytes but
80/// not their *age*: the interval to the next flush is the time the
81/// busiest connection needs to accumulate an MSS, which grows with
82/// client count. At 4 clients that is microseconds; at 100 clients each
83/// sending ~40 B responses at a modest rate it is milliseconds, and every
84/// connection's already-encoded responses wait that long. Counting slots
85/// instead makes the bound depend on the stage's own throughput: one
86/// extra `io_uring_enter` per 256 responses is well amortized (~0.1 % of
87/// the per-slot cost at 1 M/s) and caps the wait at the time to encode
88/// 256 responses — tens of microseconds — however the load is spread.
89const FLUSH_SLOT_INTERVAL: usize = 256;
90
91/// The consumed path's flush cadence: both triggers plus the counter
92/// they share.
93///
94/// A struct rather than loose locals because the age counter has to be
95/// restarted by *every* flush site (consumed path, pre-gate-wait, idle),
96/// and a missed reset silently shortens the bound. One `on_flush` call
97/// per site is checkable by eye.
98#[derive(Default)]
99struct FlushCadence {
100    /// Slots appended since the last flush — the age bound.
101    slots_since_flush: usize,
102    /// Latched by the byte trigger, cleared by [`Self::on_flush`].
103    due: bool,
104}
105
106impl FlushCadence {
107    /// Record one appended slot on a connection that now holds
108    /// `buffered_bytes` unflushed.
109    #[inline]
110    fn on_append(&mut self, buffered_bytes: usize) {
111        self.slots_since_flush += 1;
112        self.due |= buffered_bytes >= FLUSH_BYTES_THRESHOLD;
113    }
114
115    /// Whether a flush must run before the next slot is encoded.
116    #[inline]
117    fn is_due(&self) -> bool {
118        self.due || self.slots_since_flush >= FLUSH_SLOT_INTERVAL
119    }
120
121    /// Restart both triggers. Call immediately after any `flush_sends`.
122    #[inline]
123    fn on_flush(&mut self) {
124        self.due = false;
125        self.slots_since_flush = 0;
126    }
127}
128
129/// How long a connection's socket may accept *zero* bytes while it has
130/// undelivered data before it is dropped. Partial progress restarts the
131/// clock — a slow-but-draining client is `MAX_SEND_BUF`'s problem, not
132/// this one's. Complements `MAX_SEND_BUF`, which only catches clients
133/// that lag while new responses keep *arriving* — a client that stops
134/// reading during a quiet period accumulates almost nothing (heartbeats
135/// only) and would otherwise pin its buffered bytes and its slot
136/// indefinitely.
137const BLOCKED_SEND_TIMEOUT: Duration = Duration::from_secs(5);
138
139pub use crate::ControlEvent;
140
141/// Encoder type alias: response encoder bound to the application's
142/// `Report` / `QueryResponse` types. Hides the long
143/// `dyn ResponseEncoder<Report = ..., Query = ...>` at call sites.
144pub type ResponseEncoderArc<A> = Arc<
145    dyn melin_app::encoder::ResponseEncoder<
146            Report = <A as Application>::Report,
147            Query = <A as Application>::QueryResponse,
148        >,
149>;
150
151/// Configuration and shared state for the response stage.
152pub struct Response<A: Application> {
153    /// Highest wire seq durably persisted on this node's journal.
154    /// In the same sequence space as `OutputSlot.wire_seq` and the
155    /// replica metrics (`metrics.in_memory_sequence` /
156    /// `metrics.acked_sequence`), so the durability gate can compare
157    /// these values numerically and the comparison is meaningful
158    /// regardless of `starting_sequence` (fresh vs recovered primary).
159    /// Typed [`DurableWireSeqCursor`] so the gate cannot be wired to a
160    /// ring-space counter — the pre-v14 bug class. Updated by the
161    /// journal stage after every fsync batch via `set_last_seq_publisher`.
162    pub journal_persisted_wire_seq: DurableWireSeqCursor,
163    /// The ack policy the gate starts under and the sources of its
164    /// changes — see [`GatePolicy`].
165    pub policy: GatePolicy,
166    /// Per-slot replica cursors. `None` for standalone deployments
167    /// (no replication wiring) — the policy then evaluates against the
168    /// primary alone.
169    pub replication_metrics: Option<Arc<ReplicationMetrics>>,
170    /// Per-slot replica active flags. Only "true" slots are included in
171    /// the cursor view fed to `Policy::evaluate`, so disconnected slots
172    /// don't pollute the view with stale zero cursors. When the
173    /// resulting view is too small to satisfy a clause, the policy
174    /// reports degraded and the gate stalls.
175    /// Mirrors `replication_metrics` — `None` in standalone.
176    pub replica_active: Option<[Arc<AtomicBool>; 2]>,
177    pub heartbeat_interval: Option<Duration>,
178    /// How this thread waits — on an empty output ring, and inside the
179    /// durability gate on the journal-disk and replication threads.
180    pub wait: WaitStrategy,
181    pub utilization: Arc<StageUtilization>,
182    /// Wire encoder for application-shaped payloads. The application's
183    /// encoder, constructed once at boot and shared with the DPDK
184    /// response stage.
185    pub encoder: ResponseEncoderArc<A>,
186    /// Node fencing state. When latched (a higher epoch was observed), the
187    /// stage exits *without* the best-effort flush — in-flight responses
188    /// for requests on a superseded epoch must not be acknowledged. Fencing
189    /// co-sets `shutdown`, so the latch is only consulted on the shutdown
190    /// path (zero steady-state cost). See `crate::fence`.
191    pub fence_state: Arc<melin_transport_core::fence::FenceState>,
192    /// Live-connection count shared with the accept loop's
193    /// `max_connections` gate. Incremented there after auth; decremented
194    /// here — the response stage owns the connection map, so "an entry
195    /// left the map" is the one place a connection verifiably dies
196    /// exactly once, whichever side (reader disconnect or a drop
197    /// decision here) initiated it.
198    pub active_connections: Arc<AtomicU64>,
199    /// The answers the reader decided on without publishing the request:
200    /// writes refused while the node was halted, requests the decoder
201    /// filtered or refused, requests shed on a full input ring. Each is
202    /// sent at the first point between two events' replies, without
203    /// waiting for replies to earlier requests — see [`crate::halt`].
204    pub early_replies: EarlyReplyQueue<A::Report>,
205    /// Sizes the stage's io_uring instance. A flush submits at most one
206    /// SEND per dirty connection, and the connection table never holds
207    /// more than `--max-connections` entries (the accept gate counts
208    /// them), so a ring sized from that cap holds a whole flush — see
209    /// [`RingSizing`].
210    pub ring_sizing: RingSizing,
211    /// Startup report: `Ok` once the stage holds its io_uring instance,
212    /// the creation error otherwise (and the stage returns without
213    /// running). The ring is created on the stage's own thread, which is
214    /// the only one allowed to submit to it, so this is how the spawner
215    /// learns the outcome before it carries on. `None` when nobody waits
216    /// for it; a creation failure is then only logged.
217    pub ready: Option<mpsc::SyncSender<std::io::Result<()>>>,
218    /// Release of held replies on this node's own fsync while it is
219    /// halted for want of a replica; `None` when off. See
220    /// [`crate::halt::DegradedRelease`].
221    pub degraded_release: Option<crate::halt::DegradedRelease>,
222    /// Test seam: called once per iteration right after the control
223    /// channel has been drained, so a test can hold the stage there and
224    /// choose what lands while it waits. The drain runs after the
225    /// output ring is read, and the seam guards that order: were the
226    /// drain moved back in front of the read, the seam would sit
227    /// between the two, and a `Connected` landing there would be
228    /// missed by the read that follows — which `registration_race`
229    /// checks. `None` runs the stage as production does; the field
230    /// itself is compiled out of non-test builds, so the loop pays
231    /// nothing.
232    #[cfg(test)]
233    pub pause_after_control_drain: Option<Box<dyn FnMut() + Send>>,
234}
235
236/// Per-connection state for batched io_uring sends.
237struct ConnectionEntry {
238    fd: RawFd,
239    /// Owns the write half of the socket to keep the fd alive.
240    _owner: Box<dyn Send>,
241    /// Accumulates encoded response frames between flushes.
242    /// The full wire frame (length prefix + payload) is appended here.
243    /// Vec's internal data pointer is heap-stable, so io_uring SEND SQEs
244    /// referencing `as_ptr()` remain valid even if the HashMap relocates
245    /// this struct. No SEND outlives its `flush_sends` call (every SQE
246    /// carries `MSG_DONTWAIT`, and the flush reaps all completions before
247    /// returning), so the Vec is never mutated while the kernel holds its
248    /// pointer.
249    send_buf: Vec<u8>,
250    /// Last time data was sent to this connection. Used for heartbeat scheduling.
251    last_send: Instant,
252    /// When this connection's socket last made zero forward progress:
253    /// set on `EAGAIN`, reset to the flush timestamp on a partial send
254    /// (bytes moved), cleared when the buffer fully drains. `None` while
255    /// the socket is accepting everything. Drives retry pacing and the
256    /// [`BLOCKED_SEND_TIMEOUT`] drop.
257    blocked_since: Option<Instant>,
258    /// Last SEND attempt. Read only while blocked, to pace retries at
259    /// [`BLOCKED_RETRY_INTERVAL`].
260    last_send_attempt: Instant,
261    /// Whether this connection is on the dirty list. A flag on the entry
262    /// rather than set membership: the append path already holds the
263    /// entry (and just wrote its `send_buf`), so the dedup test is a
264    /// byte in a cache line that is hot, instead of a hash + probe on
265    /// every one of the ~2 frames per request. See `Egress::dirty`.
266    dirty: bool,
267}
268
269/// The stage's connections and everything that sends to them: the
270/// io_uring instance, the connection table, the dirty and drop lists, the
271/// flush cadence, and (traced builds) the samples a flush closes.
272///
273/// One struct because every place that sends needs all of it: the slot
274/// loop, the idle path, the flush before a gate wait, and the early
275/// replies sent during one ([`GateWaitHook`]), which runs inside the gate
276/// while the loop's own locals are borrowed.
277struct Egress {
278    ring: IoUring,
279    /// Connection table: maps connection IDs to their state.
280    ///
281    /// `FxHashMap` rather than std's SipHash-1-3 map: this is looked up
282    /// once per output slot on the hot path, and the keys are
283    /// server-generated monotonic connection ids — a client cannot choose
284    /// them, so the HashDoS resistance SipHash buys is worth nothing here
285    /// while its ~20 ns/lookup is not. Pre-sized for a reasonable number
286    /// of concurrent clients.
287    connections: FxHashMap<u64, ConnectionEntry>,
288    /// Connections with buffered (unflushed) writes, carried across
289    /// batches.
290    ///
291    /// `Vec<u64>` paired with `ConnectionEntry::dirty` rather than a
292    /// `HashSet`: membership is never *queried*, only iterated and
293    /// rebuilt, so the set's only contribution on the hot path was
294    /// deduplicating repeat appends — which the entry flag does with a
295    /// branch instead of a hash. Pre-sized to the connection table.
296    dirty: Vec<u64>,
297    /// Connections to remove: an append that would overflow
298    /// `MAX_SEND_BUF`, or a send error in a flush.
299    to_remove: Vec<u64>,
300    /// Pre-allocated CQE collection buffer. Must collect CQEs before
301    /// processing because the CQ borrow must end before mutating
302    /// connections. Pre-sized to the ring's depth (a flush's most SENDs)
303    /// to avoid per-iteration heap allocation.
304    cqes: Vec<(u64, i32)>,
305    /// Flush triggers for the consumed path, and for a drain of early
306    /// replies. Lives across batches because the regime the age bound
307    /// exists for is a ring that never empties: the byte threshold is per
308    /// connection, so with many low-rate clients no single `send_buf`
309    /// reaches an MSS for milliseconds while the stage encodes
310    /// continuously.
311    cadence: FlushCadence,
312    /// See [`Response::active_connections`]: decremented here, as each
313    /// entry leaves the table.
314    active_connections: Arc<AtomicU64>,
315    /// Server-side end-to-end samples awaiting a flush, tagged with the
316    /// connection whose bytes they measure.
317    ///
318    /// The stage encodes into `send_buf` and hands the bytes to the
319    /// kernel later, so closing the sample at encode time would omit
320    /// every microsecond a response spends buffered — the interval this
321    /// stage most needs to be honest about, since that is exactly where a
322    /// deferred flush hides. Holding each frame's `recv_ts` until the
323    /// flush completes measures reader-recv → kernel, which is what the
324    /// stage name claims and what the DPDK path already records (it
325    /// samples after `tx_producers.flush()`).
326    ///
327    /// The connection id is what lets a connection dropped without a
328    /// flush have its samples thrown away rather than closed — see
329    /// `discard_e2e_samples`. Two rules keep the queue honest, and every
330    /// mutation of `dirty` obeys one of them:
331    ///
332    /// - a clear-by-flush closes the queue (`close_e2e_samples`),
333    /// - a removal-by-drop discards that connection's entries
334    ///   (`discard_e2e_samples`).
335    ///
336    /// Together they give the invariant "queue non-empty implies some
337    /// connection is dirty", which is what makes the flush sites
338    /// sufficient drain points.
339    ///
340    /// `Vec` rather than a per-connection map: entries are appended and
341    /// then walked wholesale, order is irrelevant, and the dropped-
342    /// connection lists they are matched against are almost always empty.
343    /// A flat append is the cheapest thing that does that. Capacity is a
344    /// starting point, not a bound — under saturation the output ring
345    /// never empties, so the queue grows until something forces a flush.
346    ///
347    /// Only compiled under `latency-trace`; production builds carry
348    /// neither the buffer nor the pushes.
349    #[cfg(feature = "latency-trace")]
350    pending_e2e: Vec<(u64, trace::MonoTraceInstant)>,
351    #[cfg(feature = "latency-trace")]
352    server_e2e_rec: trace::StageRecorder,
353    /// One sample per io_uring flush that ships responses, batching many
354    /// slots.
355    #[cfg(feature = "tick-to-trade")]
356    egress_rec: trace::StageRecorder,
357}
358
359impl Egress {
360    /// An empty table over `ring`, whose depth sizes the CQE buffer.
361    fn new(
362        ring: IoUring,
363        ring_entries: u32,
364        active_connections: Arc<AtomicU64>,
365        #[cfg(feature = "latency-trace")] server_e2e_rec: trace::StageRecorder,
366        #[cfg(feature = "tick-to-trade")] egress_rec: trace::StageRecorder,
367    ) -> Self {
368        Self {
369            ring,
370            connections: FxHashMap::with_capacity_and_hasher(256, Default::default()),
371            dirty: Vec::with_capacity(256),
372            to_remove: Vec::new(),
373            cqes: Vec::with_capacity(ring_entries as usize),
374            cadence: FlushCadence::default(),
375            active_connections,
376            #[cfg(feature = "latency-trace")]
377            pending_e2e: Vec::with_capacity(MAX_BATCH),
378            #[cfg(feature = "latency-trace")]
379            server_e2e_rec,
380            #[cfg(feature = "tick-to-trade")]
381            egress_rec,
382        }
383    }
384
385    /// Submit every dirty connection's buffer ([`flush_sends`]) and
386    /// restart the flush cadence. Leaves the flush's failures in
387    /// `to_remove`.
388    fn send_dirty(&mut self) {
389        flush_sends(
390            &mut self.ring,
391            &mut self.connections,
392            &mut self.dirty,
393            &mut self.to_remove,
394            &mut self.cqes,
395        );
396        self.cadence.on_flush();
397    }
398
399    /// Flush every dirty connection now, if any is, closing the queued
400    /// samples against it, and tear down the connections the flush
401    /// failed. The idle path's flush, and the one before a gate wait.
402    ///
403    /// Restarts the cadence even with nothing dirty: the appends it
404    /// counted may all have been to connections dropped since, and a
405    /// cadence left due would make every later slot's
406    /// [`Self::flush_if_due`] run again for nothing.
407    fn flush(&mut self) {
408        if self.dirty.is_empty() {
409            self.cadence.on_flush();
410            return;
411        }
412        #[cfg(feature = "tick-to-trade")]
413        let egress_start = trace::mono_trace_ns();
414        self.send_dirty();
415        #[cfg(feature = "tick-to-trade")]
416        self.egress_rec
417            .record_elapsed(egress_start, trace::mono_trace_ns());
418        #[cfg(feature = "latency-trace")]
419        close_e2e_samples(&mut self.pending_e2e, &mut self.server_e2e_rec);
420        self.teardown_flush_failures();
421    }
422
423    /// Tear down the connections a flush failed. They are off the dirty
424    /// list already (see [`flush_sends`]).
425    fn teardown_flush_failures(&mut self) {
426        for conn_id in self.to_remove.drain(..) {
427            if let Some(entry) = self.connections.remove(&conn_id) {
428                teardown_dropped(entry, &self.active_connections);
429            }
430        }
431    }
432
433    /// Tear down the connections an append overflowed, before anything
434    /// flushes them. This un-dirties a connection without flushing it, so
435    /// its queued samples go too: they measure bytes that will never be
436    /// sent, and would otherwise land on some later, unrelated flush.
437    /// Later slots addressed to them are skipped by the table miss.
438    fn drop_overflowed(&mut self) {
439        #[cfg(feature = "latency-trace")]
440        discard_e2e_samples(&mut self.pending_e2e, &self.to_remove);
441        for conn_id in self.to_remove.drain(..) {
442            if let Some(entry) = self.connections.remove(&conn_id) {
443                teardown_dropped(entry, &self.active_connections);
444            }
445            unmark_dirty(conn_id, &mut self.dirty);
446        }
447    }
448
449    /// The consumed path's flush, run between slots and between released
450    /// early replies alike: when the cadence says one is due, drop the
451    /// overflowed connections, so the flush skips them, and flush.
452    ///
453    /// The still-open half of July-audit finding 4, plus its age bound
454    /// (August audit finding 3). Under sustained load with an open gate
455    /// this is the ONLY flush that runs: the idle path needs an empty
456    /// SPSC, the gate path needs a durability wait, and heartbeats need
457    /// idleness. Without it, delivery on a busy stretch degenerates to
458    /// `MAX_SEND_BUF` evicting the very clients being served — and so
459    /// would a drain of early replies, which can release thousands to one
460    /// connection at once. Trigger-gated so light traffic keeps full
461    /// batching; once a connection holds ~an MSS — or the stage has
462    /// appended `FLUSH_SLOT_INTERVAL` times since the last flush,
463    /// whichever comes first — the extra submit no longer costs wire
464    /// efficiency. MSG_DONTWAIT flushes cannot block, so this adds no
465    /// head-of-line exposure (the hazard that deferred this trigger in
466    /// July). A blocked peer re-arms the flag on its later appends, but
467    /// its paced retry inside `flush_sends` keeps that cheap.
468    #[inline]
469    fn flush_if_due(&mut self) {
470        if !self.cadence.is_due() {
471            return;
472        }
473        self.drop_overflowed();
474        self.flush();
475    }
476
477    /// Drain the control channel: register each new connection, forget
478    /// each closed one.
479    fn drain_control(&mut self, control_rx: &mpsc::Receiver<ControlEvent>) {
480        while let Ok(event) = control_rx.try_recv() {
481            match event {
482                ControlEvent::Connected {
483                    connection_id,
484                    fd,
485                    writer,
486                } => {
487                    // The writer keeps the fd alive — store it as the owner.
488                    let owner: Box<dyn Send> = Box::new(writer);
489                    self.connections.insert(
490                        connection_id,
491                        ConnectionEntry {
492                            fd,
493                            _owner: owner,
494                            send_buf: Vec::with_capacity(4096),
495                            last_send: Instant::now(),
496                            blocked_since: None,
497                            last_send_attempt: Instant::now(),
498                            dirty: false,
499                        },
500                    );
501                }
502                ControlEvent::Disconnected { connection_id } => {
503                    // Reader-initiated teardown: the reader already
504                    // closed its half. Decrement only if the entry was
505                    // actually present — a response-initiated drop
506                    // already removed it (and paid the decrement), and
507                    // this event is its echo.
508                    if self.connections.remove(&connection_id).is_some() {
509                        self.active_connections.fetch_sub(1, Ordering::Relaxed);
510                    }
511                    unmark_dirty(connection_id, &mut self.dirty);
512                    // Anything this connection had buffered goes with
513                    // it, so its queued samples measure bytes that will
514                    // never be sent.
515                    #[cfg(feature = "latency-trace")]
516                    discard_e2e_samples(&mut self.pending_e2e, &[connection_id]);
517                }
518            }
519        }
520    }
521
522    /// Send every early reply `registered` holds, between two events'
523    /// replies (`at`), each appended to its connection's buffer
524    /// ([`append_early_reply`]) with the consumed path's flush cadence
525    /// applied after each one ([`Self::flush_if_due`]): a drain can
526    /// release thousands of replies to one connection, and appending them
527    /// all before a flush would take a client that reads its replies past
528    /// `MAX_SEND_BUF`. Flushing here is allowed because the drain holds
529    /// `at`: no event's reply is half-buffered. Returns how many were
530    /// sent; `now` is read once, and only if there is one.
531    fn release_early_replies<R: Copy, Q: Copy>(
532        &mut self,
533        registered: &mut Registered<'_, R>,
534        at: &AtBoundary<'_>,
535        encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
536        early_buf: &mut EarlyReplyBuf,
537        now: impl Fn() -> Instant,
538    ) -> usize {
539        let mut stamp = None;
540        registered.drain(at, |reply| {
541            let now = *stamp.get_or_insert_with(&now);
542            append_early_reply(
543                reply,
544                encoder,
545                &mut self.connections,
546                early_buf,
547                now,
548                &mut self.dirty,
549                &mut self.to_remove,
550                &mut self.cadence,
551            );
552            self.flush_if_due();
553        })
554    }
555}
556
557/// The io_uring stage's [`GateWait`] hook, built for one slot's admit:
558/// flush what is buffered before the wait, and, if the held slot is the
559/// first of its event (`at_boundary`), send early replies during it —
560/// registering new connections first ([`Registered::reregister`]) — and
561/// flush them. A slot held in the middle of an event sends nothing: an
562/// early reply there would split that event's reply.
563struct GateWaitHook<'a, 'q, R: Copy, Q: Copy> {
564    egress: &'a mut Egress,
565    at_boundary: Option<AtBoundary<'a>>,
566    registered: &'a mut Registered<'q, R>,
567    control_rx: &'a mpsc::Receiver<ControlEvent>,
568    encoder: &'a dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
569    early_buf: &'a mut EarlyReplyBuf,
570}
571
572impl<R: Copy, Q: Copy> GateWait for GateWaitHook<'_, '_, R, Q> {
573    /// Drain buffered sends before blocking, never after.
574    ///
575    /// The steady-state flush happens on the `count == 0` path — once the
576    /// output ring is empty. That batches many responses behind one
577    /// `io_uring_enter`, which is the right trade on the fast path. But it
578    /// means a response that has already cleared its own durability gate
579    /// stays in `send_buf` while the loop blocks on a later event's gate,
580    /// so a client waits out an fsync + replica round-trip for an event
581    /// whose durability was confirmed before that event was even
582    /// sequenced.
583    ///
584    /// Flushing here costs nothing in latency terms: it only fires when we
585    /// are about to spin on the gate anyway, so the syscall overlaps time
586    /// that would otherwise be dead. Slots whose gate is already satisfied
587    /// fall straight through and keep their batching.
588    ///
589    /// Traced builds only: this delays the tracker's first `observe` by
590    /// the flush duration, so a cursor that crosses mid-flush is
591    /// attributed late (or, if it crosses past `needed`, not attributed at
592    /// all). Bounded by one flush and confined to the tick-to-trade
593    /// breakdown — the gate's own behaviour is unchanged.
594    ///
595    /// The held slot and later ones addressed to a connection the flush
596    /// drops are skipped by the table lookup after the admit, so removing
597    /// here is safe.
598    fn before_wait(&mut self) {
599        self.egress.flush();
600    }
601
602    /// Early replies decided while the gate holds a write — a refusal, a
603    /// busy — go out now rather than behind it. Nothing to do, and no
604    /// clock read, on a turn with none; one cursor load and a control
605    /// channel poll is the cost of a turn.
606    fn while_waiting(&mut self) {
607        let Some(at) = &self.at_boundary else {
608            return;
609        };
610        let egress = &mut *self.egress;
611        let control_rx = self.control_rx;
612        self.registered
613            .reregister(|| egress.drain_control(control_rx));
614        if egress.release_early_replies(
615            self.registered,
616            at,
617            self.encoder,
618            self.early_buf,
619            Instant::now,
620        ) > 0
621        {
622            egress.drop_overflowed();
623            egress.flush();
624        }
625    }
626}
627
628/// Run the io_uring response stage loop. Blocks the calling thread until shutdown.
629///
630/// Consumes from the output SPSC, waits for durability confirmation, and
631/// sends responses via io_uring SEND.
632///
633/// Ack gating: every gate iteration reads the journal cursor (this
634/// node's persisted) plus per-slot replica cursors (in-memory and
635/// persisted) from `replication_metrics` and feeds them through the
636/// configured [`Policy`]. See `evaluate_durability`.
637///
638/// Returns `true` once the stage has run and stopped on shutdown, `false`
639/// when it never started: its io_uring instance could not be created
640/// (reported through [`Response::ready`]), or nobody was left to report
641/// to.
642pub fn run<A: Application>(
643    mut consumer: ring::Consumer<OutputSlot<A::Report, A::QueryResponse>>,
644    control_rx: mpsc::Receiver<ControlEvent>,
645    config: Response<A>,
646    shutdown: &AtomicBool,
647) -> bool {
648    let Response {
649        journal_persisted_wire_seq,
650        policy,
651        replication_metrics,
652        replica_active,
653        heartbeat_interval,
654        wait,
655        utilization,
656        encoder,
657        fence_state,
658        active_connections,
659        mut early_replies,
660        ring_sizing,
661        ready,
662        degraded_release,
663        #[cfg(test)]
664        mut pause_after_control_drain,
665    } = config;
666    // SINGLE_ISSUER: created and submitted from this thread only — the
667    // kernel skips SQ locking and rejects cross-thread submission with
668    // EEXIST instead of racing. Matches the journal/replication rings.
669    // Created first, before anything else the stage builds, so a node
670    // that cannot have it learns so before the stage touches anything.
671    let ring_entries = ring_sizing.ring_entries();
672    let ring: IoUring = match IoUring::builder().setup_single_issuer().build(ring_entries) {
673        Ok(ring) => ring,
674        Err(e) => {
675            let err = ring_setup_error("response", ring_entries, e);
676            match ready {
677                // Dropped send: a spawner that stopped waiting is
678                // unwinding, and the stage returns either way.
679                Some(ready) => {
680                    let _ = ready.send(Err(err));
681                }
682                None => error!(error = %err, "response stage cannot start"),
683            }
684            return false;
685        }
686    };
687    // Started. A failed send means the spawner is no longer waiting — it
688    // is unwinding — so nothing would ever stop this stage: return.
689    if let Some(ready) = ready
690        && ready.send(Ok(())).is_err()
691    {
692        return false;
693    }
694    // The ack policy in force and the durability gate. The gate owns the
695    // policy for the stage's lifetime and switches it at the slots of
696    // journaled changes. Built here — before the first batch — so a
697    // degraded startup shows on `/healthz` and in the journal
698    // immediately. See `DurabilityGate`.
699    let mut gate = DurabilityGate::new(
700        GateInputs {
701            journal_persisted_wire_seq,
702            replication_metrics,
703            replica_active,
704            utilization: Arc::clone(&utilization),
705            wait,
706            degraded_release,
707        },
708        policy,
709        "response",
710    );
711
712    let mut encode_buf: EncodeBuf = [0u8; MAX_APP_FRAME];
713    // An early reply's frames, encoded together: off the hot path, but on
714    // the stack all the same.
715    let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
716
717    /// Cadence at which the idle spin does its housekeeping *clock read*.
718    /// The heartbeat scan, the policy re-check and the trace-stats flush
719    /// each keep their own (coarser) interval; this only bounds how
720    /// often the loop asks the clock what time it is, so it must stay at
721    /// or below the finest of them (`trace::IDLE_FLUSH_INTERVAL`, 100 ms).
722    const IDLE_HOUSEKEEPING_INTERVAL: Duration = Duration::from_millis(10);
723
724    // Stage histograms registered with the global registry — see
725    // `melin_transport_core::trace`. The four breakdown stages
726    // (journal-wait, replica-wait, encode, egress) feed the bench's
727    // tick-to-trade decomposition; spsc/dispatch/server-e2e are kept
728    // alongside as overall sanity checks.
729    #[cfg(feature = "latency-trace")]
730    let mut spsc_rec =
731        trace::register_stage("response: SPSC wakeup (matching publish → response consume)");
732    #[cfg(feature = "latency-trace")]
733    let mut dispatch_rec = trace::register_stage("response: dispatch (consume → socket write)");
734    #[cfg(feature = "latency-trace")]
735    let server_e2e_rec = trace::register_stage("server e2e (reader recv → response flush)");
736    // Tick-to-trade breakdown: per-slot wait observed for each
737    // durability path (recorded only when the gate actually held us
738    // up — cache-hit paths skip to avoid inflating the metric with
739    // crossings that happened before we noticed). Encode is wall-time
740    // around `encode_transport_response`. Egress wraps a `flush_sends`
741    // call (one sample per io_uring flush, batching many slots).
742    // Gated on `tick-to-trade`, not `latency-trace`, because these
743    // stages roughly double the hot-path mutex traffic vs the lighter
744    // 4-stage mode.
745    #[cfg(feature = "tick-to-trade")]
746    let mut journal_wait_rec =
747        trace::register_stage("response: journal-wait (match_complete → journal cursor crossed)");
748    #[cfg(feature = "tick-to-trade")]
749    let mut replica_wait_rec = trace::register_stage(
750        "response: replica-wait (match_complete → replication cursor crossed)",
751    );
752    #[cfg(feature = "tick-to-trade")]
753    let mut encode_rec = trace::register_stage("response: encode (per-kind wire encoding)");
754    #[cfg(feature = "tick-to-trade")]
755    let egress_rec = trace::register_stage("response: egress (flush_sends elapsed)");
756    // Paces the idle-path recorder flush. Without it, every sample this
757    // thread recorded stays in its thread-local buffer once traffic
758    // stops — which is exactly when the bench scrapes /stats-dump.
759    #[cfg(feature = "latency-trace")]
760    let mut last_stats_flush = Instant::now();
761
762    let mut egress = Egress::new(
763        ring,
764        ring_entries,
765        active_connections,
766        #[cfg(feature = "latency-trace")]
767        server_e2e_rec,
768        #[cfg(feature = "tick-to-trade")]
769        egress_rec,
770    );
771
772    // Pre-encode the heartbeat response frame once. Full wire frame
773    // (length prefix + tag) for direct append to send_buf.
774    let heartbeat_wire_frame = {
775        let mut buf = [0u8; 8];
776        let written =
777            control_codec::encode_transport_response(&TransportResponse::Heartbeat, &mut buf)
778                .expect("heartbeat encodes");
779        buf[..written].to_vec()
780    };
781
782    // Pre-encode the two request terminators once. Unlike the heartbeat
783    // these are on the hot path — every request ends with one — and only
784    // their request ID changes, so the stage writes the ID into one rather
785    // than encode a frame per request. An early reply's batch end is the
786    // plain one: it is a definite answer, with nothing on any disk to
787    // qualify. A busy, as every other early reply, is encoded per reply
788    // with its request's ID (`encode_early_reply`).
789    let mut batch_ends = BatchEnds::new();
790
791    // Coarse timestamp for heartbeat scan — avoids Instant::now() on every spin.
792    let mut last_heartbeat_scan = Instant::now();
793
794    // Gates the idle path's housekeeping clock read. The idle spin used
795    // to call `Instant::now()` twice per iteration (heartbeat scan gate,
796    // policy re-check) to decide it had nothing to do — the same vDSO
797    // tax the DPDK stage removed with this timer.
798    let mut idle_housekeeping_timer = AmortizedTimer::new();
799
800    let mut waiter = wait.waiter();
801
802    let mut busy_count: u64 = 0;
803    let mut idle_count: u64 = 0;
804
805    // Whether the stage stands between two events' replies, the only
806    // place early replies may go: told about every slot handled.
807    let mut boundary = BatchBoundary::new();
808
809    'run: loop {
810        if shutdown.load(Ordering::Relaxed) {
811            // Fence: a superseded ex-primary must not acknowledge any
812            // further in-flight work — skip the best-effort flush so
813            // responses buffered for requests on the old epoch are dropped
814            // (the client sees a connection reset and reconciles on
815            // reconnect). Checked only here, not per slot: fencing always
816            // co-sets `shutdown` (`FenceState::fence_if_superseded` owns
817            // that invariant), and the gate wait comes straight back here
818            // on shutdown, so this branch is the first one a fenced node
819            // reaches and the steady-state loop pays nothing.
820            let flush = !fence_state.is_fenced();
821            // Best-effort flush before shutdown.
822            if flush && !egress.dirty.is_empty() {
823                egress.send_dirty();
824                #[cfg(feature = "latency-trace")]
825                close_e2e_samples(&mut egress.pending_e2e, &mut egress.server_e2e_rec);
826            }
827            utilization.busy.store(busy_count, Ordering::Relaxed);
828            utilization.idle.store(idle_count, Ordering::Relaxed);
829            #[cfg(feature = "pipeline-stats")]
830            print_utilization("response", busy_count, idle_count);
831            return true;
832        }
833
834        // Borrow output slots from the matching stage in place.
835        //
836        // `read_contiguous` rather than `consume_batch`: the latter
837        // memcpy'd every ready slot into a stack array before the first
838        // one was touched, and `OutputSlot` embeds the application's
839        // largest query response (often hundreds of bytes), so the head
840        // slot of a deep batch paid for the whole copy before it could
841        // be encoded. Borrowing costs nothing and the loop below reads
842        // each slot exactly once anyway.
843        //
844        // The progress counter now moves *after* the batch instead of
845        // before it (`consume_batch` committed up front), so the
846        // matching stage cannot reclaim these slots until the loop —
847        // durability gate waits included — is done with them. That is
848        // at most `MAX_BATCH` of a 1 M-slot ring, and the ring fills at
849        // the same rate either way.
850        let slots = consumer.read_contiguous(MAX_BATCH);
851
852        // Take a snapshot of the early replies, then poll the control
853        // channel (non-blocking) for connect and disconnect — after the
854        // ring is read, never before. A reply always trails its
855        // connection's `Connected`: the accept loop queues that event
856        // before it registers the socket with the reader, so no request
857        // can be read, let alone answered, until the event is in the
858        // channel. Every slot in `slots` was published before the read
859        // above, and every early reply in the snapshot before the drain,
860        // so their `Connected` was queued before that, and this drain sees
861        // it. Drained first, the stage could read the ring after a
862        // `Connected` it had not seen and drop the reply — which it did,
863        // now and then, on a fresh node under load. `registration_race`
864        // in the tests holds the stage right after this drain to pin the
865        // order.
866        let mut registered = early_replies.register(|| egress.drain_control(&control_rx));
867
868        // Test seam: hold the stage right after the drain — see
869        // `Response::pause_after_control_drain`.
870        #[cfg(test)]
871        if let Some(pause) = pause_after_control_drain.as_mut() {
872            pause();
873        }
874
875        if slots.is_empty() {
876            // Between two events, the snapshot's early replies go with
877            // this flush. An empty read is not a boundary in itself: the
878            // matching stage may have published only part of an event.
879            if let Some(at) = boundary.at_boundary() {
880                egress.release_early_replies(
881                    &mut registered,
882                    &at,
883                    &*encoder,
884                    &mut early_buf,
885                    Instant::now,
886                );
887                // An early reply can overflow its connection's buffer;
888                // drop those before the flush, as the batch end does.
889                egress.drop_overflowed();
890            }
891
892            // SPSC is empty — flush all dirty connections via io_uring.
893            // This is the response-data egress path; heartbeat flushes
894            // below aren't sampled because they're admin traffic, not
895            // on the client RTT path.
896            egress.flush();
897
898            // Everything below is periodic housekeeping on
899            // sub-second-to-second cadences, and each check needs the
900            // current time only to discover it has nothing to do. One
901            // amortized tick per idle iteration replaces the two
902            // unconditional `Instant::now()` calls that decision used to
903            // cost; the inner intervals are unchanged, so cadences are
904            // the same (see `IDLE_HOUSEKEEPING_INTERVAL`).
905            //
906            // The timer's iteration mask is only engaged once the stage
907            // has been idle for a sustained stretch (the waiter resets
908            // on every consumed batch). That is deliberate: the mask
909            // trades cadence for clock reads, and it should only do so
910            // when clock reads are the loop's whole cost. A stage that
911            // is nearly saturated reaches this path a handful of times
912            // between batches — there the read is already rare, and
913            // masking it would stretch the one-second heartbeat scan
914            // into minutes. A yielding thread never masks either: the
915            // yield syscall dwarfs a vDSO read.
916            //
917            // "Busy-spinning, and has been for a while": under
918            // spin-then-yield a loop past its budget is no longer
919            // spinning, so the conjunction is only ever true under
920            // `BusySpin`.
921            if idle_housekeeping_timer
922                .tick(
923                    IDLE_HOUSEKEEPING_INTERVAL,
924                    waiter.spinning() && waiter.past_spin_budget(),
925                )
926                .is_some()
927            {
928                let now = Instant::now();
929
930                // Send heartbeats to idle connections. Only checked
931                // during idle periods (SPSC empty) to avoid overhead on
932                // the hot path.
933                //
934                // No end-to-end samples to close here: the sample
935                // queue's invariant is that a queued entry implies a
936                // dirty connection, so the flush above — which closes
937                // the queue whenever anything was dirty — leaves it
938                // empty. Anything dirtied from here on is heartbeat
939                // frames, which carry no samples.
940                if let Some(interval) = heartbeat_interval
941                    // Coarse gate: only scan at most once per second.
942                    && now.duration_since(last_heartbeat_scan) >= Duration::from_secs(1)
943                {
944                    last_heartbeat_scan = now;
945                    for (&conn_id, entry) in egress.connections.iter_mut() {
946                        if heartbeat_due(entry, now, interval, heartbeat_wire_frame.len()) {
947                            entry.send_buf.extend_from_slice(&heartbeat_wire_frame);
948                            entry.last_send = now;
949                            mark_dirty(entry, conn_id, &mut egress.dirty);
950                        }
951                    }
952                    // Flush the heartbeat sends immediately. Unsampled:
953                    // admin traffic, and no sample is queued (see above).
954                    if !egress.dirty.is_empty() {
955                        egress.send_dirty();
956                        egress.teardown_flush_failures();
957                    }
958                }
959
960                // Re-evaluate the ack policy on a slow timer so the
961                // `policy_degraded` flag and the periodic warn track the
962                // cluster's real state even on an idle / quiet node —
963                // the no-batch counterpart of `gate.after_batch()`.
964                gate.idle_recheck(now);
965
966                // Hand buffered latency samples to the stats registry
967                // while we have nothing better to do. Reuses the
968                // housekeeping clock read, so the spin path picks up no
969                // extra `clock_gettime`.
970                #[cfg(feature = "latency-trace")]
971                if now.duration_since(last_stats_flush) >= trace::IDLE_FLUSH_INTERVAL {
972                    last_stats_flush = now;
973                    spsc_rec.flush();
974                    dispatch_rec.flush();
975                    egress.server_e2e_rec.flush();
976                    #[cfg(feature = "tick-to-trade")]
977                    {
978                        journal_wait_rec.flush();
979                        replica_wait_rec.flush();
980                        encode_rec.flush();
981                        egress.egress_rec.flush();
982                    }
983                }
984            }
985
986            idle_count += 1;
987            if idle_count.is_multiple_of(1024) {
988                utilization.busy.store(busy_count, Ordering::Relaxed);
989                utilization.idle.store(idle_count, Ordering::Relaxed);
990            }
991            waiter.idle();
992            continue;
993        }
994        waiter.reset();
995        busy_count += 1;
996
997        #[cfg(feature = "latency-trace")]
998        let consume_ts = trace::mono_trace_ns();
999
1000        // Wait for durability confirmation before sending responses.
1001        //
1002        // Gate on `wire_seq`, not `input_seq`. `input_seq` is in
1003        // local-consumer space (the matching cursor on the input ring,
1004        // starts at 0 in this process) while replica metrics and the
1005        // primary's `journal_persisted_wire_seq` live in wire-seq space
1006        // (allocated by the journal stage starting at
1007        // `starting_sequence`). A `needed` derived from `input_seq` and
1008        // compared against wire-seq cursors only works when
1009        // `starting_sequence == 1`; a recovered primary (or any process
1010        // whose journal already has prior content pushing
1011        // `starting_sequence` above 1) would silently open the gate
1012        // ahead of the replica's actual replicated state.
1013        //
1014        // Every cursor in the policy view (`journal_persisted_wire_seq`,
1015        // `metrics.in_memory_sequence`, `metrics.acked_sequence`)
1016        // carries "highest wire seq known to be in that state on node
1017        // X". A slot's `wire_seq` is therefore the *exact* wire seq the
1018        // gate must see — not `+1` — for that slot's event to be
1019        // considered durable. The legacy `+1` was load-bearing only
1020        // because `input_seq` was off by `starting_sequence - 1` from
1021        // wire seq; with the wire-seq stamp it would over-shoot by one
1022        // event and make the gate stall an extra round-trip per
1023        // response.
1024        //
1025        // The gate is evaluated **per slot**, not once per batch against
1026        // the batch's maximum `wire_seq`. Batch-max gating made every
1027        // response in a batch wait for the newest event in it to become
1028        // durable, so the oldest slot paid the durability latency of an
1029        // event sequenced up to `MAX_BATCH` positions after it — a
1030        // head-of-line block bounded only by the batch size, and one
1031        // that bites hardest exactly when the stage has caught up to the
1032        // durability frontier under load. Per-slot gating walks that
1033        // frontier instead: each response is released as soon as its own
1034        // event is durable.
1035        //
1036        // The extra waits are self-limiting. Slots arrive FIFO with
1037        // monotonic `wire_seq`, and the journal publishes its cursor per
1038        // fsync batch, so one wait typically advances the cursor past a
1039        // whole run of following slots and they fall through without
1040        // spinning. The number of waits (and so of flushes) tracks real
1041        // durability boundaries, not the slot count. Correctness does
1042        // not depend on that ordering — an unsorted batch would simply
1043        // wait more often.
1044        let batch_now = Instant::now();
1045
1046        // `flush` (see `FlushCadence`) is fed at append time and checked
1047        // after every slot, so the flush runs *within* the batch.
1048        // Deferring it to the end of the batch would reopen the defect
1049        // the byte threshold exists to close: one MAX_BATCH batch of
1050        // large frames (anything over ~64 bytes average) can push a
1051        // healthy connection from empty past `MAX_SEND_BUF` before a
1052        // batch-end flush ever ran, tearing it down with nothing
1053        // written. A flag written at append time rather than a per-slot
1054        // scan of the dirty set: the scan would cost a hash lookup per
1055        // dirty connection on the hot path.
1056        for slot in slots {
1057            #[cfg(feature = "latency-trace")]
1058            spsc_rec.record_elapsed(slot.match_complete_ts, consume_ts);
1059
1060            // The first slot of an event: the last one handled ended its
1061            // event, so the snapshot's early replies go here, ahead of the
1062            // gate, which holds this event, not them. The iteration's
1063            // snapshot decides for the whole batch; the gate's hook takes
1064            // fresh ones if this slot waits.
1065            let at_boundary = boundary.at_boundary();
1066            if let Some(at) = &at_boundary {
1067                egress.release_early_replies(
1068                    &mut registered,
1069                    at,
1070                    &*encoder,
1071                    &mut early_buf,
1072                    || batch_now,
1073                );
1074            }
1075
1076            // Per-slot journal-wait / replica-wait tracker. See
1077            // `GateCrossTracker` for the rationale (only records cursors
1078            // that were actually on the critical path). Both the
1079            // tracker's replica cursor and the attribution counters
1080            // below are derived from the policy in force rather than a
1081            // fixed level, so "which subsystem to optimize" answers for
1082            // the deployment the operator actually configured. A slot
1083            // that never waits leaves the tracker unobserved, and an
1084            // unobserved tracker reports no crossing — so the fast path
1085            // records nothing rather than a zero-length sample.
1086            #[cfg(feature = "tick-to-trade")]
1087            let mut gate_tracker = GateCrossTracker::new(slot.wire_seq);
1088
1089            // Admit the slot: held until its event is durable under the
1090            // policy in force, and for a policy change, the switch point.
1091            // The hook flushes what is buffered if the slot has to wait,
1092            // and sends early replies during the wait if the slot is the
1093            // first of its event (`GateWaitHook`). What backs a reply
1094            // decides its terminator: the policy, unless the gate released
1095            // it on this node's own fsync while halted.
1096            //
1097            // Shutdown while the gate is closed: straight back to the
1098            // top of the loop, where the shutdown branch decides what to
1099            // flush. This slot's reply, which the policy never confirmed,
1100            // is not appended, and the rest of the batch goes with it.
1101            // Those clients see a reset, as after a crash, and reconcile
1102            // on reconnect.
1103            //
1104            // A policy change slot is the gate's own: its frames go to no
1105            // connection, whatever it is addressed to.
1106            let admission = gate.admit(
1107                slot,
1108                shutdown,
1109                #[cfg(feature = "tick-to-trade")]
1110                &mut gate_tracker,
1111                &mut GateWaitHook {
1112                    egress: &mut egress,
1113                    at_boundary,
1114                    registered: &mut registered,
1115                    control_rx: &control_rx,
1116                    encoder: &*encoder,
1117                    early_buf: &mut early_buf,
1118                },
1119            );
1120            let (destination, backing) = match admission {
1121                Admission::Reply {
1122                    connection_id,
1123                    backing,
1124                } => (Some(connection_id), backing),
1125                Admission::GateOnly => (None, Backing::Policy),
1126                Admission::Shutdown => continue 'run,
1127            };
1128            // Handled from here on, whether or not its frames go
1129            // anywhere: nothing between here and the next slot sends an
1130            // early reply.
1131            boundary.handled(slot.is_last_in_request);
1132
1133            // Per-slot durability-gate breakdown. Recorded only when the
1134            // gate actually held us up (the tracker captured a cross).
1135            // The cross timestamp is this slot's own `wire_seq`, so the
1136            // sample measures the wait that slot really paid — under
1137            // batch-max gating it was the batch maximum's crossing,
1138            // which overstated every slot but the last by up to the
1139            // batch's matching span.
1140            #[cfg(feature = "tick-to-trade")]
1141            if let Some(ts) = gate_tracker.journal_crossed() {
1142                journal_wait_rec.record_elapsed(slot.match_complete_ts, ts);
1143            }
1144            #[cfg(feature = "tick-to-trade")]
1145            if let Some(ts) = gate_tracker.replica_crossed() {
1146                replica_wait_rec.record_elapsed(slot.match_complete_ts, ts);
1147            }
1148
1149            // Each slot expands to at most two wire frames: the
1150            // application payload (Report / Query) and an optional
1151            // trailing `BatchEnd`, naming the slot's request, when
1152            // `is_last_in_request` is set. Application-shaped
1153            // payloads go through the encoder; the transport-shaped
1154            // `BatchEnd` is the runtime's own.
1155            if let Some((connection_id, entry)) =
1156                destination.and_then(|id| egress.connections.get_mut(&id).map(|entry| (id, entry)))
1157            {
1158                // Frame 1: application payload (if any). BatchEnd
1159                // payloads carry no body — the terminator below
1160                // handles them via `is_last_in_request`.
1161                let payload_result = encode_slot_payload(&slot.payload, &*encoder, &mut encode_buf);
1162
1163                // Frame 2: the pre-encoded terminator, `BatchEnd` or
1164                // `BatchEndDegraded` as the slot is backed, with the
1165                // request's ID written in, appended with the payload in
1166                // one call.
1167                let trailer: &[u8] = if slot.is_last_in_request {
1168                    gate.batch_end(backing, &mut batch_ends, slot.request_id)
1169                } else {
1170                    &[]
1171                };
1172
1173                #[cfg(feature = "tick-to-trade")]
1174                let encode_start = trace::mono_trace_ns();
1175                // Underscored: with both frames in one call there is no
1176                // second append to skip, so the outcome is purely
1177                // informational — `append_frames` has already queued an
1178                // overflowing connection for removal. Only the traced
1179                // build reads it, to avoid measuring bytes that were
1180                // never buffered.
1181                let _outcome = append_frames(
1182                    payload_result,
1183                    trailer,
1184                    connection_id,
1185                    entry,
1186                    &encode_buf,
1187                    batch_now,
1188                    &mut egress.dirty,
1189                    &mut egress.to_remove,
1190                );
1191                #[cfg(feature = "tick-to-trade")]
1192                encode_rec.record_elapsed(encode_start, trace::mono_trace_ns());
1193
1194                // Queue the server-side end-to-end sample: reader recv ->
1195                // response flush. Only a request's last slot carries this
1196                // measurement; queued after the append so a dropped
1197                // connection doesn't skew the metric, and closed by
1198                // whichever flush ships the bytes.
1199                #[cfg(feature = "latency-trace")]
1200                if slot.is_last_in_request && matches!(_outcome, AppendOutcome::Continue) {
1201                    egress.pending_e2e.push((connection_id, slot.recv_ts));
1202                }
1203
1204                egress.cadence.on_append(entry.send_buf.len());
1205            } else if let Some(connection_id) = destination.filter(|&id| id != 0) {
1206                // A reply for a connection this stage does not hold: it
1207                // was dropped earlier in this batch, or went away before
1208                // its reply was encoded. The drain above runs after the
1209                // ring is read, so a connection that is merely new is
1210                // never a miss. Logged so a lost reply leaves a trace.
1211                // Connection 0 is a server-originated event — a seed, a
1212                // tick — whose reports have no client to go to, and is
1213                // skipped without a word.
1214                debug!(
1215                    connection_id,
1216                    wire_seq = slot.wire_seq,
1217                    "reply dropped: connection not registered"
1218                );
1219            }
1220
1221            // Consumed-path flush (see `Egress::flush_if_due`). Runs
1222            // between slots, not after the batch, so `MAX_SEND_BUF` can
1223            // never trip on a healthy connection within a single batch.
1224            egress.flush_if_due();
1225        }
1226
1227        // The borrowed slots are dead here, so the batch can be handed
1228        // back to the producer. Publishing progress is deliberately the
1229        // last thing the iteration does with the ring: every slot has
1230        // cleared its durability gate and been encoded by now.
1231        //
1232        // Invariant the event publisher depends on: this consumer's
1233        // progress never passes a slot that has not cleared the gate.
1234        // The publisher's consumer is chained behind this one (see
1235        // `build_pipeline_with_replication`), so this commit is what
1236        // releases events to it; committing earlier, or on the shutdown
1237        // path out of a gate wait (which goes back to the top of the
1238        // loop without committing), would broadcast events the ack
1239        // policy has not confirmed. The DPDK stage keeps the same one.
1240        consumer.commit();
1241
1242        // Remove connections that exceeded the send buffer limit.
1243        egress.drop_overflowed();
1244
1245        // Log degradation transitions / re-emit the reminder — after
1246        // dispatch, not before it. See `DurabilityGate::after_batch`.
1247        gate.after_batch();
1248
1249        #[cfg(feature = "latency-trace")]
1250        dispatch_rec.record_elapsed(consume_ts, trace::mono_trace_ns());
1251    }
1252}
1253
1254/// Close every queued server-side end-to-end sample against the flush
1255/// that just completed. Call immediately after `flush_sends`.
1256///
1257/// Closes the whole queue unconditionally, including entries for a
1258/// connection the flush then dropped on a send error. That is
1259/// deliberate: the stage measures "reader recv → response flush", which
1260/// ends when the bytes are handed to the kernel, not when the peer
1261/// acknowledges them. A failed SEND still produces a sample whose
1262/// duration is the real one for everything this stage controls.
1263/// Contrast [`discard_e2e_samples`], where no flush happens at all.
1264///
1265/// One clock read serves the whole queue: the samples all end at the
1266/// same flush, so reading per entry would only measure the drain loop.
1267#[cfg(feature = "latency-trace")]
1268fn close_e2e_samples(
1269    pending: &mut Vec<(u64, trace::MonoTraceInstant)>,
1270    rec: &mut trace::StageRecorder,
1271) {
1272    let flushed_at = trace::mono_trace_ns();
1273    for (_, recv_ts) in pending.drain(..) {
1274        rec.record_elapsed(recv_ts, flushed_at);
1275    }
1276}
1277
1278/// Drop queued samples belonging to connections going away *without* a
1279/// flush — the disconnect handler and the send-buffer-overflow drain.
1280///
1281/// Their buffered bytes are discarded along with the connection, so
1282/// there is no flush to close the samples against and nothing to
1283/// measure. Left queued, they would instead be timed against some later,
1284/// unrelated connection's flush: a millisecond-scale phantom in a
1285/// microsecond histogram, landing squarely in the tail this stage exists
1286/// to report.
1287///
1288/// Not to be used after `flush_sends`. `dropped` there can hold a
1289/// connection queued by an earlier append overflow whose buffered bytes
1290/// the flush nonetheless shipped — discarding would lose a legitimate
1291/// sample.
1292///
1293/// Linear scan per entry, but `dropped` is empty on every iteration that
1294/// loses no connection, which is essentially all of them, and the early
1295/// return keeps that case free.
1296#[cfg(feature = "latency-trace")]
1297fn discard_e2e_samples(pending: &mut Vec<(u64, trace::MonoTraceInstant)>, dropped: &[u64]) {
1298    if dropped.is_empty() {
1299        return;
1300    }
1301    pending.retain(|(conn_id, _)| !dropped.contains(conn_id));
1302}
1303
1304/// Whether this slot must wait on the durability gate before its
1305/// response is encoded: its own event is not yet known to be durable.
1306///
1307/// Every slot is subject to it. The one reply a halted node sends without
1308/// waiting — the refusal of a write — never enters the output ring (see
1309/// [`crate::halt`]).
1310///
1311/// Shared by both response stages, so the io_uring and DPDK paths cannot
1312/// drift apart on it.
1313#[inline]
1314pub(crate) fn slot_needs_gate<R: Copy, Q: Copy>(
1315    slot: &OutputSlot<R, Q>,
1316    cached_durable_pos: u64,
1317) -> bool {
1318    cached_durable_pos < slot.wire_seq
1319}
1320
1321/// Tear down a connection the *response stage* decided to drop (send
1322/// error, `MAX_SEND_BUF` overflow, blocked-send timeout).
1323///
1324/// Removing the map entry alone only drops this stage's dup of the
1325/// socket — the reader still holds its own dup, so the socket would
1326/// stay fully open: the dropped client's requests keep flowing through
1327/// the reader (whose idle timeout never fires while the client keeps
1328/// sending) with no response ever delivered, and the accept loop's
1329/// `max_connections` permit stays pinned forever. `shutdown(2)` reaches
1330/// every dup: the reader's multishot RECV completes with 0, it tears
1331/// down its slab entry and emits `Disconnected` — which finds this
1332/// entry already gone, so the permit is released exactly once.
1333fn teardown_dropped(entry: ConnectionEntry, active_connections: &AtomicU64) {
1334    // Best-effort: ENOTCONN just means the peer tore the socket down
1335    // first — dropping `entry` below still reclaims our dup.
1336    unsafe {
1337        libc::shutdown(entry.fd, libc::SHUT_RDWR);
1338    }
1339    active_connections.fetch_sub(1, Ordering::Relaxed);
1340}
1341
1342/// Outcome of a slot's append. `Continue` means the slot's bytes are
1343/// buffered (or there were none); `ConnectionDropped` means the
1344/// connection's send buffer overflowed and the connection has been
1345/// queued for removal — nothing further should be appended for it.
1346#[derive(Clone, Copy)]
1347enum AppendOutcome {
1348    Continue,
1349    ConnectionDropped,
1350}
1351
1352/// Whether the idle-path heartbeat scan should append a frame to this
1353/// connection: idle for a full interval — and able to receive it.
1354///
1355/// The two guards close the one append path that used to bypass
1356/// `MAX_SEND_BUF`. A blocked peer's socket is full, so a heartbeat
1357/// would only sit in `send_buf` — and an idle client that trickle-read
1358/// a few bytes per interval kept resetting the blocked clock while
1359/// unchecked heartbeat appends grew its buffer without bound, pinning
1360/// its connection permit on ever-growing memory. Skipping blocked
1361/// peers (they detect liveness by draining, not by new frames) and
1362/// cap-checking the append bounds the buffer; a peer that never drains
1363/// is then `BLOCKED_SEND_TIMEOUT`'s or `MAX_SEND_BUF`'s problem, as
1364/// designed. Residual: a trickling client still holds its permit while
1365/// it drains — bounded memory, and the reader's idle timeout covers
1366/// clients that stop sending entirely.
1367/// Put a connection on the dirty list if it isn't already there.
1368/// Idempotent — the entry flag is the dedup that the old `HashSet`
1369/// membership provided.
1370#[inline]
1371fn mark_dirty(entry: &mut ConnectionEntry, connection_id: u64, dirty: &mut Vec<u64>) {
1372    if !entry.dirty {
1373        entry.dirty = true;
1374        dirty.push(connection_id);
1375    }
1376}
1377
1378/// Take a connection off the dirty list — teardown paths only. `retain`
1379/// rather than a swap-remove scan because the ordering is cheap to keep
1380/// and this runs once per dropped connection, never per frame.
1381fn unmark_dirty(connection_id: u64, dirty: &mut Vec<u64>) {
1382    dirty.retain(|&id| id != connection_id);
1383}
1384
1385fn heartbeat_due(
1386    entry: &ConnectionEntry,
1387    now: Instant,
1388    interval: Duration,
1389    frame_len: usize,
1390) -> bool {
1391    now.duration_since(entry.last_send) >= interval
1392        && entry.blocked_since.is_none()
1393        && entry.send_buf.len() + frame_len <= MAX_SEND_BUF
1394}
1395
1396/// Copy a slot's wire bytes into the connection's send buffer with
1397/// overflow checking and dirty tracking: the encoded payload (from the
1398/// `ResponseEncoder` for application shapes, or
1399/// `encode_transport_response` for transport ones) followed by an
1400/// optional pre-encoded `trailer`.
1401///
1402/// One call per slot rather than one per frame. A request's last slot
1403/// carries a `BatchEnd` terminator behind its payload, and appending
1404/// them separately paid the cap check, the `last_send` stamp and the
1405/// dirty marking twice for every request — plus a re-encode of a frame
1406/// whose bytes never change.
1407#[allow(clippy::too_many_arguments)]
1408fn append_frames(
1409    payload: Option<Result<usize, &'static str>>,
1410    trailer: &[u8],
1411    connection_id: u64,
1412    entry: &mut ConnectionEntry,
1413    encode_buf: &[u8],
1414    batch_now: Instant,
1415    dirty_connections: &mut Vec<u64>,
1416    to_remove: &mut Vec<u64>,
1417) -> AppendOutcome {
1418    // An encode failure is this server's bug, not the client's — log it
1419    // and carry on with the trailer, exactly as when the two frames were
1420    // appended by separate calls.
1421    let written = match payload {
1422        Some(Ok(n)) => n,
1423        Some(Err(reason)) => {
1424            tracing::error!(connection_id, reason, "encode error");
1425            0
1426        }
1427        None => 0,
1428    };
1429    let total = written + trailer.len();
1430    if total == 0 {
1431        return AppendOutcome::Continue;
1432    }
1433
1434    // Drop slow clients whose send buffer has grown too large. This
1435    // prevents unbounded memory growth from a single laggy connection
1436    // causing allocator pressure and tail latency spikes. Checked
1437    // against both frames together, so a request is never delivered
1438    // without its terminator.
1439    if entry.send_buf.len() + total > MAX_SEND_BUF {
1440        debug!(
1441            connection_id,
1442            send_buf_len = entry.send_buf.len(),
1443            "send buffer exceeded limit, dropping connection"
1444        );
1445        to_remove.push(connection_id);
1446        return AppendOutcome::ConnectionDropped;
1447    }
1448
1449    // Append the full wire frames to the connection's send buffer.
1450    // `encode_buf` already holds a complete frame — header written by
1451    // `frame_app_response`, or by the control codec — so no extra
1452    // framing is needed.
1453    entry.send_buf.extend_from_slice(&encode_buf[..written]);
1454    entry.send_buf.extend_from_slice(trailer);
1455    entry.last_send = batch_now;
1456    mark_dirty(entry, connection_id, dirty_connections);
1457    AppendOutcome::Continue
1458}
1459
1460/// Append an answer the reader decided on — a halt rejection, a filtered
1461/// or refused request, a shed one — to its connection's send buffer, its
1462/// frames encoded by [`encode_early_reply`]. A connection that has gone
1463/// away loses nothing it can still receive.
1464///
1465/// No durability gate: nothing was published, so there is nothing to wait
1466/// for. The caller stands between two events' replies, so it splits none
1467/// (see [`crate::halt`]).
1468#[allow(clippy::too_many_arguments)]
1469fn append_early_reply<R: Copy, Q: Copy>(
1470    reply: EarlyReply<R>,
1471    encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
1472    connections: &mut FxHashMap<u64, ConnectionEntry>,
1473    early_buf: &mut EarlyReplyBuf,
1474    now: Instant,
1475    dirty_connections: &mut Vec<u64>,
1476    to_remove: &mut Vec<u64>,
1477    flush: &mut FlushCadence,
1478) {
1479    let Some(entry) = connections.get_mut(&reply.connection_id) else {
1480        debug!(
1481            connection_id = reply.connection_id,
1482            "early reply dropped: connection not registered"
1483        );
1484        return;
1485    };
1486    let written = encode_early_reply(&reply, encoder, early_buf);
1487    append_frames(
1488        Some(Ok(written)),
1489        &[],
1490        reply.connection_id,
1491        entry,
1492        early_buf,
1493        now,
1494        dirty_connections,
1495        to_remove,
1496    );
1497    flush.on_append(entry.send_buf.len());
1498}
1499
1500/// Each dirty connection's accumulated send buffer is sent in a single
1501/// SEND operation, and the flush never blocks on a slow peer.
1502///
1503/// Every SEND carries `MSG_DONTWAIT`, so a full socket buffer completes
1504/// immediately with `EAGAIN` instead of parking the operation until the
1505/// peer reads. That matters because io_uring never surfaces `EAGAIN` for
1506/// a plain SEND — it arms an internal poll and withholds the CQE, which
1507/// is what let one zero-window client wedge this stage (and with it every
1508/// client's acks) behind the `submit_and_wait` below.
1509///
1510/// Undelivered bytes (EAGAIN or a partial send) stay in the connection's
1511/// `send_buf` and the connection stays in `dirty`; retries are paced by
1512/// [`BLOCKED_RETRY_INTERVAL`] and bounded by [`BLOCKED_SEND_TIMEOUT`].
1513/// `MAX_SEND_BUF` remains the growth cap while new responses accumulate.
1514///
1515/// On return, `dirty` holds exactly the connections that still have
1516/// undelivered bytes and were not queued in `to_remove` — callers must
1517/// not clear it. Failed connections are collected in `to_remove` for the
1518/// caller to purge from `connections`.
1519fn flush_sends(
1520    ring: &mut IoUring,
1521    connections: &mut FxHashMap<u64, ConnectionEntry>,
1522    dirty: &mut Vec<u64>,
1523    to_remove: &mut Vec<u64>,
1524    cqes: &mut Vec<(u64, i32)>,
1525) {
1526    // One clock read per flush, shared by the retry-pacing and
1527    // blocked-timeout decisions. Cheap next to the submit syscall below,
1528    // and flushes are already batched (one per drained SPSC batch or
1529    // idle-loop pass, not one per response).
1530    let now = Instant::now();
1531
1532    // Submit SEND SQEs for all dirty connections not in retry backoff.
1533    let mut pending: usize = 0;
1534    for &conn_id in dirty.iter() {
1535        let Some(entry) = connections.get_mut(&conn_id) else {
1536            continue;
1537        };
1538        if entry.send_buf.is_empty() {
1539            continue;
1540        }
1541        if let Some(blocked_at) = entry.blocked_since {
1542            // A peer that has refused bytes for the full timeout is not
1543            // coming back for them — reclaim the slot. Client-caused,
1544            // hence debug (see the log-level convention).
1545            if now.duration_since(blocked_at) >= BLOCKED_SEND_TIMEOUT {
1546                debug!(
1547                    connection_id = conn_id,
1548                    pending_bytes = entry.send_buf.len(),
1549                    blocked_ms = now.duration_since(blocked_at).as_millis() as u64,
1550                    "socket blocked past timeout, dropping connection"
1551                );
1552                to_remove.push(conn_id);
1553                continue;
1554            }
1555            if now.duration_since(entry.last_send_attempt) < BLOCKED_RETRY_INTERVAL {
1556                continue;
1557            }
1558        }
1559        entry.last_send_attempt = now;
1560
1561        let sqe = opcode::Send::new(
1562            types::Fd(entry.fd),
1563            entry.send_buf.as_ptr(),
1564            entry.send_buf.len() as u32,
1565        )
1566        .flags(libc::MSG_DONTWAIT)
1567        .build()
1568        .user_data(conn_id);
1569
1570        unsafe {
1571            ring.submission().push(&sqe).expect(
1572                "io_uring SQ full: a flush submits one SEND per connection, and the ring \
1573                 is sized above --max-connections",
1574            );
1575        }
1576        pending += 1;
1577    }
1578
1579    if pending > 0 {
1580        // With MSG_DONTWAIT every SEND completes during submission
1581        // (success, partial, or EAGAIN — the flag sets the kernel's
1582        // no-wait path, so nothing is deferred to a poll retry). The
1583        // wait is bounded by op execution, never by a peer.
1584        //
1585        // EINTR must retry, not return: a signal (profilers use them
1586        // routinely) can interrupt the CQE wait *after* the submit phase
1587        // consumed the SQEs — the inline completions are already posted
1588        // by then. Returning without reaping would leave stale CQEs that
1589        // the next flush drains against *updated* buffer contents:
1590        // a delivered prefix gets re-sent and `drain(..sent)` double-
1591        // drains. The retry submits an empty SQ and finds the CQ already
1592        // holding `pending` entries, so it cannot stall.
1593        loop {
1594            match ring.submit_and_wait(pending) {
1595                Ok(_) => break,
1596                Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1597                Err(e) => {
1598                    // Submit-phase failure (ENOMEM class — EBUSY cannot
1599                    // happen: the CQ is twice the ring's depth and fully
1600                    // reaped every flush, and `pending` — one SEND per
1601                    // connection — is at most `--max-connections`, below
1602                    // the depth `RingSizing` derives from it, so the CQ
1603                    // never holds more than one flush). An error here means
1604                    // the SQEs were NOT consumed: continuing would leave
1605                    // them queued for the next flush's submit, by which
1606                    // time their addr fields can point at drained or
1607                    // reallocated send_bufs — garbage on client sockets,
1608                    // on the ack path. The kernel is out of resources
1609                    // and the stage cannot proceed without corruption
1610                    // risk: fail loudly and let the accept loop's
1611                    // pipeline-death detection take the server down.
1612                    error!(error = %e, "io_uring submit_and_wait failed in response stage");
1613                    panic!("response stage io_uring submit failed: {e}");
1614                }
1615            }
1616        }
1617
1618        // Drain completions into pre-allocated buffer. Must collect to
1619        // release CQ borrow before mutating connections.
1620        cqes.clear();
1621        cqes.extend(ring.completion().map(|cqe| (cqe.user_data(), cqe.result())));
1622
1623        for &(conn_id, result) in cqes.iter() {
1624            let Some(entry) = connections.get_mut(&conn_id) else {
1625                continue;
1626            };
1627            if result == -libc::EAGAIN {
1628                // Socket buffer full. Keep the bytes; the connection
1629                // stays dirty and retries at the paced cadence.
1630                if entry.blocked_since.is_none() {
1631                    entry.blocked_since = Some(now);
1632                }
1633                continue;
1634            }
1635            if result < 0 {
1636                debug!(
1637                    connection_id = conn_id,
1638                    error = result,
1639                    "send error, dropping connection"
1640                );
1641                to_remove.push(conn_id);
1642                continue;
1643            }
1644
1645            let sent = result as usize;
1646            if sent == 0 {
1647                // Unreachable for SOCK_STREAM sends of len > 0 (the
1648                // kernel reports -EAGAIN instead), but keep the
1649                // zero-progress invariant literal: 0 bytes must not
1650                // restart the blocked clock via the partial branch.
1651                if entry.blocked_since.is_none() {
1652                    entry.blocked_since = Some(now);
1653                }
1654            } else if sent >= entry.send_buf.len() {
1655                entry.send_buf.clear();
1656                entry.blocked_since = None;
1657            } else {
1658                // Partial send — the socket buffer filled mid-copy. An
1659                // immediate retry would only report EAGAIN, so keep the
1660                // remainder for the paced retry. Bytes moved, though:
1661                // restart the blocked clock from this progress, so only
1662                // a peer accepting *zero* bytes for the full timeout is
1663                // dropped — a slow-but-draining client keeps its
1664                // connection (until `MAX_SEND_BUF` says otherwise).
1665                entry.send_buf.drain(..sent);
1666                entry.blocked_since = Some(now);
1667            }
1668        }
1669    }
1670
1671    // Keep exactly the connections that still hold undelivered bytes and
1672    // survived this flush. Runs even when nothing was submitted so a
1673    // blocked-timeout drop above still leaves the dirty list consistent.
1674    // The entry flag is cleared in lockstep — it is what keeps a
1675    // connection off the list until its next append.
1676    dirty.retain(|conn_id| {
1677        let keep = !to_remove.contains(conn_id)
1678            && connections
1679                .get(conn_id)
1680                .is_some_and(|e| !e.send_buf.is_empty());
1681        if !keep && let Some(entry) = connections.get_mut(conn_id) {
1682            entry.dirty = false;
1683        }
1684        keep
1685    });
1686}
1687
1688/// Read the live gate cursors and lend them to `f` as a [`CursorView`].
1689///
1690/// The view contains the primary followed by every *currently connected*
1691/// replica slot. Disconnected slots are omitted rather than entered with
1692/// zero cursors, so `CursorView::len` reflects how many nodes are
1693/// actually available to satisfy a clause — too few and the policy
1694/// reports degraded and the gate stalls. Node 0 is always the primary;
1695/// [`Policy::attribute_blocker`] relies on that index convention to tell
1696/// journal from replication.
1697///
1698/// The primary's in-memory cursor is modeled as `u64::MAX` because the
1699/// response stage only gates events the matching stage has already
1700/// processed — those are trivially in-memory on the primary.
1701///
1702/// Scoped-borrow shape rather than a returned view: `CursorView` holds a
1703/// slice, so it cannot outlive the array behind it. Lending it to a
1704/// closure keeps that array on this frame and off every caller's
1705/// signature.
1706#[inline]
1707fn with_cursor_view<R>(
1708    journal_pos: WireSeq,
1709    metrics: Option<&ReplicationMetrics>,
1710    replica_active: Option<&[Arc<AtomicBool>; 2]>,
1711    f: impl FnOnce(&CursorView<'_>) -> R,
1712) -> R {
1713    // Fixed-size array rather than a `Vec`: the cluster is capped at
1714    // MAX_CLUSTER_SIZE and this runs on the gate path, so a heap
1715    // allocation would be pure overhead. Raw `u64` in wire-seq space —
1716    // `journal_pos` leaves the type system here, alongside the replica
1717    // metrics gauges (the `Ack` frame's wire-seq fields verbatim).
1718    let mut nodes = [[0u64; 2]; MAX_CLUSTER_SIZE as usize];
1719    nodes[0] = [u64::MAX, journal_pos.get()];
1720    let mut len = 1;
1721    if let (Some(m), Some(active)) = (metrics, replica_active) {
1722        for (i, slot_active) in active.iter().enumerate() {
1723            // Skip inactive slots up-front.
1724            if !slot_active.load(Ordering::Acquire) {
1725                continue;
1726            }
1727            let in_mem = m.in_memory_sequence[i].load(Ordering::Acquire);
1728            let persisted = m.acked_sequence[i].load(Ordering::Acquire);
1729            nodes[len] = [in_mem, persisted];
1730            len += 1;
1731        }
1732    }
1733    f(&CursorView::new(&nodes[..len]))
1734}
1735
1736/// Highest sequence at which the policy is satisfied by the live
1737/// cursors, plus whether the cluster shape can satisfy it at all.
1738#[inline]
1739pub(crate) fn evaluate_durability(
1740    policy: &Policy,
1741    journal_pos: WireSeq,
1742    metrics: Option<&ReplicationMetrics>,
1743    replica_active: Option<&[Arc<AtomicBool>; 2]>,
1744) -> EvalStatus {
1745    with_cursor_view(journal_pos, metrics, replica_active, |view| {
1746        policy.evaluate_with_status(view)
1747    })
1748}
1749
1750/// One-snapshot gate evaluation: the policy's durable position plus,
1751/// when that position opens the gate (`>= needed`), which subsystem
1752/// supplied the binding cursor.
1753///
1754/// Both answers come from the *same* [`CursorView`]. Re-reading the
1755/// replica cursors for attribution after the evaluation that opened the
1756/// gate would rank a later snapshot — the replica side can advance
1757/// between the two reads and flip the verdict to a subsystem that was
1758/// not the one the gate actually opened on.
1759///
1760/// The blocker is `None` while the gate stays closed (attribution is
1761/// only read at the open, and ranking the clauses for it on every spin
1762/// iteration would be wasted work). At an open with `needed >= 1` it is
1763/// always `Some`: a policy left unsatisfiable by the cluster shape pins
1764/// `durable_pos` to 0, which cannot reach `needed`.
1765///
1766/// The attribution itself replaces the old "compare the journal cursor
1767/// against the minimum replica *persisted* cursor" heuristic, which
1768/// reported replication as the blocker under `disk` whenever a replica
1769/// merely lagged — a replica binds `persisted>=1` only when its fsync
1770/// leads the journal's — and read the persisted level under
1771/// `disk+ram`, which gates on in-memory.
1772#[inline]
1773pub(crate) fn evaluate_gate(
1774    policy: &Policy,
1775    needed: u64,
1776    journal_pos: WireSeq,
1777    metrics: Option<&ReplicationMetrics>,
1778    replica_active: Option<&[Arc<AtomicBool>; 2]>,
1779) -> (EvalStatus, Option<Blocker>) {
1780    with_cursor_view(journal_pos, metrics, replica_active, |view| {
1781        let status = policy.evaluate_with_status(view);
1782        let blocker = if status.durable_pos >= needed {
1783            policy.attribute_blocker(view)
1784        } else {
1785            None
1786        };
1787        (status, blocker)
1788    })
1789}
1790
1791/// The replica-side cursor the active policy is waiting on, for the
1792/// `tick-to-trade` replica-wait histogram. `None` when no clause is
1793/// replica-supplied (e.g. `disk`).
1794#[cfg(feature = "tick-to-trade")]
1795#[inline]
1796pub(crate) fn policy_replica_cursor(
1797    policy: &Policy,
1798    journal_pos: WireSeq,
1799    metrics: Option<&ReplicationMetrics>,
1800    replica_active: Option<&[Arc<AtomicBool>; 2]>,
1801) -> Option<u64> {
1802    with_cursor_view(journal_pos, metrics, replica_active, |view| {
1803        policy.replica_gate_cursor(view)
1804    })
1805}
1806
1807/// Hold-time before a state transition is committed to the log.
1808/// Suppresses log spam when a replica flaps faster than this — only
1809/// transitions that hold for at least this long emit warn/info
1810/// entries. The `/healthz` gauge updates immediately regardless,
1811/// so dashboards and alerts still see real-time state.
1812const DEGRADED_FLAP_HOLD: Duration = Duration::from_secs(1);
1813
1814/// Tracks degradation state across calls and emits warn/info logs
1815/// with sustained-state gating + a periodic heartbeat re-emit.
1816///
1817/// The hot path calls [`Self::tick`] every gate iteration / idle
1818/// poll with the current `degraded` value and the wall clock. The
1819/// logger handles:
1820///
1821/// - Updating the `policy_degraded` health gauge immediately.
1822/// - Suppressing log lines for transitions that don't hold for at
1823///   least [`DEGRADED_FLAP_HOLD`] — a replica flapping at sub-second
1824///   cadence produces no log noise, only a quietly-updating gauge.
1825/// - Emitting a warn at the moment a sustained degraded state
1826///   crosses the hold threshold, plus a periodic re-emit every
1827///   `heartbeat_interval` while it persists.
1828/// - Emitting an info when a sustained healthy state crosses the
1829///   hold threshold (the cluster is back to its target shape and
1830///   stayed there long enough that we trust the recovery).
1831pub(crate) struct DegradationLogger {
1832    /// Last value passed to `tick`; what we'd log about if it stayed
1833    /// at this value past the hold threshold.
1834    pending_state: bool,
1835    /// When `pending_state` first appeared. Reset on every flip.
1836    pending_since: Instant,
1837    /// Whether the current pending state has been logged yet. Only
1838    /// the *first* log per sustained streak crosses; subsequent
1839    /// re-emits while degraded are heartbeat warns.
1840    pending_logged: bool,
1841    /// When the last warn fired. Drives the periodic re-emit while
1842    /// degraded.
1843    last_log: Option<Instant>,
1844    /// Wall clock of the previous accrual. The interval since this
1845    /// instant is attributed to the state observed then (`pending_state`)
1846    /// and accumulated into the `policy_degraded_nanos` counter. Advanced
1847    /// by [`Self::accrue`] — reached on every `tick` (post-gate, idle, and
1848    /// the gate-wait pacing tick) and on `reseed`.
1849    last_tick: Instant,
1850}
1851
1852impl DegradationLogger {
1853    pub(crate) fn new(now: Instant) -> Self {
1854        Self {
1855            pending_state: false,
1856            pending_since: now,
1857            pending_logged: true, // healthy is the assumed initial state; nothing to log
1858            last_log: None,
1859            last_tick: now,
1860        }
1861    }
1862
1863    /// Use when the policy is known to start in a degraded state
1864    /// (e.g. a primary under `disk+ram` with no replica yet
1865    /// connected). Logs a startup warn immediately and treats the
1866    /// state as already-logged so the next tick doesn't re-emit.
1867    pub(crate) fn new_starting_degraded(now: Instant, policy: &Policy) -> Self {
1868        tracing::warn!(
1869            policy = %policy,
1870            "ack policy starts degraded — fewer connected nodes than the target count"
1871        );
1872        Self {
1873            pending_state: true,
1874            pending_since: now,
1875            pending_logged: true,
1876            last_log: Some(now),
1877            last_tick: now,
1878        }
1879    }
1880
1881    /// Charge the interval `[last_tick, now]` to the degraded-duration
1882    /// counter when `prev_degraded` (the state observed at the previous
1883    /// tick) was degraded, then advance `last_tick`. Drives the
1884    /// `_seconds_total` counter so `rate()` reflects time-in-degraded
1885    /// continuously, even mid-incident, rather than only stepping on
1886    /// recovery. Shared by [`Self::tick`] and [`Self::reseed`] so accrual
1887    /// is never skipped on a logger lifecycle boundary.
1888    #[inline]
1889    fn accrue(&mut self, utilization: &StageUtilization, prev_degraded: bool, now: Instant) {
1890        if prev_degraded {
1891            // `duration_since` saturates to zero when `now < last_tick`;
1892            // `now` is always >= `last_tick` here (single-thread monotonic
1893            // clock), matching the plain `duration_since` used elsewhere
1894            // in this file.
1895            let elapsed = now.duration_since(self.last_tick);
1896            // `as_nanos()` is u128; one inter-tick interval is sub-second
1897            // to a few seconds, far below the u64 nanos ceiling (~584
1898            // years), so the cast can't truncate. See the field doc on
1899            // `StageUtilization::policy_degraded_nanos`.
1900            utilization
1901                .policy_degraded_nanos
1902                .fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed);
1903        }
1904        self.last_tick = now;
1905    }
1906
1907    /// Flush in-flight degraded accrual up to `now`, then reset to a
1908    /// fresh healthy-start logger. Called on a runtime ack-policy
1909    /// swap, which is a logger lifecycle boundary: without flushing
1910    /// first, the pre-swap degraded interval — on the gate-wait path the
1911    /// entire wedge — would be silently dropped. The new policy's actual
1912    /// degraded state is re-derived by the next `tick`, so the brief
1913    /// swap-to-next-tick window is attributed healthy; that residual is
1914    /// bounded by one tick interval, the counter's inherent granularity.
1915    pub(crate) fn reseed(&mut self, utilization: &StageUtilization, now: Instant) {
1916        let prev_state = self.pending_state;
1917        self.accrue(utilization, prev_state, now);
1918        *self = Self::new(now);
1919    }
1920
1921    /// Update the gauge + emit transition/heartbeat logs as needed.
1922    /// Cheap on the hot path: one atomic store, a few branches, one
1923    /// `Instant::duration_since`.
1924    pub(crate) fn tick(
1925        &mut self,
1926        policy: &Policy,
1927        utilization: &StageUtilization,
1928        degraded_now: bool,
1929        now: Instant,
1930        heartbeat_interval: Duration,
1931    ) {
1932        // Charge the just-elapsed interval to the state observed at the
1933        // *previous* tick. Snapshot it up front, before the flap-hold
1934        // bookkeeping below mutates `pending_state`, so accrual stays
1935        // correct even if that block is later reordered.
1936        let prev_state = self.pending_state;
1937        self.accrue(utilization, prev_state, now);
1938
1939        utilization
1940            .policy_degraded
1941            .store(degraded_now, Ordering::Relaxed);
1942
1943        if degraded_now != self.pending_state {
1944            // State changed — start a new hold window. Don't log
1945            // until / unless this new state stays long enough.
1946            self.pending_state = degraded_now;
1947            self.pending_since = now;
1948            self.pending_logged = false;
1949            return;
1950        }
1951
1952        // State held. If we haven't yet logged this streak's onset,
1953        // and it's been pending for at least the flap-hold time,
1954        // emit the transition message and mark logged.
1955        if !self.pending_logged && now.duration_since(self.pending_since) >= DEGRADED_FLAP_HOLD {
1956            if degraded_now {
1957                tracing::warn!(
1958                    policy = %policy,
1959                    "ack policy operating degraded — fewer connected nodes than the target count, response gate stalled until the cluster recovers or the policy is swapped"
1960                );
1961            } else {
1962                tracing::info!(
1963                    policy = %policy,
1964                    "ack policy returned to target shape"
1965                );
1966            }
1967            self.pending_logged = true;
1968            self.last_log = Some(now);
1969            return;
1970        }
1971
1972        // Heartbeat re-emit while a degraded state persists.
1973        if degraded_now
1974            && self.pending_logged
1975            && self
1976                .last_log
1977                .is_none_or(|t| now.duration_since(t) >= heartbeat_interval)
1978        {
1979            tracing::warn!(
1980                policy = %policy,
1981                "ack policy still degraded — fewer connected nodes than the target count"
1982            );
1983            self.last_log = Some(now);
1984        }
1985    }
1986}
1987
1988/// Tracks per-cursor "first observed transition from below to >= needed"
1989/// inside the durability gate loop, to drive the journal-wait /
1990/// replica-wait histograms in the bench's tick-to-trade decomposition.
1991///
1992/// A sample is recorded only for cursors that were strictly below
1993/// `needed` at the loop's first observation. Cursors already past at
1994/// entry were not on the critical path for this batch, so attributing
1995/// "wait time" to them would inflate the metric with cursor-poll
1996/// observation timestamps that have nothing to do with how long the
1997/// stage actually held us up.
1998///
1999/// `now_ns` is taken as a parameter rather than read internally so
2000/// tests can supply deterministic timestamps. The caller's hot path
2001/// reads `trace::mono_trace_ns()` once per gate iteration and feeds it in.
2002#[cfg(feature = "tick-to-trade")]
2003pub(crate) struct GateCrossTracker {
2004    needed: u64,
2005    journal_crossed_ts: Option<trace::MonoTraceInstant>,
2006    replica_crossed_ts: Option<trace::MonoTraceInstant>,
2007    journal_was_below: bool,
2008    replica_was_below: bool,
2009    first: bool,
2010}
2011
2012#[cfg(feature = "tick-to-trade")]
2013impl GateCrossTracker {
2014    pub(crate) fn new(needed: u64) -> Self {
2015        Self {
2016            needed,
2017            journal_crossed_ts: None,
2018            replica_crossed_ts: None,
2019            journal_was_below: false,
2020            replica_was_below: false,
2021            first: true,
2022        }
2023    }
2024
2025    /// `replica_pos` is the replica-side cursor the *active policy*
2026    /// gates on — see [`policy_replica_cursor`]. `None` means no replica
2027    /// currently supplies a binding cursor, which happens permanently
2028    /// under `disk` and transiently when the binding replica drops out
2029    /// of the cursor view part-way through a wait.
2030    ///
2031    /// Both cases must leave `replica_crossed_ts` alone. Treating
2032    /// `None` as an infinite cursor would satisfy the crossing test and
2033    /// latch the timestamp at the *disconnect*, reporting a replica
2034    /// wait that ended when the link died rather than when the replica
2035    /// caught up — understating exactly the failover runs the histogram
2036    /// exists to measure. On the first observe it additionally leaves
2037    /// `replica_was_below` false, so a policy that never waits on a
2038    /// replica records no sample at all.
2039    pub(crate) fn observe(
2040        &mut self,
2041        journal_pos: u64,
2042        replica_pos: Option<u64>,
2043        now_ns: trace::MonoTraceInstant,
2044    ) {
2045        if self.first {
2046            self.journal_was_below = journal_pos < self.needed;
2047            self.replica_was_below = replica_pos.is_some_and(|p| p < self.needed);
2048            self.first = false;
2049        }
2050        if self.journal_was_below && self.journal_crossed_ts.is_none() && journal_pos >= self.needed
2051        {
2052            self.journal_crossed_ts = Some(now_ns);
2053        }
2054        if self.replica_was_below
2055            && self.replica_crossed_ts.is_none()
2056            && replica_pos.is_some_and(|p| p >= self.needed)
2057        {
2058            self.replica_crossed_ts = Some(now_ns);
2059        }
2060    }
2061
2062    pub(crate) fn journal_crossed(&self) -> Option<trace::MonoTraceInstant> {
2063        self.journal_crossed_ts
2064    }
2065
2066    pub(crate) fn replica_crossed(&self) -> Option<trace::MonoTraceInstant> {
2067        self.replica_crossed_ts
2068    }
2069}
2070
2071/// Print busy/idle utilization for a pipeline stage on shutdown.
2072#[cfg(feature = "pipeline-stats")]
2073fn print_utilization(stage: &str, busy: u64, idle: u64) {
2074    let total = busy + idle;
2075    if total == 0 {
2076        tracing::info!(stage, "no iterations recorded");
2077        return;
2078    }
2079    let pct = (busy as f64 / total as f64) * 100.0;
2080    tracing::info!(
2081        stage,
2082        pct_busy = format_args!("{pct:.2}%"),
2083        busy,
2084        idle,
2085        total,
2086        "pipeline utilization",
2087    );
2088}
2089
2090#[cfg(test)]
2091pub(crate) mod tests {
2092    #[cfg(feature = "tick-to-trade")]
2093    use super::GateCrossTracker;
2094    use super::{
2095        Blocker, DegradationLogger, OutputSlot, WireSeq, evaluate_durability, evaluate_gate,
2096        slot_needs_gate,
2097    };
2098    use crate::ack_policy::{AckPolicy, Clause, Level, Policy};
2099    use crate::replication::ReplicationMetrics;
2100
2101    /// Queued end-to-end samples survive a flush that dropped nothing,
2102    /// and are closed (drained) by it.
2103    #[cfg(feature = "latency-trace")]
2104    #[test]
2105    fn e2e_samples_survive_until_a_flush_closes_them() {
2106        let mut pending = vec![(1u64, 10u64), (2, 20), (1, 30)];
2107        super::discard_e2e_samples(&mut pending, &[]);
2108        assert_eq!(pending.len(), 3, "an empty drop list must lose nothing");
2109
2110        let mut rec = melin_transport_core::trace::register_stage("test::response_e2e_close");
2111        super::close_e2e_samples(&mut pending, &mut rec);
2112        assert!(pending.is_empty(), "a flush closes the whole queue");
2113    }
2114
2115    /// The defect this pairing exists to prevent: a dropped connection's
2116    /// buffered bytes are discarded with it, so its samples must not
2117    /// survive to be timed against some later connection's flush.
2118    #[cfg(feature = "latency-trace")]
2119    #[test]
2120    fn e2e_samples_for_a_dropped_connection_are_discarded() {
2121        let mut pending = vec![(1u64, 10u64), (2, 20), (1, 30), (3, 40)];
2122        super::discard_e2e_samples(&mut pending, &[1, 3]);
2123        assert_eq!(
2124            pending,
2125            vec![(2, 20)],
2126            "only the surviving connection's samples remain"
2127        );
2128
2129        // Dropping the last live connection empties the queue, which is
2130        // what restores "queue non-empty implies something is dirty" —
2131        // the invariant the flush sites rely on to be sufficient drain
2132        // points.
2133        super::discard_e2e_samples(&mut pending, &[2]);
2134        assert!(pending.is_empty());
2135    }
2136
2137    /// Output slot carrying only the fields the gate decision reads.
2138    /// `()` for the report/query types keeps the fixture independent of
2139    /// any concrete application.
2140    fn slot(wire_seq: u64) -> OutputSlot<(), ()> {
2141        OutputSlot {
2142            wire_seq,
2143            ..Default::default()
2144        }
2145    }
2146
2147    #[test]
2148    fn slot_gates_until_its_own_sequence_is_durable() {
2149        assert!(slot_needs_gate(&slot(10), 0));
2150        assert!(slot_needs_gate(&slot(10), 9));
2151    }
2152
2153    #[test]
2154    fn slot_clears_at_its_own_sequence_not_the_batch_maximum() {
2155        // The cursor reaching exactly this slot's `wire_seq` is enough —
2156        // an off-by-one here would stall every response an extra
2157        // durability round-trip. A later slot in the same batch needing
2158        // more must not hold this one back.
2159        assert!(!slot_needs_gate(&slot(10), 10));
2160        assert!(!slot_needs_gate(&slot(10), 11));
2161    }
2162
2163    /// Build a [`Policy`] from a mini DSL: one or more
2164    /// `"<level>>=<count>"` clauses joined with `&&`. Test-only
2165    /// ergonomics — production builds policies via
2166    /// [`AckPolicy::to_policy`].
2167    fn parse(s: &str) -> Result<Policy, String> {
2168        let mut clauses = Vec::new();
2169        for raw in s.split("&&") {
2170            let token = raw.trim();
2171            let (lvl, rhs) = token
2172                .split_once(">=")
2173                .ok_or_else(|| format!("clause `{token}` missing `>=`"))?;
2174            let level = match lvl.trim() {
2175                "persisted" => Level::Persisted,
2176                "in_memory" => Level::InMemory,
2177                other => return Err(format!("unknown level `{other}`")),
2178            };
2179            let count: u8 = rhs.trim().parse().map_err(|e| format!("bad count: {e}"))?;
2180            clauses.push(Clause { count, level });
2181        }
2182        Policy::new(clauses).map_err(|e| e.to_string())
2183    }
2184    use melin_transport_core::pipeline::StageUtilization;
2185    use std::sync::Arc;
2186    use std::sync::atomic::{AtomicBool, Ordering};
2187    use std::time::{Duration, Instant};
2188
2189    /// Build a `ReplicationMetrics` with both slots populated. Tests
2190    /// that need to simulate a disconnected slot use [`flags`] to mark
2191    /// it inactive — its cursors are then ignored regardless of value.
2192    fn metrics(slot0: (u64, u64), slot1: (u64, u64)) -> Arc<ReplicationMetrics> {
2193        let m = Arc::new(ReplicationMetrics::default());
2194        m.in_memory_sequence[0].store(slot0.0, Ordering::Relaxed);
2195        m.acked_sequence[0].store(slot0.1, Ordering::Relaxed);
2196        m.in_memory_sequence[1].store(slot1.0, Ordering::Relaxed);
2197        m.acked_sequence[1].store(slot1.1, Ordering::Relaxed);
2198        m
2199    }
2200
2201    /// Build a `[active; 2]` flags array.
2202    fn flags(slot0_active: bool, slot1_active: bool) -> [Arc<AtomicBool>; 2] {
2203        [
2204            Arc::new(AtomicBool::new(slot0_active)),
2205            Arc::new(AtomicBool::new(slot1_active)),
2206        ]
2207    }
2208
2209    /// Both replicas active — the common healthy-cluster case.
2210    fn both_active() -> [Arc<AtomicBool>; 2] {
2211        flags(true, true)
2212    }
2213
2214    // --- Standalone (no replicas wired) ---
2215
2216    #[test]
2217    fn standalone_persisted_one_gates_on_journal() {
2218        // No metrics → only the primary is in the view. `persisted>=1`
2219        // is satisfied by the primary alone at journal_pos.
2220        let p = parse("persisted>=1").unwrap();
2221        assert_eq!(
2222            evaluate_durability(&p, WireSeq::new(500), None, None).durable_pos,
2223            500
2224        );
2225    }
2226
2227    #[test]
2228    fn standalone_strict_persisted_two_never_opens() {
2229        // `persisted>=2` on a standalone primary stays at 0: the
2230        // operator asked for two copies and there is only one. The
2231        // policy surfaces as degraded so the operator sees the gate
2232        // is stalled because the cluster can't meet the policy.
2233        let p = parse("persisted>=2").unwrap();
2234        let r = evaluate_durability(&p, WireSeq::new(500), None, None);
2235        assert_eq!(r.durable_pos, 0);
2236        assert!(
2237            r.degraded,
2238            "policy structurally unsatisfiable on this shape → degraded",
2239        );
2240    }
2241
2242    // --- 2 replicas connected ---
2243
2244    #[test]
2245    fn quorum_both_replicas_ahead_of_journal() {
2246        // Both replicas persisted past journal. `persisted>=2` returns
2247        // the 2nd-largest persisted across {primary, slot0, slot1}.
2248        let p = parse("persisted>=2").unwrap();
2249        let m = metrics((100, 100), (120, 120));
2250        let a = both_active();
2251        assert_eq!(
2252            evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
2253            100
2254        );
2255    }
2256
2257    #[test]
2258    fn quorum_journal_ahead_of_both_replicas() {
2259        // Journal at 500, replicas at 100/120. 2nd-largest persisted = 120.
2260        let p = parse("persisted>=2").unwrap();
2261        let m = metrics((100, 100), (120, 120));
2262        let a = both_active();
2263        assert_eq!(
2264            evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
2265            120
2266        );
2267    }
2268
2269    #[test]
2270    fn quorum_journal_between_slow_and_fast_replica() {
2271        // {primary=150, slot0_persisted=50, slot1_persisted=200}.
2272        // 2nd-largest = 150 (primary itself).
2273        let p = parse("persisted>=2").unwrap();
2274        let m = metrics((50, 50), (200, 200));
2275        let a = both_active();
2276        assert_eq!(
2277            evaluate_durability(&p, WireSeq::new(150), Some(&m), Some(&a)).durable_pos,
2278            150
2279        );
2280    }
2281
2282    #[test]
2283    fn ram_quorum_gates_on_replica_memory_not_on_any_disk() {
2284        // `ram` (`in_memory>=2`): the journal position must not
2285        // bind. Journal at 0 — nothing persisted anywhere on the
2286        // primary — while the replicas hold 100/120 in memory. The
2287        // 2nd-largest in-memory across {primary=MAX, 100, 120} is 120:
2288        // the gate is exactly the fastest replica's RAM receipt.
2289        let p = AckPolicy::Ram.to_policy();
2290        let m = metrics((100, 0), (120, 0));
2291        let a = both_active();
2292        let r = evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a));
2293        assert_eq!(r.durable_pos, 120);
2294        assert!(!r.degraded);
2295    }
2296
2297    #[test]
2298    fn ram_quorum_stalls_with_no_replica_connected() {
2299        // Fail-closed: with only the primary in the view, `in_memory>=2`
2300        // is structurally unsatisfiable however far the journal is.
2301        let p = AckPolicy::Ram.to_policy();
2302        let r = evaluate_durability(&p, WireSeq::new(500), None, None);
2303        assert_eq!(r.durable_pos, 0);
2304        assert!(r.degraded);
2305    }
2306
2307    // --- Single replica connected ---
2308
2309    #[test]
2310    fn single_replica_strict_persisted_two_requires_both_survivors() {
2311        // Slot 0 connected, slot 1 disconnected. View = {primary, slot0}.
2312        // Strict `persisted>=2`: 2nd-largest of the 2-row view =
2313        // min(primary, slot0). Strictly stronger than legacy auto-
2314        // degrade-to-1-node in the same shape.
2315        let p = parse("persisted>=2").unwrap();
2316        let m = metrics((100, 100), (999, 999)); // slot 1 cursors ignored
2317        let a = flags(true, false);
2318        assert_eq!(
2319            evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
2320            50
2321        );
2322        assert_eq!(
2323            evaluate_durability(&p, WireSeq::new(200), Some(&m), Some(&a)).durable_pos,
2324            100
2325        );
2326    }
2327
2328    #[test]
2329    fn single_replica_persisted_two_requires_both_survivors() {
2330        // 2-node view (primary + surviving replica). `persisted>=2` is
2331        // satisfiable; the gate opens at the slower of the two and the
2332        // policy is not degraded.
2333        let p = parse("persisted>=2").unwrap();
2334        let m = metrics((100, 100), (999, 999));
2335        let a = flags(true, false);
2336        let r = evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a));
2337        assert_eq!(r.durable_pos, 50);
2338        assert!(!r.degraded);
2339    }
2340
2341    #[test]
2342    fn both_replicas_disconnected_strict_stalls() {
2343        // View has only the primary. Strict `persisted>=2` cannot be
2344        // satisfied — operator opted out of degrade.
2345        let p = parse("persisted>=2").unwrap();
2346        let m = metrics((999, 999), (999, 999));
2347        let a = flags(false, false);
2348        assert_eq!(
2349            evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
2350            0
2351        );
2352    }
2353
2354    #[test]
2355    fn both_replicas_disconnected_strict_stalls_and_flags_degraded() {
2356        // With `persisted>=2` and both replicas down, the cursor view
2357        // collapses to {primary}: the clause's count (=2) exceeds the
2358        // view size, so the gate stays at 0 and the policy flags
2359        // degraded. Note the readers' separate halt at
2360        // `replicas_connected==0` refuses new writes before they reach
2361        // the gate; this verifies the gate semantics in isolation.
2362        let p = parse("persisted>=2").unwrap();
2363        let m = metrics((999, 999), (999, 999));
2364        let a = flags(false, false);
2365        let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
2366        assert_eq!(r.durable_pos, 0);
2367        assert!(r.degraded);
2368    }
2369
2370    // --- Mixed-level policies ---
2371
2372    #[test]
2373    fn persisted_one_and_in_memory_two() {
2374        // "Leader persists, plus one other node has it in memory" —
2375        // the cheap-but-non-zero durability target. Slot 0 has it in
2376        // memory, slot 1 disconnected.
2377        let p = parse("persisted>=1 && in_memory>=2").unwrap();
2378        // primary persisted=50, slot0 in_mem=80 / persisted=20.
2379        // persisted>=1: max(50, 20, 0) = 50.
2380        // in_memory>=2: primary in_mem=u64::MAX (always), slot0_eff=max(80, 20)=80,
2381        //               slot1=0. 2nd-largest = 80.
2382        // min(50, 80) = 50.
2383        let m = metrics((80, 20), (999, 999));
2384        let a = flags(true, false);
2385        assert_eq!(
2386            evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
2387            50
2388        );
2389    }
2390
2391    // --- Edge: journal at 0 ---
2392
2393    #[test]
2394    fn journal_at_zero_with_replicas_persisted_one() {
2395        // Journal hasn't fsynced anything; both replicas have. With
2396        // `persisted>=1` the gate opens at the fastest replica.
2397        let p = parse("persisted>=1").unwrap();
2398        let m = metrics((100, 100), (200, 200));
2399        let a = both_active();
2400        assert_eq!(
2401            evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a)).durable_pos,
2402            200
2403        );
2404    }
2405
2406    // --- gate-blocker attribution ---
2407    //
2408    // The attribution must follow the policy actually in force. The
2409    // previous heuristic compared the journal cursor against the
2410    // minimum replica *persisted* cursor unconditionally, which
2411    // reported replication as the blocker under `disk` whenever a
2412    // replica lagged (a replica binds `persisted>=1` only when its fsync
2413    // leads) and read the wrong level under `disk+ram` (which gates
2414    // replicas on in-memory).
2415    //
2416    // Attribution comes out of `evaluate_gate` alongside the durable
2417    // position, computed from the same cursor snapshot, and is `None`
2418    // while the gate stays closed — so each test passes a `needed` at
2419    // or below the scenario's durable position to model the iteration
2420    // on which the gate opens.
2421
2422    #[test]
2423    fn disk_policy_credits_journal_when_replicas_lag() {
2424        // `persisted>=1` is satisfied by the highest persisted cursor
2425        // in the cluster — here the primary's. A connected replica lagging
2426        // far behind is irrelevant to the gate, so it must not be
2427        // credited as the blocker. This is the case the old heuristic
2428        // got flatly wrong: journal_pos (500) > repl_min (10) sent it
2429        // down the `else` branch every single time.
2430        let p = parse("persisted>=1").unwrap();
2431        let m = metrics((10, 10), (10, 10));
2432        let a = both_active();
2433        assert_eq!(
2434            evaluate_gate(&p, 500, WireSeq::new(500), Some(&m), Some(&a)).1,
2435            Some(Blocker::Journal)
2436        );
2437    }
2438
2439    #[test]
2440    fn disk_policy_credits_replication_when_a_replica_fsync_leads() {
2441        // The mirror: policies count copies, not nodes. With the
2442        // primary's disk stalled at 10 and a replica persisted through
2443        // 500, `persisted>=1` opens on the replica's copy — and the
2444        // attribution must say so, or an operator reading the gate
2445        // counters would conclude the primary's disk was keeping up.
2446        let p = parse("persisted>=1").unwrap();
2447        let m = metrics((500, 500), (10, 10));
2448        let a = both_active();
2449        assert_eq!(
2450            evaluate_gate(&p, 500, WireSeq::new(10), Some(&m), Some(&a)).1,
2451            Some(Blocker::Replication)
2452        );
2453    }
2454
2455    #[test]
2456    fn disk_and_ram_reads_replica_in_memory_not_persisted() {
2457        // `persisted>=1 && in_memory>=2`. The replica has the event in
2458        // memory (400) well ahead of its own fsync (100), and the
2459        // primary's journal is behind that in-memory cursor (300).
2460        // The binding clause is therefore the primary's persisted
2461        // cursor → journal. Comparing against the replica's *persisted*
2462        // cursor (100) would have said replication.
2463        let p = parse("persisted>=1 && in_memory>=2").unwrap();
2464        let m = metrics((400, 100), (0, 0));
2465        let a = flags(true, false);
2466        assert_eq!(
2467            evaluate_gate(&p, 300, WireSeq::new(300), Some(&m), Some(&a)).1,
2468            Some(Blocker::Journal)
2469        );
2470
2471        // Same policy, replica in-memory now the laggard → replication.
2472        // The gate opens at the replica's in-memory cursor (200).
2473        let m = metrics((200, 100), (0, 0));
2474        assert_eq!(
2475            evaluate_gate(&p, 200, WireSeq::new(300), Some(&m), Some(&a)).1,
2476            Some(Blocker::Replication)
2477        );
2478    }
2479
2480    #[test]
2481    fn disk_and_ram_is_satisfied_by_the_faster_replica() {
2482        // `in_memory>=2` needs the primary plus *one* replica, so the
2483        // best replica binds, not the worst. Replica 0 holds the event
2484        // in memory at 400 with its own fsync trailing at 250 (behind
2485        // the primary, as the physical ordering requires); replica 1
2486        // lags badly at 10. The gate is not held up by replication —
2487        // the primary's persisted cursor (300) is the binding term.
2488        // Taking the min across replicas would have blamed the slow
2489        // slot.
2490        let p = parse("persisted>=1 && in_memory>=2").unwrap();
2491        let m = metrics((400, 250), (10, 10));
2492        let a = both_active();
2493        assert_eq!(
2494            evaluate_gate(&p, 300, WireSeq::new(300), Some(&m), Some(&a)).1,
2495            Some(Blocker::Journal)
2496        );
2497    }
2498
2499    #[test]
2500    fn two_disks_uses_second_largest_persisted() {
2501        // `persisted>=2` is met by the primary plus the *best* replica's
2502        // persisted cursor. With the primary ahead at 900 and replicas
2503        // at 400 and 10, the clause resolves at 400 — the fast replica —
2504        // not at the minimum of 10.
2505        let p = parse("persisted>=2").unwrap();
2506        let m = metrics((400, 400), (10, 10));
2507        let a = both_active();
2508        assert_eq!(
2509            evaluate_durability(&p, WireSeq::new(900), Some(&m), Some(&a)).durable_pos,
2510            400,
2511            "clause takes the second-largest persisted cursor, not the minimum"
2512        );
2513        // Replication binds, which is the norm for this policy: the
2514        // primary persists before it ships, so it is normally ahead of
2515        // every replica.
2516        assert_eq!(
2517            evaluate_gate(&p, 400, WireSeq::new(900), Some(&m), Some(&a)).1,
2518            Some(Blocker::Replication)
2519        );
2520    }
2521
2522    #[test]
2523    fn standalone_credits_the_journal() {
2524        // No replication wired: the primary is the only node, so the
2525        // journal is the only thing the gate can be waiting on.
2526        let p = parse("persisted>=1").unwrap();
2527        assert_eq!(
2528            evaluate_gate(&p, 500, WireSeq::new(500), None, None).1,
2529            Some(Blocker::Journal)
2530        );
2531    }
2532
2533    #[test]
2534    fn unsatisfiable_shape_never_opens_so_never_attributes() {
2535        // `persisted>=2` with no replica connected: the shape pins
2536        // `durable_pos` to 0, so the gate cannot open and no blocker is
2537        // ever produced — the stall is a missing node, not either
2538        // subsystem's progress. `policy_degraded` is the metric for it.
2539        let p = parse("persisted>=2").unwrap();
2540        let (status, blocker) = evaluate_gate(&p, 1, WireSeq::new(500), None, None);
2541        assert!(status.degraded);
2542        assert_eq!(status.durable_pos, 0);
2543        assert_eq!(blocker, None);
2544    }
2545
2546    #[test]
2547    fn closed_gate_yields_no_attribution() {
2548        // The policy is satisfiable and healthy, but the batch needs a
2549        // sequence the cursors haven't reached — the gate stays closed
2550        // and `evaluate_gate` must not spend a ranking pass (nor name a
2551        // blocker) on an iteration that keeps spinning.
2552        let p = parse("persisted>=1").unwrap();
2553        let (status, blocker) = evaluate_gate(&p, 501, WireSeq::new(500), None, None);
2554        assert!(!status.degraded);
2555        assert_eq!(status.durable_pos, 500);
2556        assert_eq!(blocker, None);
2557    }
2558
2559    #[test]
2560    fn attribution_skips_disconnected_slots() {
2561        // Slot 1 is disconnected, so its (very advanced) cursors must
2562        // not satisfy `in_memory>=2`; only slot 0 counts, and it lags.
2563        // The gate opens at slot 0's in-memory cursor (100).
2564        let p = parse("persisted>=1 && in_memory>=2").unwrap();
2565        let m = metrics((100, 100), (999, 999));
2566        let a = flags(true, false);
2567        assert_eq!(
2568            evaluate_gate(&p, 100, WireSeq::new(500), Some(&m), Some(&a)).1,
2569            Some(Blocker::Replication)
2570        );
2571    }
2572
2573    /// Fresh-cluster catch-up: a replica that handshakes at sequence
2574    /// 0 (the legitimate genesis case, not a stale-flag race) must be
2575    /// included in the cursor view with its zero cursors so the policy
2576    /// behaves the same way it would for a 1-replica deployment that
2577    /// has just produced its first batch. The disconnect-race
2578    /// mitigations (B1 seed-on-connect + B2 reorder) keep this from
2579    /// being conflated with the stale-flag-paired-with-zero-cursor
2580    /// case under normal cluster lifecycles.
2581    #[test]
2582    fn fresh_cluster_zero_cursors_included_in_view() {
2583        let p = parse("persisted>=2").unwrap();
2584        // Both replicas just handshook at seq 0, primary also at 0
2585        // (fresh cluster, no events yet). View = 3 nodes; the clause's
2586        // count (=2) is met by the view size, so the policy is not
2587        // degraded and the gate sits at the 2nd-largest persisted = 0.
2588        let m = metrics((0, 0), (0, 0));
2589        let a = both_active();
2590        let r = evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a));
2591        assert_eq!(r.durable_pos, 0);
2592        assert!(
2593            !r.degraded,
2594            "all 3 nodes present, view meets clause target — should not flag degraded"
2595        );
2596    }
2597
2598    // -- Race-window regression tests --
2599    //
2600    // The replication senders fix two memory-ordering issues at the
2601    // active-flag transition points:
2602    //
2603    //   B1 (`a84540a`): seed `metrics.{acked,in_memory}_sequence[i]`
2604    //   to `handshake.last_sequence` BEFORE setting active_flag=true
2605    //   on reconnect. Without this, the gate would observe (active=
2606    //   true, cursor=0) for ~1 RTT after a replica catch-up completed,
2607    //   pinning any multi-node clause — and thus the gate — to 0.
2608    //
2609    //   B2 (`8888732`): zero `metrics.{acked,in_memory}_sequence[i]`
2610    //   BEFORE setting active_flag=false on disconnect. Without this,
2611    //   a weak-memory reader could observe (active=true, cursor=0)
2612    //   for one iteration during the disconnect window.
2613    //
2614    // Both fixes are in the senders, but the gate's *behaviour* under
2615    // the race-window inputs is tested here. The intent is to lock in
2616    // the invariant: even under a hypothetical (active=true,cursor=0)
2617    // observation, the gate must not produce a spuriously-open answer
2618    // that would cause a client to be told "your event is durable"
2619    // when it isn't. Stalling-briefly is safe; opening-spuriously is
2620    // not.
2621
2622    #[test]
2623    fn race_b1_post_seed_gate_doesnt_freeze_on_reconnect() {
2624        // Post-B1-fix state: replica reconnected, cursors seeded to
2625        // `handshake.last_sequence` (480) before active flipped to
2626        // true. Primary kept moving and is at 500. The gate's view
2627        // is now [primary=500, slot=480]; the durable position dips
2628        // from 500 (primary alone, degraded) to 480 (both nodes).
2629        //
2630        // The dip is correct, not a bug: once a 2nd node is
2631        // connected, durability is bounded by the slower of the two.
2632        // Events 481-500 were already served as durable on primary
2633        // alone — they aren't unsent. New responses for seq>500 wait
2634        // until slot acks; we just don't freeze at 0.
2635        let p = parse("persisted>=2").unwrap();
2636        let m = metrics((480, 480), (999, 999));
2637        let a = flags(true, false);
2638        let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
2639        assert_eq!(
2640            r.durable_pos, 480,
2641            "post-seed reconnect should produce a coherent gate position equal to the slower node, not freeze at 0"
2642        );
2643    }
2644
2645    #[test]
2646    fn race_b1_pre_seed_freeze_is_what_the_fix_avoids() {
2647        // Pre-B1-fix state: cursors at 0, active=true. The gate sees
2648        // [primary=500, slot=[0,0]] and 2nd-largest persisted = 0.
2649        // The gate WOULD freeze at 0. This test documents the bug
2650        // the seeding fix is designed to avoid; the senders ensure
2651        // this state is never observed in production.
2652        let p = parse("persisted>=2").unwrap();
2653        let m = metrics((0, 0), (999, 999));
2654        let a = flags(true, false);
2655        let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
2656        assert_eq!(
2657            r.durable_pos, 0,
2658            "the gate behaviour under (active=true, cursor=0) — if the senders ever fail to seed before flipping active, this is the freeze the operator would see"
2659        );
2660    }
2661
2662    #[test]
2663    fn race_b2_disconnect_window_doesnt_open_gate_spuriously() {
2664        // Simulates the B2 race window: a weak-memory reader observes
2665        // (active=true, cursor=0) for one iteration during the
2666        // disconnect transition. The slot legitimately has cursor=0
2667        // because the disconnect handler just zeroed the metrics.
2668        //
2669        // Critical invariant: the gate must NOT produce a higher
2670        // durable_pos than it would with the slot correctly excluded.
2671        // Specifically: with primary at 500, slot stale-zero-included,
2672        // the gate must not "see" the primary alone and open at 500
2673        // — that would let a client be told a seq is durable when
2674        // only the primary has it under a `persisted>=2` policy that
2675        // demands 2 nodes.
2676        let p = parse("persisted>=2").unwrap();
2677        let m = metrics((0, 0), (999, 999));
2678        let a = flags(true, false);
2679        let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
2680        // 2nd-largest persisted across {primary=500, slot=0} = 0.
2681        // Gate stalls. ✓
2682        assert_eq!(r.durable_pos, 0);
2683
2684        // Post-disconnect (active=false): view shrinks to {primary}.
2685        // `persisted>=2` is structurally unsatisfiable on a 1-node
2686        // view, so the gate stays at 0 AND surfaces degraded. The
2687        // readers' `replicas_connected==0` halt is what stops
2688        // accepting new writes; the gate side's job is just to keep
2689        // the existing in-flight writes stalled and the alert lit.
2690        let a_disconnected = flags(false, false);
2691        let r_after = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a_disconnected));
2692        assert_eq!(r_after.durable_pos, 0);
2693        assert!(
2694            r_after.degraded,
2695            "post-disconnect view of size 1 cannot meet persisted>=2 → degraded"
2696        );
2697    }
2698
2699    #[test]
2700    fn race_invariant_zero_cursor_never_opens_gate_above_slower_node() {
2701        // Property under both B1 and B2 race windows: for any slot
2702        // observed at cursor=0 with active=true, the gate cannot
2703        // produce a durable_pos that exceeds what an honest 2-node
2704        // evaluation would give. Spot-check a handful of primary
2705        // positions to lock the invariant.
2706        let p = parse("persisted>=2").unwrap();
2707        let m = metrics((0, 0), (999, 999));
2708        let a = flags(true, false);
2709        for primary_pos in [0, 1, 100, 500, 1_000_000_000_u64] {
2710            let r = evaluate_durability(&p, WireSeq::new(primary_pos), Some(&m), Some(&a));
2711            // 2nd-largest of {primary_pos, 0} = 0 for any primary > 0.
2712            // For primary_pos = 0, also 0. So always 0.
2713            assert_eq!(
2714                r.durable_pos, 0,
2715                "race-window observation must not open the gate above 0 for any primary position (got {} for primary_pos={primary_pos})",
2716                r.durable_pos
2717            );
2718        }
2719    }
2720
2721    // ------------------------------------------------------------------
2722    // GateCrossTracker — per-cursor "first transition from below to
2723    // crossed" inside the gate loop, used by the journal-wait /
2724    // replica-wait histograms.
2725    // ------------------------------------------------------------------
2726
2727    #[cfg(feature = "tick-to-trade")]
2728    #[test]
2729    fn gate_cross_tracker_records_journal_when_strictly_below() {
2730        // Journal starts at 5 (< 10), repl_min already at 100.
2731        // Journal crosses on the second observation. Replica was already
2732        // past at entry, so no replica sample.
2733        let mut t = GateCrossTracker::new(10);
2734        t.observe(5, Some(100), 1_000);
2735        t.observe(15, Some(100), 2_000);
2736        assert_eq!(t.journal_crossed(), Some(2_000));
2737        assert_eq!(t.replica_crossed(), None);
2738    }
2739
2740    #[cfg(feature = "tick-to-trade")]
2741    #[test]
2742    fn gate_cross_tracker_records_replica_when_strictly_below() {
2743        // Mirror image: journal already past, replica below at entry.
2744        let mut t = GateCrossTracker::new(10);
2745        t.observe(50, Some(5), 1_000);
2746        t.observe(50, Some(12), 2_000);
2747        assert_eq!(t.journal_crossed(), None);
2748        assert_eq!(t.replica_crossed(), Some(2_000));
2749    }
2750
2751    #[cfg(feature = "tick-to-trade")]
2752    #[test]
2753    fn gate_cross_tracker_records_both_when_both_below() {
2754        // Both below at entry, both cross independently.
2755        let mut t = GateCrossTracker::new(100);
2756        t.observe(50, Some(60), 1_000); // both below
2757        t.observe(105, Some(60), 2_000); // journal crosses
2758        t.observe(105, Some(110), 3_000); // replica crosses
2759        assert_eq!(t.journal_crossed(), Some(2_000));
2760        assert_eq!(t.replica_crossed(), Some(3_000));
2761    }
2762
2763    #[cfg(feature = "tick-to-trade")]
2764    #[test]
2765    fn gate_cross_tracker_skips_cursor_already_past_at_entry() {
2766        // Both cursors already >= needed at first observation —
2767        // neither was on the critical path. No samples.
2768        let mut t = GateCrossTracker::new(10);
2769        t.observe(50, Some(100), 1_000);
2770        // Even later observations don't backfill: was_below is sticky.
2771        t.observe(60, Some(110), 2_000);
2772        assert_eq!(t.journal_crossed(), None);
2773        assert_eq!(t.replica_crossed(), None);
2774    }
2775
2776    #[cfg(feature = "tick-to-trade")]
2777    #[test]
2778    fn gate_cross_tracker_first_observation_only_for_cross_decision() {
2779        // A cursor that goes back below `needed` after first iteration
2780        // (impossible in practice — cursors are monotonic — but we
2781        // verify the first-iteration snapshot is what gates the
2782        // sample). Journal: 50 < 10 false → was_below=false → no sample.
2783        let mut t = GateCrossTracker::new(10);
2784        t.observe(50, Some(5), 1_000); // journal already past, replica below
2785        t.observe(20, Some(12), 2_000); // both >= needed now
2786        // Journal: was_below=false at entry → still no sample.
2787        assert_eq!(t.journal_crossed(), None);
2788        // Replica: was_below=true at entry, crosses on iter 2 → sample.
2789        assert_eq!(t.replica_crossed(), Some(2_000));
2790    }
2791
2792    #[cfg(feature = "tick-to-trade")]
2793    #[test]
2794    fn gate_cross_tracker_holds_first_cross_only() {
2795        // Once a cross is recorded, later observations don't
2796        // overwrite — the metric is "when did it first cross", not
2797        // "when was it last below".
2798        let mut t = GateCrossTracker::new(10);
2799        t.observe(5, Some(100), 1_000);
2800        t.observe(15, Some(100), 2_000); // first cross
2801        t.observe(25, Some(100), 3_000); // would otherwise re-record
2802        assert_eq!(t.journal_crossed(), Some(2_000));
2803    }
2804
2805    #[cfg(feature = "tick-to-trade")]
2806    #[test]
2807    fn gate_cross_tracker_ignores_replica_dropping_out_mid_wait() {
2808        // The regression this signature exists for. The replica is
2809        // behind at entry (was_below latches true), then disconnects
2810        // part-way through the wait so no replica supplies a binding
2811        // cursor. That must not be read as "caught up" — the histogram
2812        // would otherwise report a wait that ended at the disconnect.
2813        let mut t = GateCrossTracker::new(100);
2814        t.observe(50, Some(60), 1_000); // replica behind
2815        t.observe(105, None, 2_000); // replica drops out mid-wait
2816        t.observe(105, None, 3_000); // still gone
2817        assert_eq!(t.journal_crossed(), Some(2_000));
2818        assert_eq!(t.replica_crossed(), None);
2819        // Reconnecting past `needed` still records the real crossing.
2820        t.observe(105, Some(150), 4_000);
2821        assert_eq!(t.replica_crossed(), Some(4_000));
2822    }
2823
2824    #[cfg(feature = "tick-to-trade")]
2825    #[test]
2826    fn gate_cross_tracker_records_nothing_when_no_replica_clause() {
2827        // `local` never binds on a replica, so `policy_replica_cursor`
2828        // returns `None` for the whole wait: was_below stays false and
2829        // no replica sample is produced.
2830        let mut t = GateCrossTracker::new(100);
2831        t.observe(50, None, 1_000);
2832        t.observe(105, None, 2_000);
2833        assert_eq!(t.journal_crossed(), Some(2_000));
2834        assert_eq!(t.replica_crossed(), None);
2835    }
2836
2837    // -- DegradationLogger flap-suppression --
2838    //
2839    // The logger gates transition logs on a sustained-state hold so a
2840    // replica flapping at sub-second cadence doesn't spam the journal.
2841    // These tests don't observe the logs themselves (tracing is
2842    // process-global and brittle to capture in unit tests); they
2843    // assert the underlying state machine via the `policy_degraded`
2844    // gauge, which the logger updates on every tick regardless of
2845    // log emission.
2846
2847    fn logger_test_policy() -> crate::ack_policy::Policy {
2848        parse("persisted>=2").unwrap()
2849    }
2850
2851    /// Tick the logger N times at `step` intervals, alternating
2852    /// `degraded` per call. Returns the gauge value after the last
2853    /// tick — useful for asserting that flap cycles don't leak the
2854    /// AtomicBool into a wrong terminal state.
2855    fn drive_logger(
2856        logger: &mut DegradationLogger,
2857        utilization: &StageUtilization,
2858        policy: &crate::ack_policy::Policy,
2859        start: Instant,
2860        states: &[bool],
2861        step: Duration,
2862    ) -> bool {
2863        for (i, &state) in states.iter().enumerate() {
2864            let now = start + step.checked_mul(i as u32).unwrap_or(Duration::ZERO);
2865            logger.tick(policy, utilization, state, now, Duration::from_secs(5));
2866        }
2867        utilization.policy_degraded.load(Ordering::Relaxed)
2868    }
2869
2870    #[test]
2871    fn logger_gauge_tracks_state_immediately() {
2872        // The /healthz gauge reflects the *latest* state on every
2873        // tick — this is what dashboards / alerts read. Sustained-
2874        // state gating only affects the warn/info log emission.
2875        let p = logger_test_policy();
2876        let utilization = StageUtilization::new();
2877        let now = Instant::now();
2878        let mut logger = DegradationLogger::new(now);
2879
2880        logger.tick(&p, &utilization, true, now, Duration::from_secs(5));
2881        assert!(utilization.policy_degraded.load(Ordering::Relaxed));
2882
2883        logger.tick(
2884            &p,
2885            &utilization,
2886            false,
2887            now + Duration::from_millis(50),
2888            Duration::from_secs(5),
2889        );
2890        assert!(!utilization.policy_degraded.load(Ordering::Relaxed));
2891    }
2892
2893    #[test]
2894    fn logger_starting_degraded_marks_initial_state_logged() {
2895        // `new_starting_degraded` emits the startup warn and treats
2896        // the state as already-logged so the next tick at the same
2897        // cluster shape doesn't re-emit instantly. The gauge starts
2898        // at 1.
2899        let p = logger_test_policy();
2900        let utilization = StageUtilization::new();
2901        let now = Instant::now();
2902        let mut logger = DegradationLogger::new_starting_degraded(now, &p);
2903        // The logger doesn't write the gauge from the constructor —
2904        // first tick does. Tick at the same state to settle the
2905        // gauge. No new log line should fire (state hasn't changed).
2906        logger.tick(
2907            &p,
2908            &utilization,
2909            true,
2910            now + Duration::from_millis(10),
2911            Duration::from_secs(5),
2912        );
2913        assert!(utilization.policy_degraded.load(Ordering::Relaxed));
2914    }
2915
2916    #[test]
2917    fn logger_accumulates_degraded_seconds_only_while_degraded() {
2918        // The `policy_degraded_nanos` counter must advance by exactly
2919        // the wall-clock time spent in the degraded state and stay flat
2920        // while healthy. Each tick attributes the interval since the
2921        // previous tick to the state observed at that previous tick.
2922        let p = logger_test_policy();
2923        let utilization = StageUtilization::new();
2924        let start = Instant::now();
2925        let mut logger = DegradationLogger::new(start);
2926        let sec = Duration::from_secs(1);
2927
2928        // t0: healthy -> degraded. The zero-length interval before the
2929        // transition is attributed to the healthy start: no accrual.
2930        logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
2931        assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
2932
2933        // t0..t1 spent degraded -> +1s.
2934        logger.tick(&p, &utilization, true, start + sec, Duration::from_secs(5));
2935        // t1..t2 still degraded, then flips healthy at the tick -> +1s.
2936        logger.tick(
2937            &p,
2938            &utilization,
2939            false,
2940            start + 2 * sec,
2941            Duration::from_secs(5),
2942        );
2943        assert_eq!(
2944            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2945            2 * sec.as_nanos() as u64
2946        );
2947
2948        // t2..t3 spent healthy -> counter holds flat.
2949        logger.tick(
2950            &p,
2951            &utilization,
2952            false,
2953            start + 3 * sec,
2954            Duration::from_secs(5),
2955        );
2956        assert_eq!(
2957            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2958            2 * sec.as_nanos() as u64
2959        );
2960    }
2961
2962    #[test]
2963    fn logger_starting_degraded_accrues_from_construction() {
2964        // A primary that boots already-degraded (e.g. `disk+ram` with no
2965        // replica yet) must accrue from construction, not from the first
2966        // observed transition — `new_starting_degraded` seeds the
2967        // degraded state so the first tick's interval counts.
2968        let p = logger_test_policy();
2969        let utilization = StageUtilization::new();
2970        let start = Instant::now();
2971        let mut logger = DegradationLogger::new_starting_degraded(start, &p);
2972
2973        logger.tick(
2974            &p,
2975            &utilization,
2976            true,
2977            start + Duration::from_secs(1),
2978            Duration::from_secs(5),
2979        );
2980        assert_eq!(
2981            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2982            Duration::from_secs(1).as_nanos() as u64
2983        );
2984    }
2985
2986    #[test]
2987    fn logger_reseed_flushes_in_flight_degraded_accrual() {
2988        // A runtime policy swap re-seeds the logger. On the gate-wait path
2989        // the swap can land after many seconds of wedged-degraded time
2990        // with no intervening tick, so `reseed` must flush that interval
2991        // before resetting — otherwise the whole wedge is silently lost,
2992        // which is exactly the incident the metric exists to measure.
2993        let p = logger_test_policy();
2994        let utilization = StageUtilization::new();
2995        let start = Instant::now();
2996        let sec = Duration::from_secs(1);
2997
2998        let mut logger = DegradationLogger::new_starting_degraded(start, &p);
2999        logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
3000        assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
3001
3002        // 30s wedged-degraded with no tick (frozen spin), then swap.
3003        logger.reseed(&utilization, start + 30 * sec);
3004        assert_eq!(
3005            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
3006            30 * sec.as_nanos() as u64
3007        );
3008
3009        // Post-reseed the logger is healthy-start; a healthy interval
3010        // does not accrue, and `last_tick` was rebased to the swap so
3011        // the next interval isn't double-counted.
3012        logger.tick(
3013            &p,
3014            &utilization,
3015            false,
3016            start + 31 * sec,
3017            Duration::from_secs(5),
3018        );
3019        assert_eq!(
3020            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
3021            30 * sec.as_nanos() as u64
3022        );
3023    }
3024
3025    #[test]
3026    fn logger_starting_degraded_then_first_tick_healthy_counts_construction_interval() {
3027        // `new_starting_degraded` seeds the degraded state, so the
3028        // construction→first-tick interval is attributed degraded even
3029        // when the first observed state is already healthy (the cluster
3030        // recovered before the first tick). Locks this intentional edge.
3031        let p = logger_test_policy();
3032        let utilization = StageUtilization::new();
3033        let start = Instant::now();
3034        let mut logger = DegradationLogger::new_starting_degraded(start, &p);
3035
3036        logger.tick(
3037            &p,
3038            &utilization,
3039            false,
3040            start + Duration::from_secs(2),
3041            Duration::from_secs(5),
3042        );
3043        assert_eq!(
3044            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
3045            Duration::from_secs(2).as_nanos() as u64
3046        );
3047    }
3048
3049    #[test]
3050    fn logger_counter_matches_total_degraded_time_across_flaps() {
3051        // The cumulative counter must equal exactly the wall time spent
3052        // in degraded states across a flap sequence — the guarantee
3053        // `rate(...degraded_seconds_total)` dashboards depend on.
3054        let p = logger_test_policy();
3055        let utilization = StageUtilization::new();
3056        let start = Instant::now();
3057        let step = Duration::from_millis(500);
3058        let mut logger = DegradationLogger::new(start);
3059
3060        // State observed at tick i (fired at start + i*step). The state
3061        // set at tick i holds over [tick i, tick i+1], so degraded wall
3062        // time = the spans where the *prior* tick's state was true:
3063        // [t1,t2], [t2,t3], [t4,t5] = 3 * step.
3064        let states = [false, true, true, false, true, false];
3065        for (i, &s) in states.iter().enumerate() {
3066            logger.tick(
3067                &p,
3068                &utilization,
3069                s,
3070                start + step * i as u32,
3071                Duration::from_secs(5),
3072            );
3073        }
3074
3075        assert_eq!(
3076            utilization.policy_degraded_nanos.load(Ordering::Relaxed),
3077            (3 * step).as_nanos() as u64
3078        );
3079    }
3080
3081    #[test]
3082    fn logger_handles_rapid_flap_without_panic() {
3083        // Drive the logger through 100 alternating flips at 100ms
3084        // each (faster than the 1s flap-hold). The state machine
3085        // must remain coherent — no panics, gauge tracks final
3086        // state, and `pending_logged` doesn't get stuck.
3087        let p = logger_test_policy();
3088        let utilization = StageUtilization::new();
3089        let now = Instant::now();
3090        let mut logger = DegradationLogger::new(now);
3091        let states: Vec<bool> = (0..100u32).map(|i| i.is_multiple_of(2)).collect();
3092        let final_state = drive_logger(
3093            &mut logger,
3094            &utilization,
3095            &p,
3096            now,
3097            &states,
3098            Duration::from_millis(100),
3099        );
3100        // 100 states starting at i=0 → final state is i=99 → odd → false.
3101        assert!(!final_state);
3102    }
3103
3104    #[test]
3105    fn logger_sustained_degraded_eventually_settles() {
3106        // After a sustained-true state, the logger should be in the
3107        // "logged the onset" mode. Drive 5 ticks of degraded=true at
3108        // 500ms intervals — total 2s, well past the 1s flap-hold.
3109        // Last tick should leave gauge=1 and the heartbeat re-emit
3110        // window primed (last_log set).
3111        let p = logger_test_policy();
3112        let utilization = StageUtilization::new();
3113        let now = Instant::now();
3114        let mut logger = DegradationLogger::new(now);
3115        let final_state = drive_logger(
3116            &mut logger,
3117            &utilization,
3118            &p,
3119            now,
3120            &[true; 5],
3121            Duration::from_millis(500),
3122        );
3123        assert!(final_state);
3124    }
3125
3126    #[test]
3127    fn logger_recovery_to_healthy_settles_gauge() {
3128        // Sustained degraded → sustained healthy. Gauge should end at 0.
3129        let p = logger_test_policy();
3130        let utilization = StageUtilization::new();
3131        let now = Instant::now();
3132        let mut logger = DegradationLogger::new_starting_degraded(now, &p);
3133        let mut states = vec![true; 5]; // 2.5s degraded
3134        states.extend(vec![false; 5]); // 2.5s healthy
3135        let final_state = drive_logger(
3136            &mut logger,
3137            &utilization,
3138            &p,
3139            now,
3140            &states,
3141            Duration::from_millis(500),
3142        );
3143        assert!(!final_state);
3144    }
3145
3146    /// Dirty tracking: an entry flag for dedup plus a flat list for
3147    /// iteration, replacing the per-append `HashSet` insert.
3148    mod dirty_tracking {
3149        use super::super::{mark_dirty, unmark_dirty};
3150        use super::flush_sends::entry_for;
3151        use std::os::unix::net::UnixStream;
3152
3153        fn entry() -> super::super::ConnectionEntry {
3154            let (tx, _rx) = UnixStream::pair().expect("socketpair");
3155            let mut e = entry_for(tx, b"payload");
3156            e.dirty = false;
3157            e
3158        }
3159
3160        #[test]
3161        fn repeat_appends_enqueue_a_connection_once() {
3162            let mut e = entry();
3163            let mut dirty = Vec::new();
3164            for _ in 0..8 {
3165                mark_dirty(&mut e, 42, &mut dirty);
3166            }
3167            assert_eq!(dirty, vec![42], "the flag is the dedup");
3168            assert!(e.dirty);
3169        }
3170
3171        #[test]
3172        fn teardown_takes_a_connection_off_the_list() {
3173            let mut a = entry();
3174            let mut b = entry();
3175            let mut dirty = Vec::new();
3176            mark_dirty(&mut a, 1, &mut dirty);
3177            mark_dirty(&mut b, 2, &mut dirty);
3178            unmark_dirty(1, &mut dirty);
3179            assert_eq!(dirty, vec![2], "order of the survivors is preserved");
3180        }
3181    }
3182
3183    /// One append per slot: the encoded payload plus the request's
3184    /// pre-encoded `BatchEnd` terminator, sharing a single cap check and
3185    /// a single dirty marking.
3186    mod append_frames {
3187        use super::super::{AppendOutcome, ConnectionEntry, MAX_SEND_BUF, append_frames};
3188        use super::flush_sends::entry_for;
3189        use std::os::unix::net::UnixStream;
3190        use std::time::Instant;
3191
3192        const TRAILER: &[u8] = &[0xEE, 0xEE];
3193
3194        fn entry(prefill: usize) -> ConnectionEntry {
3195            let (tx, _rx) = UnixStream::pair().expect("socketpair");
3196            let mut e = entry_for(tx, &vec![0u8; prefill]);
3197            e.dirty = false;
3198            e
3199        }
3200
3201        #[test]
3202        fn payload_and_terminator_land_in_one_append() {
3203            let mut e = entry(0);
3204            let mut dirty = Vec::new();
3205            let mut to_remove = Vec::new();
3206            let encoded = [1u8, 2, 3, 4, 0xFF];
3207
3208            let outcome = append_frames(
3209                Some(Ok(4)),
3210                TRAILER,
3211                7,
3212                &mut e,
3213                &encoded,
3214                Instant::now(),
3215                &mut dirty,
3216                &mut to_remove,
3217            );
3218
3219            assert!(matches!(outcome, AppendOutcome::Continue));
3220            assert_eq!(
3221                e.send_buf,
3222                vec![1, 2, 3, 4, 0xEE, 0xEE],
3223                "payload then terminator, nothing past the encoder's length"
3224            );
3225            assert_eq!(dirty, vec![7], "one dirty marking for the pair");
3226            assert!(to_remove.is_empty());
3227        }
3228
3229        #[test]
3230        fn a_slot_with_no_bytes_does_not_dirty_the_connection() {
3231            // `OutputPayload::BatchEnd` mid-request: no payload, no
3232            // terminator. Marking it dirty would queue an empty send.
3233            let mut e = entry(0);
3234            let mut dirty = Vec::new();
3235            let mut to_remove = Vec::new();
3236
3237            let outcome = append_frames(
3238                None,
3239                &[],
3240                7,
3241                &mut e,
3242                &[],
3243                Instant::now(),
3244                &mut dirty,
3245                &mut to_remove,
3246            );
3247
3248            assert!(matches!(outcome, AppendOutcome::Continue));
3249            assert!(e.send_buf.is_empty());
3250            assert!(dirty.is_empty());
3251            assert!(!e.dirty);
3252        }
3253
3254        #[test]
3255        fn the_cap_is_checked_against_both_frames_together() {
3256            // Payload alone fits; payload + terminator does not. The
3257            // connection must be dropped *before* the payload lands —
3258            // never delivered without its terminator.
3259            let mut e = entry(MAX_SEND_BUF - 5);
3260            let mut dirty = Vec::new();
3261            let mut to_remove = Vec::new();
3262            let encoded = [1u8, 2, 3, 4];
3263
3264            let outcome = append_frames(
3265                Some(Ok(4)),
3266                TRAILER,
3267                7,
3268                &mut e,
3269                &encoded,
3270                Instant::now(),
3271                &mut dirty,
3272                &mut to_remove,
3273            );
3274
3275            assert!(matches!(outcome, AppendOutcome::ConnectionDropped));
3276            assert_eq!(to_remove, vec![7]);
3277            assert_eq!(e.send_buf.len(), MAX_SEND_BUF - 5, "no partial append");
3278            assert!(dirty.is_empty(), "a dropped connection is not queued");
3279        }
3280
3281        #[test]
3282        fn an_encode_failure_still_terminates_the_request() {
3283            // The payload is this server's bug; the client still needs
3284            // the terminator or it waits forever for the batch to end.
3285            let mut e = entry(0);
3286            let mut dirty = Vec::new();
3287            let mut to_remove = Vec::new();
3288
3289            let outcome = append_frames(
3290                Some(Err("encode error")),
3291                TRAILER,
3292                7,
3293                &mut e,
3294                &[],
3295                Instant::now(),
3296                &mut dirty,
3297                &mut to_remove,
3298            );
3299
3300            assert!(matches!(outcome, AppendOutcome::Continue));
3301            assert_eq!(e.send_buf, TRAILER);
3302            assert_eq!(dirty, vec![7]);
3303        }
3304    }
3305
3306    /// The consumed path's two flush triggers. The byte threshold bounds
3307    /// a connection's buffered *bytes*; the slot interval bounds their
3308    /// *age*, which is what many-low-rate-client deployments need (2026-08
3309    /// network audit, finding 3).
3310    mod flush_cadence {
3311        use super::super::{FLUSH_BYTES_THRESHOLD, FLUSH_SLOT_INTERVAL, FlushCadence};
3312
3313        #[test]
3314        fn byte_threshold_fires_on_a_single_fat_connection() {
3315            let mut c = FlushCadence::default();
3316            c.on_append(FLUSH_BYTES_THRESHOLD - 1);
3317            assert!(!c.is_due());
3318            c.on_append(FLUSH_BYTES_THRESHOLD);
3319            assert!(c.is_due());
3320        }
3321
3322        #[test]
3323        fn slot_interval_bounds_the_age_of_tiny_buffers() {
3324            // Every connection stays far below an MSS — the byte
3325            // trigger never fires, so before the age bound these
3326            // responses sat in `send_buf` until the ring went idle.
3327            let mut c = FlushCadence::default();
3328            for _ in 0..FLUSH_SLOT_INTERVAL - 1 {
3329                c.on_append(40);
3330                assert!(!c.is_due());
3331            }
3332            c.on_append(40);
3333            assert!(
3334                c.is_due(),
3335                "age bound must fire on the {FLUSH_SLOT_INTERVAL}th slot"
3336            );
3337        }
3338
3339        #[test]
3340        fn flushing_restarts_both_triggers() {
3341            let mut c = FlushCadence::default();
3342            for _ in 0..FLUSH_SLOT_INTERVAL {
3343                c.on_append(FLUSH_BYTES_THRESHOLD);
3344            }
3345            assert!(c.is_due());
3346            c.on_flush();
3347            assert!(
3348                !c.is_due(),
3349                "a flush restarts the byte flag and the counter"
3350            );
3351            // And the age bound counts from the flush, not from start.
3352            for _ in 0..FLUSH_SLOT_INTERVAL - 1 {
3353                c.on_append(1);
3354            }
3355            assert!(!c.is_due());
3356            c.on_append(1);
3357            assert!(c.is_due());
3358        }
3359    }
3360
3361    /// Regression tests for the slow-client head-of-line block fixed in
3362    /// the 2026-08 io_uring audit (docs/internal/io-uring-audit-2026-08.md,
3363    /// finding 1): `flush_sends` used to `submit_and_wait` on SENDs
3364    /// without `MSG_DONTWAIT`, and io_uring holds such a SEND's CQE back
3365    /// until the peer reads — so one zero-window client stalled every
3366    /// client's acks. These use real sockets and a real ring: the defect
3367    /// was kernel-level behaviour a mock would not reproduce.
3368    mod heartbeat {
3369        use super::super::{MAX_SEND_BUF, heartbeat_due};
3370        use super::flush_sends::entry_for;
3371        use std::os::unix::net::UnixStream;
3372        use std::time::{Duration, Instant};
3373
3374        const INTERVAL: Duration = Duration::from_secs(1);
3375        const FRAME_LEN: usize = 13;
3376
3377        /// Backdate `last_send` a full interval so only the receive-
3378        /// ability guards decide. `checked_sub` because a young
3379        /// monotonic clock can't be backdated (same pattern as the
3380        /// flush_sends tests).
3381        fn idle_entry(payload: &[u8]) -> Option<super::super::ConnectionEntry> {
3382            let (tx, _rx) = UnixStream::pair().unwrap();
3383            let mut entry = entry_for(tx, payload);
3384            entry.last_send = Instant::now().checked_sub(INTERVAL * 2)?;
3385            Some(entry)
3386        }
3387
3388        #[test]
3389        fn idle_unblocked_connection_gets_a_heartbeat() {
3390            let Some(entry) = idle_entry(&[]) else {
3391                eprintln!("monotonic clock too young to backdate; skipping");
3392                return;
3393            };
3394            assert!(heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
3395        }
3396
3397        #[test]
3398        fn recently_active_connection_is_skipped() {
3399            let (tx, _rx) = UnixStream::pair().unwrap();
3400            let entry = entry_for(tx, &[]);
3401            assert!(
3402                !heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN),
3403                "a connection inside the interval must not be pinged"
3404            );
3405        }
3406
3407        /// A blocked peer's socket is full — the frame would only grow
3408        /// `send_buf`. This is one half of the fix for the unbounded
3409        /// heartbeat growth on trickle-reading clients.
3410        #[test]
3411        fn blocked_connection_is_skipped() {
3412            let Some(mut entry) = idle_entry(&[]) else {
3413                eprintln!("monotonic clock too young to backdate; skipping");
3414                return;
3415            };
3416            entry.blocked_since = Some(Instant::now());
3417            assert!(!heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
3418        }
3419
3420        /// The other half: heartbeats respect `MAX_SEND_BUF` like every
3421        /// other append — this was the one path that bypassed the cap.
3422        #[test]
3423        fn append_never_exceeds_the_send_buffer_cap() {
3424            let payload = vec![0u8; MAX_SEND_BUF - FRAME_LEN + 1];
3425            let Some(entry) = idle_entry(&payload) else {
3426                eprintln!("monotonic clock too young to backdate; skipping");
3427                return;
3428            };
3429            assert!(!heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
3430
3431            let payload = vec![0u8; MAX_SEND_BUF - FRAME_LEN];
3432            let Some(entry) = idle_entry(&payload) else {
3433                eprintln!("monotonic clock too young to backdate; skipping");
3434                return;
3435            };
3436            assert!(
3437                heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN),
3438                "a frame that exactly fits is allowed"
3439            );
3440        }
3441    }
3442
3443    mod flush_sends {
3444        use super::super::{
3445            BLOCKED_RETRY_INTERVAL, BLOCKED_SEND_TIMEOUT, ConnectionEntry, flush_sends,
3446        };
3447        use io_uring::IoUring;
3448        use rustc_hash::FxHashMap;
3449        use std::io::Read as _;
3450        use std::os::unix::io::{AsRawFd, RawFd};
3451        use std::os::unix::net::UnixStream;
3452        use std::time::{Duration, Instant};
3453
3454        pub(super) fn entry_for(stream: UnixStream, payload: &[u8]) -> ConnectionEntry {
3455            ConnectionEntry {
3456                fd: stream.as_raw_fd(),
3457                _owner: Box::new(stream),
3458                send_buf: payload.to_vec(),
3459                last_send: Instant::now(),
3460                blocked_since: None,
3461                last_send_attempt: Instant::now(),
3462                // Every entry these tests build is handed to `flush_sends`
3463                // on the dirty list, so the flag has to agree with it.
3464                dirty: true,
3465            }
3466        }
3467
3468        /// Fill `fd`'s socket send buffer with non-blocking writes until
3469        /// the kernel refuses more — the state a zero-window / non-reading
3470        /// client leaves a connection in.
3471        pub(super) fn fill_socket(fd: RawFd) {
3472            let junk = [0u8; 4096];
3473            loop {
3474                let n =
3475                    unsafe { libc::send(fd, junk.as_ptr().cast(), junk.len(), libc::MSG_DONTWAIT) };
3476                if n < 0 {
3477                    let err = std::io::Error::last_os_error();
3478                    assert_eq!(
3479                        err.raw_os_error(),
3480                        Some(libc::EAGAIN),
3481                        "unexpected errno while filling socket"
3482                    );
3483                    break;
3484                }
3485            }
3486        }
3487
3488        /// The defect this module exists to prevent: a peer that stops
3489        /// reading must cost the flush nothing, and healthy peers on the
3490        /// same flush must still be delivered. Against the pre-fix code
3491        /// this test never returns.
3492        #[test]
3493        fn slow_peer_does_not_block_flush_and_healthy_peer_is_delivered() {
3494            let mut ring = IoUring::new(64).unwrap();
3495            let (slow_tx, _slow_rx) = UnixStream::pair().unwrap();
3496            let (fast_tx, mut fast_rx) = UnixStream::pair().unwrap();
3497            fill_socket(slow_tx.as_raw_fd());
3498
3499            let mut connections = FxHashMap::default();
3500            connections.insert(1u64, entry_for(slow_tx, &[0xAA; 1024]));
3501            connections.insert(2u64, entry_for(fast_tx, b"hello"));
3502            let mut dirty: Vec<u64> = vec![1, 2];
3503            let mut to_remove = Vec::new();
3504            let mut cqes = Vec::new();
3505
3506            let start = Instant::now();
3507            flush_sends(
3508                &mut ring,
3509                &mut connections,
3510                &mut dirty,
3511                &mut to_remove,
3512                &mut cqes,
3513            );
3514            // Generous bound to stay unflaky under CI load — the fixed
3515            // path completes in microseconds, the broken one only when
3516            // the slow peer reads (here: never).
3517            assert!(
3518                start.elapsed() < Duration::from_secs(1),
3519                "flush blocked on a slow peer"
3520            );
3521
3522            assert!(to_remove.is_empty(), "nobody dropped on first refusal");
3523            let slow = &connections[&1];
3524            assert!(
3525                !slow.send_buf.is_empty(),
3526                "slow peer keeps its undelivered bytes"
3527            );
3528            assert!(slow.blocked_since.is_some(), "slow peer marked blocked");
3529            assert!(dirty.contains(&1), "slow peer stays dirty for retry");
3530            assert!(slow.dirty, "and its flag agrees with the list");
3531
3532            assert!(connections[&2].send_buf.is_empty(), "healthy peer drained");
3533            assert!(!dirty.contains(&2), "healthy peer leaves the dirty set");
3534            // Flag and list must clear together: a stale `true` would
3535            // keep the connection off the list forever, since `mark_dirty`
3536            // only pushes when the flag is down.
3537            assert!(!connections[&2].dirty, "and its flag clears with it");
3538            fast_rx
3539                .set_read_timeout(Some(Duration::from_secs(1)))
3540                .unwrap();
3541            let mut buf = [0u8; 5];
3542            fast_rx.read_exact(&mut buf).unwrap();
3543            assert_eq!(&buf, b"hello", "healthy peer's bytes reached the socket");
3544        }
3545
3546        /// A payload larger than the socket buffer must be partially
3547        /// delivered, the remainder kept, and the connection treated as
3548        /// blocked (an immediate retry would only report EAGAIN).
3549        #[test]
3550        fn partial_send_keeps_remainder_and_marks_blocked() {
3551            let mut ring = IoUring::new(64).unwrap();
3552            let (tx, _rx) = UnixStream::pair().unwrap();
3553            // Shrink the send buffer so the payload cannot fit in one go.
3554            let sz: libc::c_int = 8192;
3555            let rc = unsafe {
3556                libc::setsockopt(
3557                    tx.as_raw_fd(),
3558                    libc::SOL_SOCKET,
3559                    libc::SO_SNDBUF,
3560                    (&raw const sz).cast(),
3561                    size_of::<libc::c_int>() as libc::socklen_t,
3562                )
3563            };
3564            assert_eq!(rc, 0, "setsockopt(SO_SNDBUF) failed");
3565            let payload = vec![0x55u8; 512 * 1024];
3566
3567            // Enter the flush as an already-blocked peer with a stale
3568            // (but unexpired) block timestamp and an expired pacing
3569            // window, so the partial delivery below must *restart* the
3570            // blocked clock — the behaviour that keeps a slow-but-
3571            // draining client alive past BLOCKED_SEND_TIMEOUT.
3572            let Some(stale_block) = Instant::now().checked_sub(BLOCKED_SEND_TIMEOUT / 2) else {
3573                eprintln!("monotonic clock too young to backdate; skipping");
3574                return;
3575            };
3576            let mut entry = entry_for(tx, &payload);
3577            entry.blocked_since = Some(stale_block);
3578            entry.last_send_attempt = stale_block;
3579
3580            let mut connections = FxHashMap::default();
3581            connections.insert(1u64, entry);
3582            let mut dirty: Vec<u64> = vec![1];
3583            let mut to_remove = Vec::new();
3584            let mut cqes = Vec::new();
3585
3586            flush_sends(
3587                &mut ring,
3588                &mut connections,
3589                &mut dirty,
3590                &mut to_remove,
3591                &mut cqes,
3592            );
3593
3594            let entry = &connections[&1];
3595            assert!(entry.send_buf.len() < payload.len(), "some bytes were sent");
3596            assert!(!entry.send_buf.is_empty(), "remainder kept for retry");
3597            let refreshed = entry.blocked_since.expect("partial send marks blocked");
3598            assert!(
3599                refreshed > stale_block,
3600                "forward progress restarts the blocked clock"
3601            );
3602            assert!(dirty.contains(&1));
3603            assert!(to_remove.is_empty());
3604        }
3605
3606        /// A connection blocked past `BLOCKED_SEND_TIMEOUT` is dropped —
3607        /// the guard for clients that stop reading during quiet periods,
3608        /// where `MAX_SEND_BUF` never trips because nothing accumulates.
3609        #[test]
3610        fn blocked_past_timeout_is_dropped() {
3611            let mut ring = IoUring::new(64).unwrap();
3612            let (tx, _rx) = UnixStream::pair().unwrap();
3613            fill_socket(tx.as_raw_fd());
3614
3615            let mut entry = entry_for(tx, &[1u8; 64]);
3616            // `checked_sub`: a plain `-` panics when the monotonic clock
3617            // is younger than the timeout (fresh-boot CI microVMs).
3618            let Some(expired) = Instant::now().checked_sub(BLOCKED_SEND_TIMEOUT) else {
3619                eprintln!("monotonic clock too young to backdate; skipping");
3620                return;
3621            };
3622            entry.blocked_since = Some(expired);
3623            let mut connections = FxHashMap::default();
3624            connections.insert(7u64, entry);
3625            let mut dirty: Vec<u64> = vec![7];
3626            let mut to_remove = Vec::new();
3627            let mut cqes = Vec::new();
3628
3629            flush_sends(
3630                &mut ring,
3631                &mut connections,
3632                &mut dirty,
3633                &mut to_remove,
3634                &mut cqes,
3635            );
3636
3637            assert_eq!(to_remove, vec![7], "timed-out peer queued for removal");
3638            assert!(!dirty.contains(&7), "dropped peer leaves the dirty set");
3639        }
3640
3641        /// A blocked connection retried a moment ago is skipped — the
3642        /// pacing that keeps a busy-spinning stage from probing a full
3643        /// socket once per loop iteration.
3644        #[test]
3645        fn blocked_retry_is_paced() {
3646            let mut ring = IoUring::new(64).unwrap();
3647            let (tx, _rx) = UnixStream::pair().unwrap();
3648            fill_socket(tx.as_raw_fd());
3649
3650            let mut entry = entry_for(tx, &[1u8; 64]);
3651            entry.blocked_since = Some(Instant::now());
3652            // A future attempt timestamp makes the pacing skip
3653            // deterministic: `duration_since` saturates to zero, so no
3654            // CI-load preemption between here and the flush's own clock
3655            // read can open the 100 µs window and flake the assertion.
3656            entry.last_send_attempt = Instant::now() + Duration::from_secs(1);
3657            let attempted_at = entry.last_send_attempt;
3658            let mut connections = FxHashMap::default();
3659            connections.insert(3u64, entry);
3660            let mut dirty: Vec<u64> = vec![3];
3661            let mut to_remove = Vec::new();
3662            let mut cqes = Vec::new();
3663
3664            flush_sends(
3665                &mut ring,
3666                &mut connections,
3667                &mut dirty,
3668                &mut to_remove,
3669                &mut cqes,
3670            );
3671
3672            assert_eq!(
3673                connections[&3].last_send_attempt, attempted_at,
3674                "no SEND submitted inside the pacing window"
3675            );
3676            assert!(dirty.contains(&3), "paced peer stays dirty");
3677            assert!(to_remove.is_empty());
3678
3679            // Outside the pacing window the retry happens: backdate the
3680            // last attempt and verify the attempt timestamp advances.
3681            let Some(backdated) = Instant::now().checked_sub(BLOCKED_RETRY_INTERVAL * 2) else {
3682                eprintln!("monotonic clock too young to backdate; skipping");
3683                return;
3684            };
3685            connections.get_mut(&3).unwrap().last_send_attempt = backdated;
3686            flush_sends(
3687                &mut ring,
3688                &mut connections,
3689                &mut dirty,
3690                &mut to_remove,
3691                &mut cqes,
3692            );
3693            assert!(
3694                connections[&3].last_send_attempt > backdated,
3695                "retry submitted once the pacing window elapsed"
3696            );
3697        }
3698
3699        /// Review finding F1 (io-uring-audit-2026-08.md): a
3700        /// response-initiated drop must tear down the *whole* socket and
3701        /// release the `max_connections` permit. The reader holds its
3702        /// own dup of the socket, so merely dropping this stage's entry
3703        /// (closing our dup) leaves the socket open — the EOF below must
3704        /// arrive *despite* a live second dup, which only shutdown(2)
3705        /// achieves.
3706        #[test]
3707        fn teardown_shuts_the_socket_despite_other_dups_and_releases_the_permit() {
3708            use std::sync::atomic::{AtomicU64, Ordering};
3709
3710            let (tx, mut peer) = UnixStream::pair().unwrap();
3711            // Simulates the reader's half: still open across the drop.
3712            let reader_dup = tx.try_clone().unwrap();
3713            let entry = entry_for(tx, &[]);
3714            let active = AtomicU64::new(1);
3715
3716            super::super::teardown_dropped(entry, &active);
3717
3718            assert_eq!(active.load(Ordering::Relaxed), 0, "permit released");
3719            peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
3720            let mut buf = [0u8; 1];
3721            assert_eq!(
3722                peer.read(&mut buf).expect("EOF, not a timeout"),
3723                0,
3724                "peer observes EOF even though another dup is still open"
3725            );
3726            drop(reader_dup);
3727        }
3728    }
3729
3730    /// One drain of early replies can release thousands to one connection
3731    /// (a burst of refused or shed requests). The drain applies the
3732    /// consumed path's flush cadence between replies, so a client that
3733    /// reads its replies keeps its connection however many there are,
3734    /// and `MAX_SEND_BUF` still drops one that does not read.
3735    mod early_drain {
3736        use std::os::unix::io::AsRawFd;
3737        use std::os::unix::net::UnixStream;
3738        use std::sync::Arc;
3739        use std::sync::atomic::{AtomicU64, Ordering};
3740        use std::thread;
3741        use std::time::{Duration, Instant};
3742
3743        use counter_server::{CounterReport, ResponseEncoder};
3744        use io_uring::IoUring;
3745        use melin_wire_protocol::control::RefusalKind;
3746
3747        use super::early_release::widest_reason;
3748        use super::flush_sends::{entry_for, fill_socket};
3749        use super::refusal_order::{Seen, early, early_id, frames_of, read_frame};
3750        use crate::batch_boundary::BatchBoundary;
3751        use crate::early_reply::{EARLY_REPLY_QUEUE_CAPACITY, EarlyKind, early_reply_channel};
3752        use crate::response::{Egress, MAX_SEND_BUF};
3753        use crate::response_frame::{EarlyReplyBuf, MAX_EARLY_REPLY};
3754
3755        /// Replies in the burst: alternately a busy and a refusal with the
3756        /// widest reason, together well past `MAX_SEND_BUF`, within the
3757        /// queue's capacity.
3758        const BURST: u64 = 800;
3759
3760        /// The kind of the burst's reply number `n`.
3761        fn kind(n: u64) -> EarlyKind<CounterReport> {
3762            if n.is_multiple_of(2) {
3763                EarlyKind::Busy
3764            } else {
3765                EarlyKind::Refused {
3766                    kind: RefusalKind::PermissionDenied,
3767                    reason: widest_reason(),
3768                }
3769            }
3770        }
3771
3772        /// A stage's egress holding one connection, 1, on `stream`, and
3773        /// the count of live connections it decrements.
3774        fn egress_over(stream: UnixStream) -> (Egress, Arc<AtomicU64>) {
3775            let active = Arc::new(AtomicU64::new(1));
3776            let mut egress = Egress::new(
3777                IoUring::new(64).expect("an io_uring"),
3778                64,
3779                Arc::clone(&active),
3780                #[cfg(feature = "latency-trace")]
3781                melin_transport_core::trace::register_stage("test: server e2e"),
3782                #[cfg(feature = "tick-to-trade")]
3783                melin_transport_core::trace::register_stage("test: egress"),
3784            );
3785            let mut entry = entry_for(stream, &[]);
3786            entry.dirty = false;
3787            egress.connections.insert(1, entry);
3788            (egress, active)
3789        }
3790
3791        /// Queue the burst for connection 1 and release it in one drain,
3792        /// then flush as the idle path does.
3793        fn drain_burst(egress: &mut Egress) -> usize {
3794            assert!(BURST as usize <= EARLY_REPLY_QUEUE_CAPACITY);
3795            let (mut tx, mut rx) =
3796                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
3797            for n in 0..BURST {
3798                tx.try_send(early(1, n, kind(n))).expect("queue has room");
3799            }
3800            tx.flush();
3801            let boundary = BatchBoundary::new();
3802            let at = boundary.at_boundary().expect("between events");
3803            let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
3804            let released = egress.release_early_replies(
3805                &mut rx.register(|| {}),
3806                &at,
3807                &ResponseEncoder,
3808                &mut early_buf,
3809                Instant::now,
3810            );
3811            egress.drop_overflowed();
3812            egress.flush();
3813            released
3814        }
3815
3816        #[test]
3817        fn a_drain_past_the_send_buffer_flushes_as_it_goes_and_keeps_a_reading_client() {
3818            let expected: Vec<Seen> = (0..BURST)
3819                .flat_map(|n| frames_of(kind(n), early_id(n)))
3820                .collect();
3821            // On the wire: a busy is its length prefix, tag and ID; a
3822            // refusal adds its kind, the reason's length, and the reason.
3823            let wire_bytes: usize = (0..BURST)
3824                .map(|n| match kind(n) {
3825                    EarlyKind::Busy => 13,
3826                    _ => 15 + widest_reason().len(),
3827                })
3828                .sum();
3829            assert!(wire_bytes > MAX_SEND_BUF, "the burst outgrows the buffer");
3830
3831            let (server, mut client) = UnixStream::pair().expect("socketpair");
3832            client
3833                .set_read_timeout(Some(Duration::from_secs(5)))
3834                .expect("set read timeout");
3835            let count = expected.len();
3836            let reader = thread::spawn(move || {
3837                (0..count)
3838                    .map_while(|_| read_frame(&mut client).ok())
3839                    .collect::<Vec<_>>()
3840            });
3841
3842            let (mut egress, active) = egress_over(server);
3843            assert_eq!(drain_burst(&mut egress), BURST as usize);
3844            // What a flush left behind on a socket the reader had not
3845            // emptied yet goes out on the paced retries.
3846            let deadline = Instant::now() + Duration::from_secs(5);
3847            while !egress.dirty.is_empty() && Instant::now() < deadline {
3848                thread::sleep(Duration::from_millis(1));
3849                egress.flush();
3850            }
3851
3852            assert!(
3853                egress.connections.contains_key(&1),
3854                "a client reading its replies was dropped"
3855            );
3856            assert_eq!(active.load(Ordering::Relaxed), 1);
3857            assert_eq!(reader.join().expect("reader"), expected, "in queue order");
3858        }
3859
3860        #[test]
3861        fn a_drain_to_a_client_that_does_not_read_drops_it_at_the_cap() {
3862            let (server, _client) = UnixStream::pair().expect("socketpair");
3863            fill_socket(server.as_raw_fd());
3864            let (mut egress, active) = egress_over(server);
3865
3866            assert_eq!(drain_burst(&mut egress), BURST as usize);
3867
3868            assert!(
3869                !egress.connections.contains_key(&1),
3870                "a client that reads nothing kept its connection"
3871            );
3872            assert!(!egress.dirty.contains(&1));
3873            assert_eq!(
3874                active.load(Ordering::Relaxed),
3875                0,
3876                "its permit released once"
3877            );
3878        }
3879
3880        /// A flush inside the drain that fails (the client has hung up)
3881        /// tears the connection down there, once, and the rest of the
3882        /// drain's replies to it are dropped as for any connection gone.
3883        #[test]
3884        fn a_drain_to_a_client_that_has_hung_up_tears_it_down_once() {
3885            let (server, client) = UnixStream::pair().expect("socketpair");
3886            drop(client);
3887            let (mut egress, active) = egress_over(server);
3888
3889            assert_eq!(drain_burst(&mut egress), BURST as usize);
3890
3891            assert!(!egress.connections.contains_key(&1));
3892            assert!(egress.to_remove.is_empty(), "nothing left to tear down");
3893            assert_eq!(
3894                active.load(Ordering::Relaxed),
3895                0,
3896                "its permit released once"
3897            );
3898        }
3899
3900        /// A cadence made due by appends to a connection gone since (so
3901        /// nothing is dirty) is restarted by the flush that finds nothing
3902        /// to send, rather than left due for every later slot.
3903        #[test]
3904        fn a_due_cadence_with_nothing_dirty_is_restarted() {
3905            let (server, _client) = UnixStream::pair().expect("socketpair");
3906            let (mut egress, _active) = egress_over(server);
3907            for _ in 0..super::super::FLUSH_SLOT_INTERVAL {
3908                egress.cadence.on_append(1);
3909            }
3910            assert!(egress.cadence.is_due());
3911            assert!(egress.dirty.is_empty());
3912
3913            egress.flush_if_due();
3914
3915            assert!(!egress.cadence.is_due());
3916        }
3917    }
3918
3919    /// The io_uring stage's gate-wait hook, called directly: the stage
3920    /// tests drive it through a running stage, where whether a reply
3921    /// queued during a wait goes out from the hook or from the boundary
3922    /// drain after the admit is up to the scheduler.
3923    mod gate_wait_hook {
3924        use std::os::unix::net::UnixStream;
3925        use std::sync::Arc;
3926        use std::sync::atomic::AtomicU64;
3927        use std::sync::mpsc;
3928        use std::time::Duration;
3929
3930        use counter_server::{CounterReport, ResponseEncoder};
3931        use io_uring::IoUring;
3932
3933        use super::refusal_order::{Seen, connection, early, early_id, frames_of, read_frame};
3934        use crate::batch_boundary::BatchBoundary;
3935        use crate::durability_gate::GateWait;
3936        use crate::early_reply::{EarlyKind, early_reply_channel};
3937        use crate::response::{Egress, GateWaitHook};
3938        use crate::response_frame::{EarlyReplyBuf, MAX_EARLY_REPLY};
3939
3940        const BUSY: EarlyKind<CounterReport> = EarlyKind::Busy;
3941
3942        /// Register nothing, then — as if during a wait — queue
3943        /// connection 1's `Connected` and a busy for it, and call the
3944        /// hook's `while_waiting` once, between events or not. Returns the
3945        /// egress after the call and the client's end of the socket.
3946        fn one_turn(between_events: bool) -> (Egress, UnixStream) {
3947            let mut egress = Egress::new(
3948                IoUring::new(64).expect("an io_uring"),
3949                64,
3950                Arc::new(AtomicU64::new(1)),
3951                #[cfg(feature = "latency-trace")]
3952                melin_transport_core::trace::register_stage("test: server e2e"),
3953                #[cfg(feature = "tick-to-trade")]
3954                melin_transport_core::trace::register_stage("test: egress"),
3955            );
3956            let (control_tx, control_rx) = mpsc::channel();
3957            let (mut tx, mut rx) =
3958                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
3959            let mut registered = rx.register(|| {});
3960
3961            let (event, client) = connection(1);
3962            control_tx.send(event).expect("the receiver is held");
3963            tx.try_send(early(1, 0, BUSY)).expect("queue has room");
3964            tx.flush();
3965
3966            let boundary = BatchBoundary::new();
3967            let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
3968            GateWaitHook {
3969                egress: &mut egress,
3970                at_boundary: if between_events {
3971                    Some(boundary.at_boundary().expect("between events"))
3972                } else {
3973                    None
3974                },
3975                registered: &mut registered,
3976                control_rx: &control_rx,
3977                encoder: &ResponseEncoder,
3978                early_buf: &mut early_buf,
3979            }
3980            .while_waiting();
3981            (egress, client)
3982        }
3983
3984        #[test]
3985        fn between_events_it_registers_sends_and_flushes() {
3986            let (egress, mut client) = one_turn(true);
3987
3988            assert!(egress.connections.contains_key(&1), "not registered");
3989            assert!(egress.dirty.is_empty(), "sent but left buffered");
3990            // A reply that never reached the socket fails the read
3991            // rather than hanging the test.
3992            client
3993                .set_read_timeout(Some(Duration::from_secs(5)))
3994                .expect("set read timeout");
3995            let expected = frames_of(BUSY, early_id(0));
3996            let seen: Vec<Seen> = expected
3997                .iter()
3998                .map(|_| read_frame(&mut client).expect("the busy's frames"))
3999                .collect();
4000            assert_eq!(seen, expected);
4001        }
4002
4003        #[test]
4004        fn inside_an_event_it_does_nothing() {
4005            let (egress, mut client) = one_turn(false);
4006
4007            assert!(egress.connections.is_empty(), "control drained");
4008            assert!(egress.dirty.is_empty());
4009            client
4010                .set_read_timeout(Some(Duration::from_millis(50)))
4011                .expect("set read timeout");
4012            assert!(read_frame(&mut client).is_err(), "a reply went out");
4013        }
4014    }
4015
4016    /// The stage learns of a connection from its control channel and of
4017    /// that connection's replies from the output ring. A reply always
4018    /// trails its `Connected`, so reading the ring before draining the
4019    /// channel is enough for every slot read to find its connection.
4020    /// Drained first, the stage could read the ring after a `Connected`
4021    /// it had not seen — the accept loop queues that event only after
4022    /// the client holds `ServerReady`, so a first request is often on
4023    /// the wire before the stage has heard of its connection — and drop
4024    /// the reply. Seen as a first reply lost on a fresh node under
4025    /// full-suite load. The seam sits right
4026    /// after the drain: with the drain after the read that is harmless,
4027    /// with the drain before it the seam is the gap, and a `Connected`
4028    /// landing there is missed by the read that follows. Holding the
4029    /// stage at the seam makes the interleaving the test's rather than
4030    /// the scheduler's.
4031    mod registration_race {
4032        use std::io::{Read, Write};
4033        use std::os::unix::io::AsRawFd;
4034        use std::os::unix::net::UnixStream;
4035        use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4036        use std::sync::{Arc, mpsc};
4037        use std::thread;
4038        use std::time::Duration;
4039
4040        use counter_server::{
4041            Counter, CounterQuery, CounterReport, KIND_RESP_ACK, ResponseEncoder,
4042        };
4043        use melin_pipeline::ring::DisruptorBuilder;
4044        use melin_pipeline::wait::WaitStrategy;
4045        use melin_transport_core::fence::FenceState;
4046        use melin_transport_core::pipeline::{OutputPayload, OutputSlot, StageUtilization};
4047        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
4048        use melin_wire_protocol::blocking::BlockingFrameWriter;
4049        use melin_wire_protocol::control_codec::TAG_APP;
4050
4051        use crate::ControlEvent;
4052        use crate::ack_policy::AckPolicy;
4053        use crate::response::{Response, run};
4054
4055        /// `CounterReport::Ack` on the wire: len(4) + tag(1), then the
4056        /// counter's body, kind(1) + value(8).
4057        const FRAME_LEN: usize = 14;
4058        /// Long enough that a genuine loss is distinguishable from
4059        /// scheduler noise on a loaded box, short enough to fail in
4060        /// bounded time.
4061        const READ_TIMEOUT: Duration = Duration::from_secs(5);
4062
4063        #[test]
4064        fn a_reply_that_lands_with_its_connection_is_delivered() {
4065            let (mut producer, mut consumers) =
4066                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
4067                    .add_consumer()
4068                    .build(WaitStrategy::SpinThenYield);
4069            let consumer = consumers.pop().expect("one consumer was requested");
4070
4071            let (server_sock, mut client_sock) = UnixStream::pair().expect("socketpair");
4072            let server_fd = server_sock.as_raw_fd();
4073            let writer = BlockingFrameWriter::new(Box::new(server_sock) as Box<dyn Write + Send>);
4074            client_sock
4075                .set_read_timeout(Some(READ_TIMEOUT))
4076                .expect("set read timeout");
4077
4078            // The stage reports reaching the gap on `parked` and waits on
4079            // `resume` — once, on its first iteration.
4080            let (parked_tx, parked_rx) = mpsc::channel::<()>();
4081            let (resume_tx, resume_rx) = mpsc::channel::<()>();
4082            let mut held = false;
4083            let pause = move || {
4084                if !held {
4085                    held = true;
4086                    parked_tx.send(()).expect("the test is waiting");
4087                    resume_rx.recv().expect("the test resumes the stage");
4088                }
4089            };
4090
4091            let (control_tx, control_rx) = mpsc::channel();
4092            let shutdown = AtomicBool::new(false);
4093            let config = Response::<Counter> {
4094                // The journal is already past the one event, so the gate
4095                // is open and delivery hinges on registration alone.
4096                journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(1)),
4097                policy: crate::response::GatePolicy {
4098                    start: crate::response::GateStart::settled(AckPolicy::Disk),
4099                    forced: melin_transport_core::lineage::forced_policy_cell().1,
4100                },
4101                replication_metrics: None,
4102                replica_active: None,
4103                heartbeat_interval: None,
4104                wait: WaitStrategy::SpinThenYield,
4105                utilization: Arc::new(StageUtilization::default()),
4106                encoder: Arc::new(ResponseEncoder),
4107                fence_state: Arc::new(FenceState::new(0)),
4108                active_connections: Arc::new(AtomicU64::new(0)),
4109                // No reader: nothing is ever answered early.
4110                early_replies: crate::early_reply::early_reply_channel(
4111                    crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY,
4112                )
4113                .1,
4114                ring_sizing: crate::connection_limit::RingSizing::for_max_connections(1)
4115                    .expect("a supported cap"),
4116                ready: None,
4117                degraded_release: None,
4118                pause_after_control_drain: Some(Box::new(pause)),
4119            };
4120
4121            thread::scope(|scope| {
4122                let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
4123
4124                // The stage has drained an empty control channel and is
4125                // held at the seam. Now both arrive: read after drain,
4126                // the next iteration finds the slot and then its
4127                // connection; drain after read, the read would find the
4128                // slot and the connection would still be in the channel.
4129                parked_rx.recv().expect("the stage reaches the seam");
4130                control_tx
4131                    .send(ControlEvent::Connected {
4132                        connection_id: 1,
4133                        fd: server_fd,
4134                        writer,
4135                    })
4136                    .expect("stage is running");
4137                producer.publish(OutputSlot {
4138                    connection_id: 1,
4139                    wire_seq: 1,
4140                    payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
4141                    // No trailing `BatchEnd`: one frame on the wire.
4142                    is_last_in_request: false,
4143                    ..Default::default()
4144                });
4145                resume_tx.send(()).expect("the stage is parked");
4146
4147                let mut frame = [0u8; FRAME_LEN];
4148                let delivered = client_sock.read_exact(&mut frame);
4149                shutdown.store(true, Ordering::Relaxed);
4150                stage.join().expect("stage panicked");
4151                delivered.expect(
4152                    "the reply was dropped: the control channel was drained before the ring was read",
4153                );
4154                assert_eq!(
4155                    frame[4..6],
4156                    [TAG_APP, KIND_RESP_ACK],
4157                    "an application frame carrying the counter's ack"
4158                );
4159                assert_eq!(
4160                    u64::from_le_bytes(frame[6..].try_into().expect("8 bytes")),
4161                    7,
4162                    "the ack carries the counter's value"
4163                );
4164            });
4165        }
4166    }
4167
4168    /// An early reply (a write refused while halted, a request filtered or
4169    /// refused by the decoder, one shed on a full ring) reaches the stage
4170    /// only with its connection registered.
4171    ///
4172    /// Its frame classifier and early-reply builders are shared with the
4173    /// other early-reply tests here and the DPDK stage's
4174    /// (`dpdk_response.rs`).
4175    pub(crate) mod refusal_order {
4176        use std::io::{Read, Write};
4177        use std::os::unix::io::AsRawFd;
4178        use std::os::unix::net::UnixStream;
4179        use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4180        use std::sync::{Arc, mpsc};
4181        use std::thread;
4182        use std::time::Duration;
4183
4184        use counter_server::{
4185            Counter, CounterQuery, CounterReport, KIND_RESP_REJECTED, ResponseEncoder,
4186        };
4187        use melin_pipeline::ring::DisruptorBuilder;
4188        use melin_pipeline::wait::WaitStrategy;
4189        use melin_transport_core::fence::FenceState;
4190        use melin_transport_core::pipeline::{OutputSlot, RequestId, StageUtilization};
4191        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
4192        use melin_wire_protocol::blocking::BlockingFrameWriter;
4193        use melin_wire_protocol::control_codec::{
4194            TAG_APP, TAG_BATCH_END, TAG_REFUSED, TAG_SERVER_BUSY, terminal_request_id,
4195        };
4196
4197        use crate::ControlEvent;
4198        use crate::ack_policy::AckPolicy;
4199        use crate::early_reply::{EarlyKind, EarlyReply, EarlyReplyQueue, early_reply_channel};
4200        use crate::response::{Response, run};
4201
4202        /// Bounds a read that must succeed; generous for a loaded box.
4203        pub(crate) const READ_TIMEOUT: Duration = Duration::from_secs(5);
4204
4205        /// What one frame on the wire was: a counter response, by the
4206        /// kind its body starts with; a frame that ends a request's reply,
4207        /// by its tag and the request's ID; or another of the protocol's
4208        /// own frames, by its tag.
4209        #[derive(Debug, PartialEq, Eq)]
4210        pub(crate) enum Seen {
4211            App(u8),
4212            Final(u8, RequestId),
4213            Protocol(u8),
4214        }
4215
4216        impl Seen {
4217            /// What a frame with this payload (its length prefix stripped)
4218            /// is.
4219            pub(crate) fn of(payload: &[u8]) -> Self {
4220                if let Some(id) = terminal_request_id(payload) {
4221                    return Seen::Final(payload[0], id);
4222                }
4223                match *payload {
4224                    [TAG_APP, kind, ..] => Seen::App(kind),
4225                    // Every counter response carries a kind.
4226                    [TAG_APP] => panic!("a counter response with an empty body"),
4227                    [tag, ..] => Seen::Protocol(tag),
4228                    [] => panic!("an empty frame on the wire"),
4229                }
4230            }
4231        }
4232
4233        /// The ID early reply number `n` carries in these tests: distinct
4234        /// per reply, and from the events' own.
4235        pub(crate) fn early_id(n: u64) -> RequestId {
4236            RequestId(1000 + n)
4237        }
4238
4239        /// Early reply number `n`, of `kind`, for `connection_id`: its
4240        /// request is [`early_id`]`(n)`.
4241        pub(crate) fn early(
4242            connection_id: u64,
4243            n: u64,
4244            kind: EarlyKind<CounterReport>,
4245        ) -> EarlyReply<CounterReport> {
4246            EarlyReply {
4247                connection_id,
4248                request_id: early_id(n),
4249                kind,
4250            }
4251        }
4252
4253        /// The frames an early reply of `kind` for request `id` goes out
4254        /// as.
4255        pub(crate) fn frames_of(kind: EarlyKind<CounterReport>, id: RequestId) -> Vec<Seen> {
4256            match kind {
4257                EarlyKind::Rejected(_) => {
4258                    vec![
4259                        Seen::App(KIND_RESP_REJECTED),
4260                        Seen::Final(TAG_BATCH_END, id),
4261                    ]
4262                }
4263                EarlyKind::Filtered => vec![Seen::Final(TAG_BATCH_END, id)],
4264                EarlyKind::Refused { .. } => vec![Seen::Final(TAG_REFUSED, id)],
4265                EarlyKind::Busy => vec![Seen::Final(TAG_SERVER_BUSY, id)],
4266            }
4267        }
4268
4269        /// Read one frame and say what it was.
4270        pub(super) fn read_frame(sock: &mut UnixStream) -> std::io::Result<Seen> {
4271            let mut len = [0u8; 4];
4272            sock.read_exact(&mut len)?;
4273            let mut payload = vec![0u8; u32::from_le_bytes(len) as usize];
4274            sock.read_exact(&mut payload)?;
4275            Ok(Seen::of(&payload))
4276        }
4277
4278        /// Read one frame into `seen`; false if none arrived.
4279        fn read_into(sock: &mut UnixStream, seen: &mut Vec<Seen>) -> bool {
4280            match read_frame(sock) {
4281                Ok(frame) => {
4282                    seen.push(frame);
4283                    true
4284                }
4285                Err(_) => false,
4286            }
4287        }
4288
4289        /// A refusal reaches the stage only with its connection registered.
4290        /// The reader refuses a new connection's first request as soon as
4291        /// that connection's `Connected` is queued, so a refusal made
4292        /// visible while the stage is past its control drain can belong to
4293        /// a connection the stage has not heard of. Released in that
4294        /// iteration it would be dropped, and the client left without a
4295        /// reply.
4296        ///
4297        /// The seam holds the stage after its drain twice. The first hold
4298        /// lands connection 1's refusal, which the next iteration's idle
4299        /// path is then due to release; the second hold, in that iteration,
4300        /// lands connection 2 and its refusal just ahead of the release.
4301        /// The same inside a gate wait is `early_release`'s
4302        /// `connection_first_seen_inside_a_gate_wait`.
4303        #[test]
4304        fn a_refusal_is_not_released_ahead_of_its_connection() {
4305            let (_producer, mut consumers) =
4306                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
4307                    .add_consumer()
4308                    .build(WaitStrategy::SpinThenYield);
4309            let consumer = consumers.pop().expect("one consumer was requested");
4310            let (connected_1, mut client_1) = connection(1);
4311            let (connected_2, mut client_2) = connection(2);
4312
4313            let (mut refusals, refusal_rx) =
4314                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
4315            let (control_tx, control_rx) = mpsc::channel();
4316            control_tx.send(connected_1).expect("channel open");
4317
4318            let (parked_tx, parked_rx) = mpsc::channel::<()>();
4319            let (resume_tx, resume_rx) = mpsc::channel::<()>();
4320            let mut holds = 0;
4321            let pause = move || {
4322                if holds < 2 {
4323                    holds += 1;
4324                    parked_tx.send(()).expect("the test is waiting");
4325                    resume_rx.recv().expect("the test resumes the stage");
4326                }
4327            };
4328            let shutdown = AtomicBool::new(false);
4329            let config = config(
4330                DurableWireSeqCursor::detached(WireSeq::new(0)),
4331                Arc::new(FenceState::new(0)),
4332                refusal_rx,
4333                Some(Box::new(pause)),
4334            );
4335
4336            thread::scope(|scope| {
4337                let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
4338
4339                parked_rx.recv().expect("the stage reaches the seam");
4340                refusals.try_send(refusal(1, 0)).expect("queue has room");
4341                refusals.flush();
4342                resume_tx.send(()).expect("the stage is parked");
4343
4344                parked_rx.recv().expect("the stage reaches the seam again");
4345                control_tx.send(connected_2).expect("stage is running");
4346                refusals.try_send(refusal(2, 0)).expect("queue has room");
4347                refusals.flush();
4348                resume_tx.send(()).expect("the stage is parked");
4349
4350                let mut seen_1 = Vec::new();
4351                let mut seen_2 = Vec::new();
4352                let got_1 = (0..2).all(|_| read_into(&mut client_1, &mut seen_1));
4353                let got_2 = (0..2).all(|_| read_into(&mut client_2, &mut seen_2));
4354                shutdown.store(true, Ordering::Relaxed);
4355                stage.join().expect("stage panicked");
4356
4357                assert!(got_1, "connection 1's refusal never arrived: {seen_1:x?}");
4358                assert!(
4359                    got_2,
4360                    "connection 2's refusal was dropped: released before its connection registered"
4361                );
4362                for seen in [seen_1, seen_2] {
4363                    assert_eq!(
4364                        seen,
4365                        [
4366                            Seen::App(KIND_RESP_REJECTED),
4367                            Seen::Final(TAG_BATCH_END, early_id(0))
4368                        ]
4369                    );
4370                }
4371            });
4372        }
4373
4374        /// A registered connection: its `Connected` event, and the client's
4375        /// end of the socket.
4376        pub(super) fn connection(connection_id: u64) -> (ControlEvent, UnixStream) {
4377            let (server_sock, client_sock) = UnixStream::pair().expect("socketpair");
4378            client_sock
4379                .set_read_timeout(Some(READ_TIMEOUT))
4380                .expect("set read timeout");
4381            let event = ControlEvent::Connected {
4382                connection_id,
4383                fd: server_sock.as_raw_fd(),
4384                writer: BlockingFrameWriter::new(Box::new(server_sock) as Box<dyn Write + Send>),
4385            };
4386            (event, client_sock)
4387        }
4388
4389        /// Halt refusal number `n`, for `connection_id`.
4390        pub(crate) fn refusal(connection_id: u64, n: u64) -> EarlyReply<CounterReport> {
4391            early(
4392                connection_id,
4393                n,
4394                EarlyKind::Rejected(CounterReport::Rejected),
4395            )
4396        }
4397
4398        /// A stage gating on `journal_cursor` under `disk`, fed
4399        /// `early_replies`.
4400        pub(super) fn config(
4401            journal_cursor: DurableWireSeqCursor,
4402            fence_state: Arc<FenceState>,
4403            early_replies: EarlyReplyQueue<CounterReport>,
4404            pause_after_control_drain: Option<Box<dyn FnMut() + Send>>,
4405        ) -> Response<Counter> {
4406            Response::<Counter> {
4407                journal_persisted_wire_seq: journal_cursor,
4408                policy: crate::response::GatePolicy {
4409                    start: crate::response::GateStart::settled(AckPolicy::Disk),
4410                    forced: melin_transport_core::lineage::forced_policy_cell().1,
4411                },
4412                replication_metrics: None,
4413                replica_active: None,
4414                heartbeat_interval: None,
4415                wait: WaitStrategy::SpinThenYield,
4416                utilization: Arc::new(StageUtilization::default()),
4417                encoder: Arc::new(ResponseEncoder),
4418                fence_state,
4419                active_connections: Arc::new(AtomicU64::new(0)),
4420                early_replies,
4421                ring_sizing: crate::connection_limit::RingSizing::for_max_connections(4)
4422                    .expect("a supported cap"),
4423                ready: None,
4424                degraded_release: None,
4425                pause_after_control_drain,
4426            }
4427        }
4428    }
4429
4430    /// Early replies go out at the first point between two events'
4431    /// replies, without waiting for replies to earlier requests, and never
4432    /// inside an event's reply. The scenarios and their rig are shared
4433    /// with the DPDK stage, whose tests run each against its own harness
4434    /// (`dpdk_response.rs`); the kernel stage's harness is here.
4435    pub(crate) mod early_release {
4436        use std::collections::{BTreeMap, HashMap};
4437        use std::os::unix::net::UnixStream;
4438        use std::sync::atomic::{AtomicBool, Ordering};
4439        use std::sync::{Arc, OnceLock, mpsc};
4440        use std::thread::{self, JoinHandle};
4441        use std::time::{Duration, Instant};
4442
4443        use counter_server::{
4444            Counter, CounterQuery, CounterReport, KIND_RESP_ACK, KIND_RESP_REJECTED,
4445        };
4446        use melin_pipeline::ring::{self, DisruptorBuilder};
4447        use melin_pipeline::wait::WaitStrategy;
4448        use melin_transport_core::fence::FenceState;
4449        use melin_transport_core::pipeline::{
4450            OutputPayload, OutputSlot, RequestId, StageUtilization,
4451        };
4452        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
4453        use melin_wire_protocol::control::{MAX_REFUSAL_REASON, RefusalKind};
4454        use melin_wire_protocol::control_codec::TAG_BATCH_END;
4455        use proptest::prelude::*;
4456
4457        use super::refusal_order::{
4458            READ_TIMEOUT, Seen, config, connection, early, early_id, frames_of, read_frame,
4459        };
4460        use crate::ControlEvent;
4461        use crate::ack_policy::AckPolicy;
4462        use crate::early_reply::{
4463            EarlyKind, EarlyReplyQueue, EarlyReplySender, early_reply_channel,
4464        };
4465        use crate::replication::ReplicationMetrics;
4466        use crate::response::run;
4467
4468        /// How long a test watches for a frame that must not arrive yet:
4469        /// long enough that a stage wrongly sending it would have.
4470        pub(crate) const QUIET: Duration = Duration::from_millis(100);
4471
4472        /// An output slot of the counter.
4473        pub(crate) type Slot = OutputSlot<CounterReport, CounterQuery>;
4474
4475        /// Every kind of early reply, a decoder refusal with the widest
4476        /// reason a frame carries.
4477        pub(crate) fn every_kind() -> [EarlyKind<CounterReport>; 4] {
4478            [
4479                EarlyKind::Rejected(CounterReport::Rejected),
4480                EarlyKind::Filtered,
4481                EarlyKind::Refused {
4482                    kind: RefusalKind::Malformed,
4483                    reason: widest_reason(),
4484                },
4485                EarlyKind::Busy,
4486            ]
4487        }
4488
4489        /// A refusal reason as wide as a `Refused` frame carries.
4490        pub(crate) fn widest_reason() -> &'static str {
4491            static REASON: OnceLock<&'static str> = OnceLock::new();
4492            REASON.get_or_init(|| "r".repeat(MAX_REFUSAL_REASON).leak())
4493        }
4494
4495        /// What a stage under test starts with: the output ring, the
4496        /// early-reply queue, and a gate under `disk+ram` over the journal
4497        /// cursor and one replica, whose in-memory cursor is far ahead,
4498        /// so the journal decides what is held until the replica is
4499        /// marked gone.
4500        pub(crate) struct StageInputs {
4501            pub(crate) consumer: ring::Consumer<Slot>,
4502            pub(crate) early_replies: EarlyReplyQueue<CounterReport>,
4503            pub(crate) journal: DurableWireSeqCursor,
4504            pub(crate) replication_metrics: Arc<ReplicationMetrics>,
4505            pub(crate) replica_active: [Arc<AtomicBool>; 2],
4506            pub(crate) utilization: Arc<StageUtilization>,
4507            pub(crate) shutdown: Arc<AtomicBool>,
4508        }
4509
4510        /// A running response stage, seen from its clients. Stops the
4511        /// stage when dropped, so a failed assertion does not leave it
4512        /// spinning.
4513        pub(crate) trait StageHarness {
4514            /// Register `connection_id` with the stage: its `Connected`
4515            /// on the control channel.
4516            fn connect(&mut self, connection_id: u64);
4517            /// The next frame the stage sent `connection_id`, waiting up
4518            /// to `within`; `None` if none came.
4519            fn next_frame(&mut self, connection_id: u64, within: Duration) -> Option<Seen>;
4520        }
4521
4522        /// The test's side of a stage under test: it plays the matching
4523        /// stage, the reader, the journal and the replica.
4524        pub(crate) struct Rig<H> {
4525            pub(crate) producer: ring::Producer<Slot>,
4526            pub(crate) early: EarlyReplySender<CounterReport>,
4527            pub(crate) journal: DurableWireSeqCursor,
4528            pub(crate) replica_active: Arc<AtomicBool>,
4529            pub(crate) utilization: Arc<StageUtilization>,
4530            pub(crate) stage: H,
4531        }
4532
4533        /// Start a stage with `start`, the journal at `journal_at`, and
4534        /// `connections` registered.
4535        pub(crate) fn rig<H: StageHarness>(
4536            start: fn(StageInputs) -> H,
4537            journal_at: u64,
4538            connections: &[u64],
4539        ) -> Rig<H> {
4540            let (producer, mut consumers) = DisruptorBuilder::<Slot>::new(128)
4541                .add_consumer()
4542                .build(WaitStrategy::SpinThenYield);
4543            let consumer = consumers.pop().expect("one consumer was requested");
4544            let (early, early_replies) =
4545                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
4546            let journal = DurableWireSeqCursor::detached(WireSeq::new(journal_at));
4547            let replication_metrics = Arc::new(ReplicationMetrics::default());
4548            replication_metrics.in_memory_sequence[0].store(1_000_000, Ordering::Relaxed);
4549            let replica_active = Arc::new(AtomicBool::new(true));
4550            let utilization = Arc::new(StageUtilization::default());
4551            let mut stage = start(StageInputs {
4552                consumer,
4553                early_replies,
4554                journal: journal.clone(),
4555                replication_metrics,
4556                replica_active: [
4557                    Arc::clone(&replica_active),
4558                    Arc::new(AtomicBool::new(false)),
4559                ],
4560                utilization: Arc::clone(&utilization),
4561                shutdown: Arc::new(AtomicBool::new(false)),
4562            });
4563            for &id in connections {
4564                stage.connect(id);
4565            }
4566            Rig {
4567                producer,
4568                early,
4569                journal,
4570                replica_active,
4571                utilization,
4572                stage,
4573            }
4574        }
4575
4576        impl<H: StageHarness> Rig<H> {
4577            /// Queue an early reply, flushed, as a reader does.
4578            pub(crate) fn answer_early(
4579                &mut self,
4580                connection_id: u64,
4581                n: u64,
4582                kind: EarlyKind<CounterReport>,
4583            ) {
4584                self.early
4585                    .try_send(early(connection_id, n, kind))
4586                    .expect("queue has room");
4587                self.early.flush();
4588            }
4589
4590            /// Publish one output slot: a counter ack for `request_id` on
4591            /// `connection_id`, its event at `wire_seq`.
4592            pub(crate) fn publish(
4593                &mut self,
4594                connection_id: u64,
4595                request_id: u64,
4596                wire_seq: u64,
4597                is_last_in_request: bool,
4598            ) {
4599                self.producer.publish(Slot {
4600                    connection_id,
4601                    request_id: RequestId(request_id),
4602                    input_seq: wire_seq - 1,
4603                    wire_seq,
4604                    payload: OutputPayload::Report(CounterReport::Ack {
4605                        new_value: wire_seq,
4606                    }),
4607                    is_last_in_request,
4608                    ..Default::default()
4609                });
4610            }
4611
4612            /// The next `count` frames for `connection_id`, each waited
4613            /// for up to [`READ_TIMEOUT`]; fewer if they stop coming.
4614            pub(crate) fn frames(&mut self, connection_id: u64, count: usize) -> Vec<Seen> {
4615                (0..count)
4616                    .map_while(|_| self.stage.next_frame(connection_id, READ_TIMEOUT))
4617                    .collect()
4618            }
4619
4620            /// Whether nothing reaches `connection_id` for [`QUIET`].
4621            pub(crate) fn quiet(&mut self, connection_id: u64) -> Option<Seen> {
4622                self.stage.next_frame(connection_id, QUIET)
4623            }
4624        }
4625
4626        /// The frames of a one-slot write's reply: the ack, then its batch
4627        /// end.
4628        fn write_reply(id: u64) -> Vec<Seen> {
4629            vec![
4630                Seen::App(KIND_RESP_ACK),
4631                Seen::Final(TAG_BATCH_END, RequestId(id)),
4632            ]
4633        }
4634
4635        /// An early reply for a connection whose event straddles two reads
4636        /// of the output ring (the matching stage has published only its
4637        /// first slot) waits for the event's terminator, and goes out
4638        /// right after it, never between its frames.
4639        pub(crate) fn an_early_reply_waits_out_an_event_split_across_reads<H: StageHarness>(
4640            start: fn(StageInputs) -> H,
4641        ) {
4642            let mut rig = rig(start, 10, &[1]);
4643            rig.publish(1, 7, 1, false);
4644            assert_eq!(
4645                rig.frames(1, 1),
4646                [Seen::App(KIND_RESP_ACK)],
4647                "the first slot"
4648            );
4649
4650            rig.answer_early(1, 0, EarlyKind::Busy);
4651            assert_eq!(
4652                rig.quiet(1),
4653                None,
4654                "an early reply inside the event's reply"
4655            );
4656
4657            rig.publish(1, 7, 1, true);
4658            let expected: Vec<Seen> = write_reply(7)
4659                .into_iter()
4660                .chain(frames_of(EarlyKind::Busy, early_id(0)))
4661                .collect();
4662            assert_eq!(rig.frames(1, 3), expected);
4663        }
4664
4665        /// Finding 18's shape: early replies of `kind` go out while the
4666        /// gate holds an earlier write of the same connection — one queued
4667        /// before the stage reached the write, and one decided while it
4668        /// waits — and so does one for another connection; the write's
4669        /// reply follows once its event is durable.
4670        pub(crate) fn early_replies_overtake_a_held_write<H: StageHarness>(
4671            start: fn(StageInputs) -> H,
4672            kind: EarlyKind<CounterReport>,
4673        ) {
4674            let mut rig = rig(start, 0, &[1, 2]);
4675            rig.answer_early(1, 0, kind);
4676            rig.publish(1, 7, 1, true);
4677            let before = frames_of(kind, early_id(0));
4678            assert_eq!(
4679                rig.frames(1, before.len()),
4680                before,
4681                "{kind:?} queued before"
4682            );
4683
4684            rig.answer_early(1, 1, kind);
4685            rig.answer_early(2, 2, kind);
4686            let during = frames_of(kind, early_id(1));
4687            assert_eq!(
4688                rig.frames(1, during.len()),
4689                during,
4690                "{kind:?} during the wait"
4691            );
4692            let other = frames_of(kind, early_id(2));
4693            assert_eq!(
4694                rig.frames(2, other.len()),
4695                other,
4696                "{kind:?} for another connection"
4697            );
4698            assert_eq!(rig.quiet(1), None, "the write is still held");
4699
4700            rig.journal.store(WireSeq::new(1));
4701            assert_eq!(rig.frames(1, 2), write_reply(7), "{kind:?}");
4702        }
4703
4704        /// A gate that blocks on a slot in the middle of an event — the
4705        /// cached durable position dropped to 0 by the idle re-check of a
4706        /// degraded policy, after the event's first slot went out — sends
4707        /// no early reply until the event's terminator has gone.
4708        pub(crate) fn a_gate_held_inside_an_event_sends_no_early_reply<H: StageHarness>(
4709            start: fn(StageInputs) -> H,
4710        ) {
4711            let mut rig = rig(start, 1, &[1]);
4712            rig.publish(1, 7, 1, false);
4713            assert_eq!(
4714                rig.frames(1, 1),
4715                [Seen::App(KIND_RESP_ACK)],
4716                "the first slot"
4717            );
4718
4719            // The replica leaves: the idle re-check finds the policy
4720            // degraded and pins the cached position to 0.
4721            rig.replica_active.store(false, Ordering::Relaxed);
4722            let deadline = Instant::now() + READ_TIMEOUT;
4723            while !rig.utilization.policy_degraded.load(Ordering::Relaxed) {
4724                assert!(Instant::now() < deadline, "the idle re-check never ran");
4725                thread::sleep(Duration::from_millis(5));
4726            }
4727
4728            // The event's last slot is held, and an early reply comes in.
4729            rig.publish(1, 7, 1, true);
4730            rig.answer_early(1, 0, EarlyKind::Busy);
4731            assert_eq!(rig.quiet(1), None, "an early reply inside the held event");
4732
4733            rig.replica_active.store(true, Ordering::Relaxed);
4734            let expected: Vec<Seen> = write_reply(7)
4735                .into_iter()
4736                .chain(frames_of(EarlyKind::Busy, early_id(0)))
4737                .collect();
4738            assert_eq!(rig.frames(1, 3), expected);
4739        }
4740
4741        /// A connection the stage first hears of while the gate holds a
4742        /// write — its `Connected` and an early reply for it both land
4743        /// during the wait — is registered before the reply is sent,
4744        /// which is therefore delivered, not dropped as unknown.
4745        pub(crate) fn connection_first_seen_inside_a_gate_wait<H: StageHarness>(
4746            start: fn(StageInputs) -> H,
4747        ) {
4748            let mut rig = rig(start, 0, &[1]);
4749            rig.publish(1, 7, 1, true);
4750            // Proof the stage has reached the held write: an early reply
4751            // for connection 1 queued after it is out.
4752            rig.answer_early(1, 0, EarlyKind::Busy);
4753            assert_eq!(rig.frames(1, 1), frames_of(EarlyKind::Busy, early_id(0)));
4754
4755            rig.stage.connect(2);
4756            let kind = EarlyKind::Rejected(CounterReport::Rejected);
4757            rig.answer_early(2, 1, kind);
4758            assert_eq!(
4759                rig.frames(2, 2),
4760                [
4761                    Seen::App(KIND_RESP_REJECTED),
4762                    Seen::Final(TAG_BATCH_END, early_id(1))
4763                ],
4764                "dropped: sent before its connection registered"
4765            );
4766            assert_eq!(rig.quiet(1), None, "the write is still held");
4767
4768            rig.journal.store(WireSeq::new(1));
4769            assert_eq!(rig.frames(1, 2), write_reply(7));
4770        }
4771
4772        /// One step of a [`Script`].
4773        #[derive(Clone, Debug)]
4774        pub(crate) enum Step {
4775            /// An event on `connection` with `slots` reports, its slots
4776            /// published one by one. With `straddle`, and more than one
4777            /// slot, the test pauses after the first, so the stage may
4778            /// read the event in two parts, and queues a busy for the same
4779            /// connection in the pause: the reply a stage that sent early
4780            /// replies mid-event would split the event with.
4781            Event {
4782                connection: u64,
4783                slots: u8,
4784                straddle: bool,
4785            },
4786            /// An early reply of kind `kind` (an index into
4787            /// [`every_kind`]) for `connection`.
4788            Early { connection: u64, kind: u8 },
4789            /// The journal catches up with every event published so far,
4790            /// and the next one: they are released, and the next event's
4791            /// slots pass the gate as they come.
4792            Open,
4793        }
4794
4795        /// The connections a script's steps are spread over.
4796        const SCRIPT_CONNECTIONS: [u64; 3] = [1, 2, 3];
4797
4798        pub(crate) fn step() -> impl Strategy<Value = Step> {
4799            prop_oneof![
4800                (1..=3u64, 1..=3u8, any::<bool>()).prop_map(|(connection, slots, straddle)| {
4801                    Step::Event {
4802                        connection,
4803                        slots,
4804                        straddle,
4805                    }
4806                }),
4807                (1..=3u64, 0..4u8).prop_map(|(connection, kind)| Step::Early { connection, kind }),
4808                Just(Step::Open),
4809            ]
4810        }
4811
4812        /// A script: events, early replies and gate releases interleaved
4813        /// across connections. Short enough that its slots fit the rig's
4814        /// ring while the gate holds all of them.
4815        pub(crate) fn script() -> impl Strategy<Value = Vec<Step>> {
4816            proptest::collection::vec(step(), 1..24)
4817        }
4818
4819        /// What a request's reply must be, as the client sees it: how many
4820        /// `Response` frames come right before its final frame, and which
4821        /// final frame.
4822        #[derive(Debug, PartialEq, Eq)]
4823        struct Owed {
4824            responses: usize,
4825            final_tag: u8,
4826        }
4827
4828        /// Run `script` against a stage, then release everything, and
4829        /// check what each connection received: every request has exactly
4830        /// one final frame naming it; its `Response` frames, and only
4831        /// them, come right before it (none of another request's frames
4832        /// between them); and the replies to published events come in
4833        /// the order the events were published.
4834        pub(crate) fn every_request_is_answered_once_and_whole<H: StageHarness>(
4835            start: fn(StageInputs) -> H,
4836            script: &[Step],
4837        ) -> Result<(), TestCaseError> {
4838            let mut rig = rig(start, 0, &SCRIPT_CONNECTIONS);
4839            // Per connection: what each request is owed, and the order
4840            // of its published events.
4841            let mut owed: HashMap<RequestId, Owed> = HashMap::new();
4842            let mut events: BTreeMap<u64, Vec<RequestId>> = BTreeMap::new();
4843            let mut expected_frames: BTreeMap<u64, usize> = BTreeMap::new();
4844            let mut wire_seq = 0;
4845            let mut early_n = 0;
4846            // Queue early reply number `early_n` of `kind` for
4847            // `connection`, and record what it is owed.
4848            let mut answer = |rig: &mut Rig<H>,
4849                              owed: &mut HashMap<RequestId, Owed>,
4850                              expected_frames: &mut BTreeMap<u64, usize>,
4851                              connection: u64,
4852                              kind: EarlyKind<CounterReport>| {
4853                let frames = frames_of(kind, early_id(early_n));
4854                let Some(&Seen::Final(final_tag, _)) = frames.last() else {
4855                    unreachable!("every early reply ends in a final frame");
4856                };
4857                owed.insert(
4858                    early_id(early_n),
4859                    Owed {
4860                        responses: frames.len() - 1,
4861                        final_tag,
4862                    },
4863                );
4864                *expected_frames.entry(connection).or_default() += frames.len();
4865                rig.answer_early(connection, early_n, kind);
4866                early_n += 1;
4867            };
4868            for step in script {
4869                match *step {
4870                    Step::Event {
4871                        connection,
4872                        slots,
4873                        straddle,
4874                    } => {
4875                        wire_seq += 1;
4876                        let id = RequestId(wire_seq);
4877                        for n in 1..=slots {
4878                            rig.publish(connection, id.0, wire_seq, n == slots);
4879                            if straddle && n == 1 && slots > 1 {
4880                                thread::sleep(Duration::from_millis(2));
4881                                answer(
4882                                    &mut rig,
4883                                    &mut owed,
4884                                    &mut expected_frames,
4885                                    connection,
4886                                    EarlyKind::Busy,
4887                                );
4888                            }
4889                        }
4890                        owed.insert(
4891                            id,
4892                            Owed {
4893                                responses: usize::from(slots),
4894                                final_tag: TAG_BATCH_END,
4895                            },
4896                        );
4897                        events.entry(connection).or_default().push(id);
4898                        *expected_frames.entry(connection).or_default() += usize::from(slots) + 1;
4899                    }
4900                    Step::Early { connection, kind } => answer(
4901                        &mut rig,
4902                        &mut owed,
4903                        &mut expected_frames,
4904                        connection,
4905                        every_kind()[usize::from(kind)],
4906                    ),
4907                    Step::Open => rig.journal.store(WireSeq::new(wire_seq + 1)),
4908                }
4909            }
4910            rig.journal.store(WireSeq::new(wire_seq + 1));
4911
4912            for connection in SCRIPT_CONNECTIONS {
4913                let count = expected_frames.get(&connection).copied().unwrap_or(0);
4914                let frames = rig.frames(connection, count);
4915                prop_assert_eq!(
4916                    frames.len(),
4917                    count,
4918                    "connection {} got {:?}",
4919                    connection,
4920                    frames
4921                );
4922                prop_assert_eq!(
4923                    rig.quiet(connection),
4924                    None,
4925                    "more than owed on {}",
4926                    connection
4927                );
4928
4929                let mut responses = 0;
4930                let mut answered = Vec::new();
4931                for frame in &frames {
4932                    match *frame {
4933                        Seen::App(_) => responses += 1,
4934                        Seen::Final(tag, id) => {
4935                            let owed = owed.remove(&id);
4936                            prop_assert_eq!(
4937                                owed,
4938                                Some(Owed {
4939                                    responses,
4940                                    final_tag: tag
4941                                }),
4942                                "request {:?} on {}: {:?}",
4943                                id,
4944                                connection,
4945                                frames
4946                            );
4947                            answered.push(id);
4948                            responses = 0;
4949                        }
4950                        Seen::Protocol(tag) => {
4951                            prop_assert!(false, "an unexpected frame {tag:#x} on {connection}")
4952                        }
4953                    }
4954                }
4955                prop_assert_eq!(
4956                    responses,
4957                    0,
4958                    "responses with no final frame on {}",
4959                    connection
4960                );
4961                let published: Vec<RequestId> =
4962                    answered.into_iter().filter(|id| id.0 <= wire_seq).collect();
4963                prop_assert_eq!(
4964                    published,
4965                    events.remove(&connection).unwrap_or_default(),
4966                    "replies to published events out of order on {}",
4967                    connection
4968                );
4969            }
4970            prop_assert!(owed.is_empty(), "never answered: {owed:?}");
4971            Ok(())
4972        }
4973
4974        /// The kernel stage under test, read through a socket per client.
4975        pub(crate) struct UringStage {
4976            control: mpsc::Sender<ControlEvent>,
4977            clients: HashMap<u64, UnixStream>,
4978            shutdown: Arc<AtomicBool>,
4979            handle: Option<JoinHandle<bool>>,
4980        }
4981
4982        pub(crate) fn start_uring(inputs: StageInputs) -> UringStage {
4983            let (control, control_rx) = mpsc::channel();
4984            let mut config = config(
4985                inputs.journal,
4986                Arc::new(FenceState::new(0)),
4987                inputs.early_replies,
4988                None,
4989            );
4990            config.policy.start = crate::response::GateStart::settled(AckPolicy::DiskAndRam);
4991            config.replication_metrics = Some(inputs.replication_metrics);
4992            config.replica_active = Some(inputs.replica_active);
4993            config.utilization = inputs.utilization;
4994            let shutdown = Arc::clone(&inputs.shutdown);
4995            let consumer = inputs.consumer;
4996            let handle =
4997                thread::spawn(move || run::<Counter>(consumer, control_rx, config, &shutdown));
4998            UringStage {
4999                control,
5000                clients: HashMap::new(),
5001                shutdown: inputs.shutdown,
5002                handle: Some(handle),
5003            }
5004        }
5005
5006        impl StageHarness for UringStage {
5007            fn connect(&mut self, connection_id: u64) {
5008                let (event, client) = connection(connection_id);
5009                self.control.send(event).expect("the stage is running");
5010                self.clients.insert(connection_id, client);
5011            }
5012
5013            fn next_frame(&mut self, connection_id: u64, within: Duration) -> Option<Seen> {
5014                let client = self.clients.get_mut(&connection_id)?;
5015                client
5016                    .set_read_timeout(Some(within))
5017                    .expect("set read timeout");
5018                read_frame(client).ok()
5019            }
5020        }
5021
5022        impl Drop for UringStage {
5023            fn drop(&mut self) {
5024                self.shutdown.store(true, Ordering::Relaxed);
5025                if let Some(handle) = self.handle.take() {
5026                    // Joined only to surface a panic; while unwinding
5027                    // from a failed assertion, another panic would abort.
5028                    if handle.join().is_err() && !thread::panicking() {
5029                        panic!("the stage panicked");
5030                    }
5031                }
5032            }
5033        }
5034
5035        #[test]
5036        fn an_early_reply_waits_out_an_event_split_across_reads_uring() {
5037            an_early_reply_waits_out_an_event_split_across_reads(start_uring);
5038        }
5039
5040        #[test]
5041        fn early_replies_overtake_a_held_write_uring() {
5042            for kind in every_kind() {
5043                early_replies_overtake_a_held_write(start_uring, kind);
5044            }
5045        }
5046
5047        #[test]
5048        fn a_gate_held_inside_an_event_sends_no_early_reply_uring() {
5049            a_gate_held_inside_an_event_sends_no_early_reply(start_uring);
5050        }
5051
5052        #[test]
5053        fn connection_first_seen_inside_a_gate_wait_uring() {
5054            connection_first_seen_inside_a_gate_wait(start_uring);
5055        }
5056
5057        proptest! {
5058            #![proptest_config(ProptestConfig::with_cases(24))]
5059            #[test]
5060            fn every_request_is_answered_once_and_whole_uring(script in script()) {
5061                every_request_is_answered_once_and_whole(start_uring, &script)?;
5062            }
5063        }
5064    }
5065
5066    /// A halted node past the grace period answers a held write once its
5067    /// own journal holds it, ending in `BatchEndDegraded`, and the
5068    /// refusals around it, which wait for nothing, end in a plain
5069    /// `BatchEnd`. With the switch off the held write waits; the refusals
5070    /// still do not.
5071    mod degraded_release {
5072        use std::sync::atomic::{AtomicBool, Ordering};
5073        use std::sync::{Arc, mpsc};
5074        use std::thread;
5075        use std::time::Duration;
5076
5077        use counter_server::{
5078            Counter, CounterQuery, CounterReport, KIND_RESP_ACK, KIND_RESP_REJECTED,
5079        };
5080        use melin_pipeline::ring::DisruptorBuilder;
5081        use melin_pipeline::wait::WaitStrategy;
5082        use melin_transport_core::fence::FenceState;
5083        use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
5084        use melin_transport_core::replica_presence::ReplicaPresence;
5085        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
5086        use melin_wire_protocol::control_codec::{TAG_BATCH_END, TAG_BATCH_END_DEGRADED};
5087
5088        use super::refusal_order::{Seen, config, connection, early_id, read_frame, refusal};
5089        use crate::ack_policy::AckPolicy;
5090        use crate::early_reply::early_reply_channel;
5091        use crate::halt::DegradedRelease;
5092        use crate::replication::ReplicationMetrics;
5093        use crate::response::run;
5094
5095        /// Run a stage under `disk+ram` on a primary whose one replica is
5096        /// gone, with degraded release `on` (no grace) or off, holding one
5097        /// write the primary's journal already holds, with two refusals
5098        /// queued. Returns what the client read before `quiet` passed with
5099        /// nothing more.
5100        fn run_halted(on: bool, quiet: Duration) -> Vec<Seen> {
5101            let (mut producer, mut consumers) =
5102                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
5103                    .add_consumer()
5104                    .build(WaitStrategy::SpinThenYield);
5105            let consumer = consumers.pop().expect("one consumer was requested");
5106            let (connected, mut client) = connection(1);
5107            let (mut refusals, refusal_rx) =
5108                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
5109
5110            let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(1));
5111            let mut config = config(
5112                journal_cursor,
5113                Arc::new(FenceState::new(0)),
5114                refusal_rx,
5115                None,
5116            );
5117            config.policy = crate::response::GatePolicy {
5118                start: crate::response::GateStart::settled(AckPolicy::DiskAndRam),
5119                forced: melin_transport_core::lineage::forced_policy_cell().1,
5120            };
5121            config.replication_metrics = Some(Arc::new(ReplicationMetrics::default()));
5122            config.replica_active = Some([
5123                Arc::new(AtomicBool::new(false)),
5124                Arc::new(AtomicBool::new(false)),
5125            ]);
5126            // No replica ever streaming: halted from the start.
5127            config.degraded_release = on.then(|| DegradedRelease {
5128                presence: Arc::new(ReplicaPresence::new()),
5129                grace: Duration::ZERO,
5130            });
5131
5132            let (control_tx, control_rx) = mpsc::channel();
5133            control_tx.send(connected).expect("channel open");
5134            refusals.try_send(refusal(1, 0)).expect("queue has room");
5135            refusals.try_send(refusal(1, 1)).expect("queue has room");
5136            refusals.flush();
5137            producer.publish(OutputSlot {
5138                connection_id: 1,
5139                request_id: HELD,
5140                input_seq: 0,
5141                wire_seq: 1,
5142                payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
5143                is_last_in_request: true,
5144                ..Default::default()
5145            });
5146
5147            let shutdown = AtomicBool::new(false);
5148            let mut seen = Vec::new();
5149            thread::scope(|scope| {
5150                let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
5151                client
5152                    .set_read_timeout(Some(quiet))
5153                    .expect("set read timeout");
5154                while let Ok(frame) = read_frame(&mut client) {
5155                    seen.push(frame);
5156                }
5157                shutdown.store(true, Ordering::Relaxed);
5158                stage.join().expect("stage panicked");
5159            });
5160            seen
5161        }
5162
5163        /// The held write's ID.
5164        const HELD: RequestId = RequestId(7);
5165
5166        #[test]
5167        fn a_held_write_is_answered_degraded_and_refusals_end_in_a_plain_batch_end() {
5168            let seen = run_halted(true, Duration::from_millis(500));
5169            assert_eq!(
5170                seen,
5171                [
5172                    Seen::App(KIND_RESP_REJECTED),
5173                    Seen::Final(TAG_BATCH_END, early_id(0)),
5174                    Seen::App(KIND_RESP_REJECTED),
5175                    Seen::Final(TAG_BATCH_END, early_id(1)),
5176                    Seen::App(KIND_RESP_ACK),
5177                    Seen::Final(TAG_BATCH_END_DEGRADED, HELD),
5178                ],
5179                "the refusals first, waiting for nothing; only the held write marked"
5180            );
5181        }
5182
5183        #[test]
5184        fn with_the_switch_off_the_held_write_waits_and_the_refusals_do_not() {
5185            let seen = run_halted(false, Duration::from_millis(300));
5186            assert_eq!(
5187                seen,
5188                [
5189                    Seen::App(KIND_RESP_REJECTED),
5190                    Seen::Final(TAG_BATCH_END, early_id(0)),
5191                    Seen::App(KIND_RESP_REJECTED),
5192                    Seen::Final(TAG_BATCH_END, early_id(1)),
5193                ],
5194                "both refusals go out; the held write does not"
5195            );
5196        }
5197    }
5198
5199    /// A node stopped while a reply is held by the ack policy. The gate
5200    /// may never open — every replica gone under a policy that needs one —
5201    /// and the shutdown sequence joins the stage without a timeout, so the
5202    /// stage must leave the gate on shutdown. It leaves without sending
5203    /// the held reply, which the policy never confirmed. A fence co-sets
5204    /// shutdown and takes the same exit.
5205    mod stop_while_gated {
5206        use std::sync::atomic::{AtomicBool, Ordering};
5207        use std::sync::{Arc, mpsc};
5208        use std::thread;
5209        use std::time::{Duration, Instant};
5210
5211        use counter_server::{Counter, CounterQuery, CounterReport, KIND_RESP_ACK};
5212        use melin_pipeline::ring::DisruptorBuilder;
5213        use melin_pipeline::wait::WaitStrategy;
5214        use melin_transport_core::fence::FenceState;
5215        use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
5216        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
5217        use melin_wire_protocol::control_codec::TAG_BATCH_END;
5218
5219        use super::refusal_order::{Seen, config, connection, read_frame};
5220        use crate::early_reply::early_reply_channel;
5221        use crate::response::run;
5222
5223        /// How long the stage gets to exit once told to; generous for a
5224        /// loaded box.
5225        const EXIT_TIMEOUT: Duration = Duration::from_secs(10);
5226
5227        fn stop_while_gated(fenced: bool) {
5228            let (mut producer, mut consumers) =
5229                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
5230                    .add_consumer()
5231                    .build(WaitStrategy::SpinThenYield);
5232            let consumer = consumers.pop().expect("one consumer was requested");
5233            let (connected, mut client) = connection(1);
5234            let (control_tx, control_rx) = mpsc::channel();
5235            control_tx.send(connected).expect("channel open");
5236
5237            // The journal confirms the first event and never the second.
5238            let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(1));
5239            let fence = Arc::new(FenceState::new(0));
5240            let early_replies =
5241                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
5242            let config = config(journal_cursor, Arc::clone(&fence), early_replies, None);
5243            for wire_seq in [1, 2] {
5244                producer.publish(OutputSlot {
5245                    connection_id: 1,
5246                    request_id: RequestId(wire_seq),
5247                    input_seq: wire_seq - 1,
5248                    wire_seq,
5249                    payload: OutputPayload::Report(CounterReport::Ack {
5250                        new_value: wire_seq,
5251                    }),
5252                    is_last_in_request: true,
5253                    ..Default::default()
5254                });
5255            }
5256
5257            // Not a scoped thread: a stage that never leaves the gate
5258            // would hang a scoped join, and that hang is the failure this
5259            // test exists to catch. The flag is leaked for the same
5260            // reason — the thread may outlive the test, and dies with the
5261            // process.
5262            let shutdown: &'static AtomicBool = Box::leak(Box::new(AtomicBool::new(false)));
5263            let stage =
5264                thread::spawn(move || run::<Counter>(consumer, control_rx, config, shutdown));
5265
5266            // The confirmed reply goes out before the stage blocks on the
5267            // gate.
5268            let mut seen = Vec::new();
5269            for _ in 0..2 {
5270                seen.push(read_frame(&mut client).expect("the confirmed reply arrives"));
5271            }
5272            assert_eq!(
5273                seen,
5274                [
5275                    Seen::App(KIND_RESP_ACK),
5276                    Seen::Final(TAG_BATCH_END, RequestId(1))
5277                ]
5278            );
5279
5280            if fenced {
5281                assert!(fence.fence(), "first latch");
5282            }
5283            shutdown.store(true, Ordering::Relaxed);
5284            let deadline = Instant::now() + EXIT_TIMEOUT;
5285            while !stage.is_finished() {
5286                assert!(
5287                    Instant::now() < deadline,
5288                    "the stage did not leave the gate on shutdown"
5289                );
5290                thread::sleep(Duration::from_millis(10));
5291            }
5292            stage.join().expect("stage panicked");
5293
5294            // The held reply never went out: the stage closed the
5295            // connection with nothing more on it.
5296            assert!(
5297                read_frame(&mut client).is_err(),
5298                "a reply the policy never confirmed was sent"
5299            );
5300        }
5301
5302        #[test]
5303        fn a_stopped_stage_leaves_the_gate_and_drops_the_held_reply() {
5304            stop_while_gated(false);
5305        }
5306
5307        #[test]
5308        fn a_fenced_stage_leaves_the_gate_and_drops_the_held_reply() {
5309            stop_while_gated(true);
5310        }
5311    }
5312
5313    /// A journaled policy change reaches the running stage as a slot that
5314    /// sends nothing, and a forced change frees a held reply only once its
5315    /// own entry is on disk. The switch itself is pinned by the gate's
5316    /// tests (`durability_gate.rs`), the in-force gauge by
5317    /// `tests/ack_policy_changes.rs`.
5318    mod policy_change {
5319        use std::io::ErrorKind;
5320        use std::os::unix::io::AsRawFd;
5321        use std::os::unix::net::UnixStream;
5322        use std::sync::atomic::{AtomicBool, Ordering};
5323        use std::sync::{Arc, mpsc};
5324        use std::thread;
5325        use std::time::{Duration, Instant};
5326
5327        use counter_server::{Counter, CounterQuery, CounterReport, KIND_RESP_ACK};
5328        use melin_journal::AckPolicyChange;
5329        use melin_pipeline::ring::DisruptorBuilder;
5330        use melin_pipeline::wait::WaitStrategy;
5331        use melin_transport_core::fence::FenceState;
5332        use melin_transport_core::lineage::{ForcedPolicy, forced_policy_cell};
5333        use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
5334        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
5335        use melin_wire_protocol::control_codec::TAG_BATCH_END;
5336
5337        use super::refusal_order::{Seen, config, connection, read_frame};
5338        use crate::ack_policy::AckPolicy;
5339        use crate::early_reply::early_reply_channel;
5340        use crate::response::run;
5341
5342        /// How long the test waits for the stage to reach a state.
5343        const SETTLE_TIMEOUT: Duration = Duration::from_secs(10);
5344
5345        /// How long a held reply is watched for: long enough that a stage
5346        /// wrongly releasing it would have sent it.
5347        const HOLD: Duration = Duration::from_millis(50);
5348
5349        fn reply(wire_seq: u64) -> OutputSlot<CounterReport, CounterQuery> {
5350            OutputSlot {
5351                connection_id: 1,
5352                request_id: RequestId(wire_seq),
5353                input_seq: wire_seq - 1,
5354                wire_seq,
5355                payload: OutputPayload::Report(CounterReport::Ack {
5356                    new_value: wire_seq,
5357                }),
5358                is_last_in_request: true,
5359                ..Default::default()
5360            }
5361        }
5362
5363        /// No frame waiting on the client's socket.
5364        fn nothing_sent(client: &UnixStream) -> bool {
5365            let mut byte = 0u8;
5366            // Safety: a live socket fd and a one-byte buffer; MSG_PEEK
5367            // leaves anything there for the reads that follow.
5368            let n = unsafe {
5369                libc::recv(
5370                    client.as_raw_fd(),
5371                    (&raw mut byte).cast(),
5372                    1,
5373                    libc::MSG_PEEK | libc::MSG_DONTWAIT,
5374                )
5375            };
5376            n < 0 && std::io::Error::last_os_error().kind() == ErrorKind::WouldBlock
5377        }
5378
5379        #[test]
5380        fn a_change_slot_sends_nothing_and_a_forced_one_waits_for_its_entry() {
5381            let (mut producer, mut consumers) =
5382                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
5383                    .add_consumer()
5384                    .build(WaitStrategy::SpinThenYield);
5385            let consumer = consumers.pop().expect("one consumer was requested");
5386            let (connected, mut client) = connection(1);
5387            let (control_tx, control_rx) = mpsc::channel();
5388            control_tx.send(connected).expect("channel open");
5389
5390            // Everything up to the reply after the change is on disk; a
5391            // standalone stage cannot satisfy `two-disks` at all.
5392            let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(3));
5393            let early_replies =
5394                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
5395            let mut config = config(
5396                journal_cursor.clone(),
5397                Arc::new(FenceState::new(0)),
5398                early_replies,
5399                None,
5400            );
5401            let (mut forced_writer, forced) = forced_policy_cell();
5402            config.policy.forced = forced;
5403            let utilization = Arc::clone(&config.utilization);
5404
5405            producer.publish(reply(1));
5406            producer.publish(OutputSlot {
5407                // Addressed to the client's own connection, which the
5408                // matching stage never does: the change still sends it
5409                // nothing, not even a batch end.
5410                connection_id: 1,
5411                input_seq: 1,
5412                wire_seq: 2,
5413                payload: OutputPayload::AckPolicyChange(AckPolicyChange {
5414                    policy: AckPolicy::TwoDisks.to_byte(),
5415                    unwitnessed: false,
5416                }),
5417                is_last_in_request: true,
5418                ..Default::default()
5419            });
5420            producer.publish(reply(3));
5421
5422            let shutdown = AtomicBool::new(false);
5423            thread::scope(|scope| {
5424                let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
5425
5426                // The reply before the change goes out; once the stage has
5427                // taken the change, it has sent nothing for it (and the
5428                // reply after it is held by `two-disks`).
5429                let before = [read_frame(&mut client), read_frame(&mut client)];
5430                let deadline = Instant::now() + SETTLE_TIMEOUT;
5431                while utilization.ack_policy_in_force.load() != Some(AckPolicy::TwoDisks.to_byte())
5432                    && Instant::now() < deadline
5433                {
5434                    thread::sleep(Duration::from_millis(1));
5435                }
5436                thread::sleep(HOLD);
5437                let the_change_sent_nothing = nothing_sent(&client);
5438
5439                // A forced swap back to `disk`, not yet on disk itself.
5440                forced_writer.store(ForcedPolicy {
5441                    wire_seq: WireSeq::new(4),
5442                    policy: AckPolicy::Disk.to_byte(),
5443                });
5444                thread::sleep(HOLD);
5445                let held_until_the_entry_is_on_disk = nothing_sent(&client);
5446                journal_cursor.store(WireSeq::new(4));
5447                let after = [read_frame(&mut client), read_frame(&mut client)];
5448
5449                shutdown.store(true, Ordering::Relaxed);
5450                stage.join().expect("stage panicked");
5451
5452                for (frames, id) in [(before, 1), (after, 3)] {
5453                    let frames = frames.map(|frame| frame.expect("a reply arrived"));
5454                    assert_eq!(
5455                        frames,
5456                        [
5457                            Seen::App(KIND_RESP_ACK),
5458                            Seen::Final(TAG_BATCH_END, RequestId(id))
5459                        ]
5460                    );
5461                }
5462                assert!(
5463                    the_change_sent_nothing,
5464                    "a frame went out for the change slot, or the reply it holds"
5465                );
5466                assert!(
5467                    held_until_the_entry_is_on_disk,
5468                    "the forced swap applied before its entry was on disk"
5469                );
5470            });
5471        }
5472    }
5473
5474    /// The stage reports whether it holds its io_uring instance before it
5475    /// runs, so the node can refuse to start instead of losing the stage
5476    /// to a panic on a thread nobody is watching.
5477    mod startup_report {
5478        use std::sync::atomic::AtomicBool;
5479        use std::sync::{Arc, mpsc};
5480
5481        use counter_server::{Counter, CounterQuery, CounterReport};
5482        use melin_pipeline::ring::DisruptorBuilder;
5483        use melin_pipeline::wait::WaitStrategy;
5484        use melin_transport_core::fence::FenceState;
5485        use melin_transport_core::pipeline::OutputSlot;
5486        use melin_transport_core::{DurableWireSeqCursor, WireSeq};
5487
5488        use super::refusal_order::config;
5489        use crate::connection_limit::RingSizing;
5490        use crate::early_reply::early_reply_channel;
5491        use crate::response::run;
5492
5493        /// A zero-entry ring stands in for the locked-memory limit: both
5494        /// fail `io_uring_setup`. The stage reports the error and returns
5495        /// without running — `run` returning at all is the proof, since a
5496        /// running stage only stops on shutdown, which is never set.
5497        #[test]
5498        fn a_ring_that_cannot_be_created_is_reported_and_the_stage_returns() {
5499            let (_producer, mut consumers) =
5500                DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
5501                    .add_consumer()
5502                    .build(WaitStrategy::SpinThenYield);
5503            let consumer = consumers.pop().expect("one consumer was requested");
5504            let (_control_tx, control_rx) = mpsc::channel();
5505            let early_replies =
5506                early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
5507            let (ready_tx, ready_rx) = mpsc::sync_channel(1);
5508            let mut config = config(
5509                DurableWireSeqCursor::detached(WireSeq::new(0)),
5510                Arc::new(FenceState::new(0)),
5511                early_replies,
5512                None,
5513            );
5514            config.ring_sizing = RingSizing::unchecked(0, 32);
5515            config.ready = Some(ready_tx);
5516
5517            let started = run::<Counter>(consumer, control_rx, config, &AtomicBool::new(false));
5518            assert!(!started, "the stage reports that it never ran");
5519
5520            let err = ready_rx
5521                .recv()
5522                .expect("the stage reports")
5523                .expect_err("a zero-entry ring cannot be created");
5524            let msg = err.to_string();
5525            assert!(msg.contains("response"), "{msg}");
5526            assert!(msg.contains("io_uring"), "{msg}");
5527        }
5528    }
5529}