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