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