Skip to main content

melin_server_runtime/replication/
tcp_sender.rs

1//! TCP replication sender (primary side).
2//!
3//! Listens for replica connections, handles authentication, journal
4//! catch-up, snapshot transfer, and live streaming. The wire protocol
5//! is defined in `super::protocol`.
6
7use std::io::{self, Write};
8use std::net::{TcpListener, TcpStream};
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
11
12use tracing::{debug, error, info, warn};
13
14use melin_journal::replication::ReplicationConsumer;
15
16use super::auth::authenticate_replica;
17use super::{ReplicaCursors, ReplicaGate, ReplicationMetrics, SentHighWater};
18use crate::uring_teardown::{DrainBackoff, wake_pending_ops};
19use melin_app::Application;
20use melin_transport_core::replication::catchup::{
21    CatchUpResult, bridge_catchup_to_live, can_catch_up_from_journal, catch_up_from_journal,
22    preflight_snapshot_transfer, snapshot_transfer_with,
23};
24use melin_transport_core::replication::protocol::{
25    MAX_CONTROL_FRAME, ReplicaMessage, decode_replica_message, encode_hash_mismatch,
26    encode_heartbeat, encode_need_snapshot, encode_stream_start, read_frame,
27};
28use melin_transport_core::replication::validate::{
29    HandshakeValidation, validate_replica_handshake_settled,
30};
31
32// --- Replication Sender (Primary side) ---
33
34/// Owned state for the replication sender thread.
35/// A TCP listener guaranteed non-blocking: the only constructor sets
36/// the flag, so the guarantee is carried by the type. [`run_sender`]'s
37/// accept loop polls the shutdown flag between accepts — handed a
38/// blocking listener it would park inside `accept()` and only observe
39/// shutdown when the next replica happened to dial in. This newtype
40/// makes that state unrepresentable instead of documenting it away.
41pub struct ReplicationListener(TcpListener);
42
43impl ReplicationListener {
44    /// Wrap a bound listener, setting it non-blocking.
45    pub fn new(listener: TcpListener) -> std::io::Result<Self> {
46        listener.set_nonblocking(true)?;
47        Ok(Self(listener))
48    }
49}
50
51pub struct Sender {
52    /// Pre-bound replication listener, non-blocking by construction.
53    /// Bound by the caller at startup — not here — so a bad or stolen
54    /// `--replication-bind` port fails the boot with a clear error.
55    /// Binding on this thread meant a failure only killed the sender
56    /// silently, leaving a primary that looked healthy but could never
57    /// satisfy hybrid/durably-replicated acks.
58    pub listener: ReplicationListener,
59    pub repl_consumer_1: ReplicationConsumer,
60    pub repl_consumer_2: ReplicationConsumer,
61    pub replica_slots: Arc<melin_transport_core::ReplicaSlotCursors>,
62    pub journal_path: std::path::PathBuf,
63    pub authorized_keys: Arc<melin_app::auth::AuthorizedKeys>,
64    pub evict_flags: [Arc<AtomicBool>; 2],
65    pub active_flags: [Arc<AtomicBool>; 2],
66    pub metrics: Arc<ReplicationMetrics>,
67    pub handler_cores: [usize; 2],
68    pub batch_size: usize,
69    pub heartbeat_secs: u64,
70    pub busy_spin: bool,
71    /// Node fencing state. Read to stamp the primary's epoch onto each
72    /// `StreamStart`, and to self-demote when a replica handshakes with a
73    /// higher epoch (this primary has been superseded). See `crate::fence`.
74    pub fence_state: Arc<melin_transport_core::fence::FenceState>,
75    /// The active durability (acking) mode, shared with the response
76    /// gate and the admin `DURABILITY` command. Stamped on every
77    /// `StreamStart` and `Heartbeat` so replicas judge auto-promotion
78    /// against the mode this primary actually acks under.
79    pub durability_mode: Arc<std::sync::atomic::AtomicU8>,
80}
81
82/// Run the replication sender. Listens for replica connections,
83/// streams journal data batches, processes acks, and updates the
84/// replication cursor.
85///
86/// Runs on a dedicated thread. Blocks until shutdown.
87pub fn run_sender<A: Application>(
88    config: Sender,
89    shutdown: &AtomicBool,
90    replica_ready: &AtomicBool,
91    replicas_connected: &AtomicU32,
92) {
93    let Sender {
94        listener,
95        repl_consumer_1,
96        repl_consumer_2,
97        replica_slots,
98        journal_path,
99        authorized_keys,
100        evict_flags,
101        active_flags,
102        metrics,
103        handler_cores,
104        batch_size,
105        heartbeat_secs,
106        busy_spin,
107        fence_state,
108        durability_mode,
109    } = config;
110    // Unwrap the invariant-carrying newtype — from here `listener` is a
111    // plain TcpListener, known non-blocking.
112    let ReplicationListener(listener) = listener;
113    match listener.local_addr() {
114        Ok(addr) => info!(%addr, "replication sender listening"),
115        // Best-effort log detail — the pre-bound listener works either way.
116        Err(_) => info!("replication sender listening"),
117    }
118
119    // Two replica slots, each with its own ring consumer and thread handle.
120    // The accept loop fills empty slots. When a replica disconnects, its
121    // slot becomes available for a new connection. The consumer is `None`
122    // while a handler thread owns it (moved into the thread, returned on exit).
123    struct ReplicaSlot {
124        consumer: Option<ReplicationConsumer>,
125        handle: Option<std::thread::JoinHandle<ReplicationConsumer>>,
126    }
127
128    let mut slots = [
129        ReplicaSlot {
130            consumer: Some(repl_consumer_1),
131            handle: None,
132        },
133        ReplicaSlot {
134            consumer: Some(repl_consumer_2),
135            handle: None,
136        },
137    ];
138
139    // Single owner of the per-replica progress cursors (per-slot acked
140    // positions and the gate's gauge pair). Shared by both handler
141    // threads — see `ReplicaCursors` for the ordering contract relative
142    // to the active flags.
143    let cursors = Arc::new(ReplicaCursors::new(replica_slots, Arc::clone(&metrics)));
144
145    // Per-slot "this connection authenticated" latch. The trading-halt gate
146    // (`replicas_connected`) is lifted by the handler thread only after auth
147    // succeeds, so the main loop must lower it on teardown only when the
148    // handler got that far — a connection that drops mid-auth never counted.
149    // Per-slot `Arc<AtomicBool>` mirrors `active_flags`/`evict_flags`; shared
150    // with the handler thread, which flips it true post-auth.
151    let authenticated_flags: [Arc<AtomicBool>; 2] = [
152        Arc::new(AtomicBool::new(false)),
153        Arc::new(AtomicBool::new(false)),
154    ];
155
156    loop {
157        if shutdown.load(Ordering::Relaxed) {
158            info!("replication sender shutting down");
159            // Wait for active replica threads to finish.
160            for slot in &mut slots {
161                if let Some(handle) = slot.handle.take()
162                    && let Ok(consumer) = handle.join()
163                {
164                    slot.consumer = Some(consumer);
165                }
166            }
167            return;
168        }
169
170        // Check eviction flags from the journal stage. When set, the
171        // journal stage timed out publishing to this slot's ring. We need
172        // to reclaim the consumer so the idle drain loop can clear the ring,
173        // allowing the journal stage to resume publishing.
174        for (i, slot) in slots.iter_mut().enumerate() {
175            if evict_flags[i].load(Ordering::Acquire) && slot.handle.is_some() {
176                metrics.evictions_total.fetch_add(1, Ordering::Relaxed);
177                warn!(
178                    slot = i,
179                    "evicting slow replica (ring backpressure timeout)"
180                );
181                // The handler thread checks shutdown — we can't signal it
182                // individually without adding per-slot flags. Instead, join
183                // the thread with a short timeout by checking is_finished.
184                // The handler's TCP read timeout (5s) will cause it to exit
185                // on the next iteration when it checks shutdown. But we
186                // want faster eviction, so we shutdown the TCP stream to
187                // unblock the read.
188                //
189                // For now, mark the slot and let it be collected below
190                // when the handler finishes naturally (TCP timeout or
191                // next send failure after the ring stops being fed).
192            }
193        }
194
195        // Disengage a finished slot's shared state. Both the clean-exit and
196        // panic arms below call this, so they cannot drift: a panicked handler
197        // must tear down exactly like a clean exit, or it leaks the trading-halt
198        // gate and — far worse — leaves the slot's progress cursors engaged,
199        // so the primary stops acking client requests even with a healthy
200        // surviving replica. Borrows only atomics (interior
201        // mutability), so it coexists with the `&mut` iteration over `slots`.
202        let disengage_slot = |slot_idx: usize| {
203            // Lower the gate only if this connection authenticated (and so
204            // lifted it); `swap` reads and resets the latch in one op, and the
205            // `join()` ordered the handler's post-auth writes before here.
206            // `lower` warns "trading halted" if this was the last replica.
207            if authenticated_flags[slot_idx].swap(false, Ordering::AcqRel) {
208                ReplicaGate::new(replicas_connected).lower();
209            }
210            // Disengage cursors BEFORE clearing the active flag — ordering
211            // contract B2 (see `ReplicaCursors`).
212            cursors.clear_on_disconnect(slot_idx);
213            metrics.catching_up[slot_idx].store(false, Ordering::Relaxed);
214            // Journal stage stops publishing to this ring.
215            active_flags[slot_idx].store(false, Ordering::Release);
216        };
217
218        // Collect finished replica threads (disconnected replicas).
219        for (i, slot) in slots.iter_mut().enumerate() {
220            if let Some(ref handle) = slot.handle
221                && handle.is_finished()
222            {
223                let handle = slot.handle.take().expect("just checked is_some");
224                match handle.join() {
225                    Ok(mut consumer) => {
226                        // Drop any unread entries before the consumer is
227                        // stashed back in the slot. The journal stage may
228                        // have published batches into the ring that the
229                        // evicted handler never got to forward — if we
230                        // left them in place, the NEXT handler on this
231                        // slot would drain them to its replica and
232                        // acknowledge with pre-eviction sequences. Those
233                        // acks would stall the slot's acked-progress
234                        // cursor at the old position and gate the
235                        // primary's response stage at the slow-replica
236                        // rate.
237                        //
238                        // Fast-forward to the producer cursor so the
239                        // live-streaming loop starts with a clean ring.
240                        consumer.skip_to_producer();
241                        slot.consumer = Some(consumer);
242                        disengage_slot(i);
243                        // Clear eviction flag after reclaiming the consumer.
244                        if evict_flags[i].load(Ordering::Relaxed) {
245                            evict_flags[i].store(false, Ordering::Release);
246                            warn!(slot = i, "evicted replica — ring ready for reconnection");
247                        } else {
248                            warn!(slot = i, "replica disconnected");
249                        }
250                    }
251                    Err(_) => {
252                        error!(slot = i, "replica handler thread panicked");
253                        // Consumer is lost — the slot stays handle=None /
254                        // consumer=None, so the accept loop skips it. With
255                        // independent rings, only this slot's ring is affected;
256                        // the other replica continues normally. Still disengage
257                        // its shared state, or the gate leaks and the cursor
258                        // freezes (see `disengage_slot`).
259                        disengage_slot(i);
260                        evict_flags[i].store(false, Ordering::Release);
261                    }
262                }
263            }
264        }
265
266        // Find a slot that has a consumer available (not in use by a thread).
267        let empty_slot = slots
268            .iter()
269            .position(|s| s.handle.is_none() && s.consumer.is_some());
270
271        // Accept a connection if there's an empty slot.
272        if let Some(slot_idx) = empty_slot {
273            match listener.accept() {
274                Ok((stream, addr)) => {
275                    info!(addr = %addr, slot = slot_idx, "replica connected");
276                    if let Err(e) = stream.set_nodelay(true) {
277                        debug!(error = %e, "failed to set TCP_NODELAY on replica connection");
278                    }
279                    // SO_BUSY_POLL on the sender side: the per-replica thread
280                    // spins on `recv` for ack frames, so kernel busy-polling
281                    // removes the softirq->wakeup handoff from the ack path.
282                    if let Err(e) =
283                        crate::server::set_busy_poll(&stream, crate::server::BUSY_POLL_US)
284                    {
285                        debug!(error = %e, "failed to set SO_BUSY_POLL on replica connection");
286                    }
287
288                    // The trading-halt gate is NOT lifted here — a bare connect
289                    // hasn't proven a Replication key. The handler lifts it after
290                    // auth succeeds (see `handle_replica_connection`).
291
292                    // Take the consumer out of the slot for the handler thread.
293                    // The slot's consumer becomes None while the thread owns it.
294                    let consumer = slots[slot_idx]
295                        .consumer
296                        .take()
297                        .expect("empty_slot check guarantees consumer is Some");
298
299                    let slot_cursors = Arc::clone(&cursors);
300                    let jpath = journal_path.clone();
301                    let auth_keys = Arc::clone(&authorized_keys);
302                    let slot_metrics = Arc::clone(&metrics);
303                    let slot_active = Arc::clone(&active_flags[slot_idx]);
304                    let slot_evict = Arc::clone(&evict_flags[slot_idx]);
305                    let slot_fence = Arc::clone(&fence_state);
306                    let slot_durability = Arc::clone(&durability_mode);
307                    let slot_authenticated = Arc::clone(&authenticated_flags[slot_idx]);
308                    let handler_core = handler_cores[slot_idx];
309                    let shutdown_flag = shutdown as *const AtomicBool as usize;
310                    let ready_flag = replica_ready as *const AtomicBool as usize;
311                    let connected_flag = replicas_connected as *const AtomicU32 as usize;
312                    let handle = std::thread::Builder::new()
313                        .name(format!("repl-{slot_idx}"))
314                        .spawn(move || {
315                            // Pin to a dedicated core if configured (> 0),
316                            // otherwise clear inherited affinity from the
317                            // sender thread so the OS can schedule freely.
318                            if handler_core > 0 {
319                                match melin_app::affinity::pin_to_core(handler_core) {
320                                    Ok(c) => tracing::info!(
321                                        core = c,
322                                        slot = slot_idx,
323                                        "replication handler pinned to core"
324                                    ),
325                                    Err(e) => tracing::warn!(
326                                        error = e,
327                                        slot = slot_idx,
328                                        "failed to pin handler"
329                                    ),
330                                }
331                            } else if let Err(e) = melin_app::affinity::clear_affinity() {
332                                tracing::warn!(error = e, "failed to clear handler affinity");
333                            }
334                            // Safety: shutdown and replica_ready outlive this thread
335                            // (they're on the parent's stack, which blocks on join
336                            // during shutdown).
337                            let shutdown_ref = unsafe { &*(shutdown_flag as *const AtomicBool) };
338                            let ready_ref = unsafe { &*(ready_flag as *const AtomicBool) };
339                            // Safety: replicas_connected outlives this thread —
340                            // same lifetime argument as shutdown/replica_ready
341                            // above (the parent blocks on join during shutdown).
342                            let connected_ref = unsafe { &*(connected_flag as *const AtomicU32) };
343                            let ctx = SlotContext {
344                                cursors: &slot_cursors,
345                                journal_path: &jpath,
346                                authorized_keys: &auth_keys,
347                                shutdown: shutdown_ref,
348                                replica_ready: ready_ref,
349                                active_flag: &slot_active,
350                                evict_flag: &slot_evict,
351                                metrics: &slot_metrics,
352                                fence_state: &slot_fence,
353                                durability_mode: &slot_durability,
354                                slot_idx,
355                                batch_size,
356                                heartbeat_secs,
357                                busy_spin,
358                                replicas_connected: connected_ref,
359                                authenticated: &slot_authenticated,
360                            };
361                            run_replica_slot::<A>(stream, consumer, &ctx)
362                        })
363                        .expect("spawn replica handler thread");
364                    slots[slot_idx].handle = Some(handle);
365                }
366                Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
367                    // No pending connection.
368                }
369                Err(e) => {
370                    error!(error = %e, "replication accept error");
371                }
372            }
373        }
374
375        // No idle drain needed — the journal stage only publishes to
376        // rings where active_flag is true (set by handler on live loop
377        // entry, cleared on disconnect). Idle consumers stay empty.
378
379        std::thread::sleep(std::time::Duration::from_millis(50));
380    }
381}
382
383/// Per-slot state shared across the replica handler call chain
384/// (`run_replica_slot` → `handle_replica_connection` → `live_stream_uring`).
385struct SlotContext<'a> {
386    cursors: &'a Arc<ReplicaCursors>,
387    journal_path: &'a std::path::Path,
388    authorized_keys: &'a melin_app::auth::AuthorizedKeys,
389    shutdown: &'a AtomicBool,
390    replica_ready: &'a AtomicBool,
391    active_flag: &'a AtomicBool,
392    evict_flag: &'a AtomicBool,
393    metrics: &'a ReplicationMetrics,
394    fence_state: &'a melin_transport_core::fence::FenceState,
395    /// The primary's active acking mode — stamped on `StreamStart` and
396    /// `Heartbeat` (see `Sender::durability_mode`).
397    durability_mode: &'a std::sync::atomic::AtomicU8,
398    slot_idx: usize,
399    batch_size: usize,
400    heartbeat_secs: u64,
401    busy_spin: bool,
402    /// Trading-halt gate. The handler lifts it (`fetch_add`) only after auth
403    /// succeeds; the main loop lowers it on teardown, gated by `authenticated`.
404    replicas_connected: &'a AtomicU32,
405    /// Per-slot latch the handler sets true once this connection passes auth,
406    /// so the main loop knows whether this slot lifted the gate.
407    authenticated: &'a AtomicBool,
408}
409
410/// Handle a single replica connection on a dedicated thread.
411/// Returns the consumer when the connection ends (for slot reuse).
412fn run_replica_slot<A: Application>(
413    stream: TcpStream,
414    mut consumer: ReplicationConsumer,
415    ctx: &SlotContext<'_>,
416) -> ReplicationConsumer {
417    match handle_replica_connection::<A>(stream, &mut consumer, ctx) {
418        Ok(()) => info!("replica disconnected cleanly"),
419        Err(e) => warn!(error = %e, "replica connection error"),
420    }
421    consumer
422}
423
424fn handle_replica_connection<A: Application>(
425    stream: TcpStream,
426    repl_consumer: &mut ReplicationConsumer,
427    ctx: &SlotContext<'_>,
428) -> io::Result<()> {
429    let SlotContext {
430        cursors,
431        journal_path,
432        authorized_keys,
433        shutdown,
434        replica_ready,
435        active_flag,
436        evict_flag: _,
437        metrics,
438        fence_state,
439        durability_mode,
440        slot_idx,
441        batch_size: _,
442        heartbeat_secs,
443        busy_spin: _,
444        replicas_connected,
445        authenticated,
446    } = ctx;
447    let slot_idx = *slot_idx;
448    let heartbeat_secs = *heartbeat_secs;
449
450    let mut reader = stream.try_clone()?;
451    let mut writer = stream;
452
453    // Set a read timeout for the handshake and auth.
454    reader.set_read_timeout(Some(std::time::Duration::from_secs(10)))?;
455
456    // Authenticate before any data exchange. `reader` and `writer` are clones
457    // of the same socket; auth is sequential (write challenge, read response,
458    // write result) so a single handle suffices. Use `reader` — it carries the
459    // read timeout set above.
460    authenticate_replica(&mut reader, authorized_keys)?;
461    info!("replica authenticated");
462
463    // Auth passed — only now lift the trading-halt gate. The main loop lowers
464    // it on slot teardown, reading `authenticated` to know this connection
465    // lifted it. Set the latch after the increment so a post-join read of the
466    // latch implies the increment is visible too.
467    ReplicaGate::new(replicas_connected).lift();
468    authenticated.store(true, Ordering::Release);
469
470    // Read handshake.
471    let handshake_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
472    let handshake = match decode_replica_message(&handshake_frame)? {
473        ReplicaMessage::Handshake(h) => h,
474        ReplicaMessage::Ack(_) => {
475            return Err(io::Error::other("expected Handshake, got Ack"));
476        }
477    };
478
479    info!(
480        last_sequence = handshake.last_sequence,
481        replica_epoch = handshake.epoch,
482        "replica handshake received"
483    );
484
485    // Fence: a replica advertising an epoch higher than ours means a
486    // promotion happened that this node missed — we are a stale ex-primary.
487    // The policy (latch fenced + trigger shutdown) lives on `FenceState` so
488    // the kernel-TCP and DPDK senders cannot drift; the matching stage
489    // halts new client writes, the response gate stops acking, and the node
490    // winds down for the operator to restart it as a replica. Refuse this
491    // connection so we send no stale stream.
492    let our_epoch = fence_state.epoch();
493    if let Some(first_latch) = fence_state.fence_if_superseded(handshake.epoch, shutdown) {
494        if first_latch {
495            error!(
496                replica_epoch = handshake.epoch,
497                our_epoch,
498                "fenced: a replica advertises a higher epoch — this primary has been \
499                 superseded; self-demoting and shutting down"
500            );
501        }
502        return Err(io::Error::other("fenced by higher-epoch replica"));
503    }
504
505    // Mark this slot as catching up. Cleared when entering the live loop.
506    metrics.catching_up[slot_idx].store(true, Ordering::Relaxed);
507
508    let mut send_buf = Vec::with_capacity(128);
509
510    // Chain validation: recompute our chain at the replica's claimed
511    // position and compare with the hash it reported. A mismatch (or a
512    // position beyond our tip) means its journal holds divergent
513    // history — streaming on top would silently fork the audit trail.
514    // Send HashMismatch (the replica archives its local journal on
515    // receipt) and route it through snapshot resync on this same
516    // connection.
517    let divergent = match validate_replica_handshake_settled(journal_path, &handshake)? {
518        HandshakeValidation::Ok => false,
519        HandshakeValidation::Divergent(kind) => {
520            // Alertable on /metrics: divergence outside an expected
521            // failover rejoin means corruption or a serious bug.
522            metrics.divergence_total.fetch_add(1, Ordering::Relaxed);
523            warn!(
524                last_sequence = handshake.last_sequence,
525                ?kind,
526                "replica journal divergent at handshake — routing through snapshot resync"
527            );
528            true
529        }
530    };
531
532    // Probe whether journal catch-up is possible before committing to
533    // a protocol path. This avoids sending StreamStart only to discover
534    // the journals are too old.
535    let can_catch_up =
536        !divergent && can_catch_up_from_journal(journal_path, handshake.last_sequence)?;
537
538    let mut publish = |buf: &[u8]| -> io::Result<()> {
539        writer.write_all(buf)?;
540        writer.flush()
541    };
542
543    // Cursor/stream floor for everything downstream. A divergent
544    // replica's claimed position is meaningless (its history forked —
545    // possibly ahead of our tip); the resync rebases it, so seed from 0
546    // like a fresh replica: the ack cursors understate until real acks
547    // arrive (safe), and the sent high-water seeds from the catch-up
548    // end instead of a bogus claim that would mark live ring chunks as
549    // already covered.
550    let stream_base = if divergent {
551        0
552    } else {
553        handshake.last_sequence
554    };
555
556    let catchup_end = if can_catch_up {
557        // The lineage origin (oldest segment's header identity) lets a
558        // fresh replica create a byte-identical journal before
559        // consuming the stream. Replicas with local state ignore it.
560        let (lineage_start, lineage_anchor) =
561            melin_transport_core::replication::catchup::lineage_origin(journal_path)?;
562        encode_stream_start(
563            handshake.last_sequence,
564            lineage_start,
565            lineage_anchor,
566            fence_state.epoch(),
567            durability_mode.load(Ordering::Relaxed),
568            &mut send_buf,
569        );
570        publish(&send_buf)?;
571        send_buf.clear();
572
573        match catch_up_from_journal::<A::Event>(
574            journal_path,
575            handshake.last_sequence,
576            &mut writer,
577            shutdown,
578        )? {
579            CatchUpResult::Ok(end) => end,
580            CatchUpResult::NeedSnapshot => {
581                return Err(io::Error::other("catch-up failed unexpectedly after probe"));
582            }
583        }
584    } else {
585        // Resync verdict precedes the snapshot data: `HashMismatch`
586        // tells the replica its journal is divergent (it archives its
587        // local lineage on receipt); plain `NeedSnapshot` is the
588        // too-far-behind rebase. The receiver expects `SnapshotBegin`
589        // as the very next frame after the verdict, so
590        // `snapshot_transfer_with` must follow immediately.
591        //
592        // Pre-flight before the verdict goes on the wire: the replica
593        // archives its lineage on receipt, so promising a snapshot we
594        // cannot produce (none configured, boundary segment pruned)
595        // would strand it with its history moved aside. Failing here
596        // drops the connection with the replica's journal intact.
597        preflight_snapshot_transfer(journal_path)?;
598        if divergent {
599            encode_hash_mismatch(&mut send_buf);
600        } else {
601            encode_need_snapshot(&mut send_buf);
602        }
603        publish(&send_buf)?;
604        send_buf.clear();
605        match snapshot_transfer_with::<A::Event>(
606            journal_path,
607            &mut publish,
608            shutdown,
609            durability_mode.load(Ordering::Relaxed),
610        )? {
611            CatchUpResult::Ok(end) => end,
612            CatchUpResult::NeedSnapshot => {
613                return Err(io::Error::other(
614                    "catch-up failed even after snapshot transfer",
615                ));
616            }
617        }
618    };
619
620    // Engage this slot's cursors and seed the gauge pair the response
621    // gate's `evaluate_durability` reads. Must happen BEFORE the
622    // bridge's active_flag Release — see `ReplicaCursors` for the
623    // ordering contract.
624    cursors.seed_on_handshake(slot_idx, stream_base);
625
626    // Bridge into live streaming: activates the ring, re-reads from the
627    // journal the entries that fell into the activation window, then
628    // drains the ring into sequence-contiguity (back-filling from disk
629    // if a skipped entry hasn't flushed yet) before going live. Returns
630    // the slot's sent high-water — the bound the ack-sanity invariant
631    // checks acks against, advanced by every chunk the live loop
632    // forwards. The bridge closes the catch-up→live gap under load (the
633    // receiver's contiguity gate backstops only the rare quiescent
634    // corner). See `bridge_catchup_to_live`.
635    let mut sent = {
636        let mut publish = |buf: &[u8]| -> io::Result<()> {
637            writer.write_all(buf)?;
638            writer.flush()
639        };
640        bridge_catchup_to_live::<A::Event>(
641            journal_path,
642            stream_base,
643            catchup_end,
644            active_flag,
645            repl_consumer,
646            &mut publish,
647            shutdown,
648        )?
649    };
650
651    // Catch-up complete — replica is entering the live streaming loop.
652    metrics.catching_up[slot_idx].store(false, Ordering::Relaxed);
653
654    // Signal that this replica is ready to consume from the replication
655    // ring. The main thread waits on this before seeding test data.
656    // Must happen AFTER catch-up and overlap drain complete — otherwise
657    // seeding fills the replication ring faster than we can drain it,
658    // deadlocking the journal stage.
659    replica_ready.store(true, Ordering::Release);
660
661    let heartbeat_interval = std::time::Duration::from_secs(heartbeat_secs);
662    let mut last_send = std::time::Instant::now();
663
664    live_stream_uring(
665        writer,
666        repl_consumer,
667        ctx,
668        heartbeat_interval,
669        &mut send_buf,
670        &mut last_send,
671        &mut sent,
672    )
673}
674
675/// Quiescing owner of the live-stream ring: the ring itself, the RECV
676/// landing buffer the kernel writes ack bytes into, the SEND source
677/// buffer the kernel reads out of, and the number of operations the
678/// kernel still owes CQEs for.
679///
680/// Exists for its `Drop`. Returning from the loop — shutdown, eviction,
681/// an I/O error, or a panic's unwind — while a RECV or SEND is still in
682/// flight would release those buffers (and drop the ring) with the
683/// kernel holding pointers into them. Ring teardown on fd close is
684/// *asynchronous*: the kernel can complete a pending RECV after the
685/// allocation has been returned to the allocator, writing replica-ack
686/// bytes into freed heap. That corruption surfaced as malloc-metadata
687/// segfaults at sender-thread exit in the failover tests, and being a
688/// kernel-side store it is invisible to AddressSanitizer. The guard
689/// closes every exit path: wake pending operations by shutting the
690/// socket down, then reap CQEs until the kernel owes none, and only
691/// then let the buffers go.
692struct InflightUring<'a> {
693    ring: io_uring::IoUring,
694    /// RECV landing buffer for ack frames. `Vec` so the heap pointer
695    /// handed to the kernel stays stable however the guard itself
696    /// moves; sized 4 KiB (acks are 13 bytes, the kernel may coalesce
697    /// many).
698    recv_buf: Vec<u8>,
699    /// SEND source buffer, borrowed from `handle_replica_connection`
700    /// (it outlives the live-stream loop so it can be reused across
701    /// reconnects). Held by the guard purely so the drain covers it:
702    /// the kernel *reads* it for an in-flight SEND, so releasing it
703    /// early would splice freed heap into the replication stream.
704    send_buf: &'a mut Vec<u8>,
705    /// SQEs pushed minus CQEs reaped. Between a push and the following
706    /// `submit` the kernel does not yet owe those CQEs, so this is an
707    /// upper bound rather than an exact debt; [`Self::drain`] submits
708    /// before waiting, which makes the two converge. `u32` matches the
709    /// ring-depth scale (at most a RECV and a SEND are ever
710    /// outstanding here).
711    in_flight: u32,
712    /// Raw socket fd, for the `shutdown(2)` wake in `Drop`. Always
713    /// outlives the guard: the owning `TcpStream` is a parameter of
714    /// [`live_stream_uring`] and parameters drop after locals.
715    tcp_fd: std::os::unix::io::RawFd,
716}
717
718impl Drop for InflightUring<'_> {
719    fn drop(&mut self) {
720        wake_pending_ops(self.tcp_fd);
721        if !self.drain() {
722            // The drain could not be proven complete, so the kernel may
723            // still write into `recv_buf` or read out of `send_buf`.
724            // Leak both: a few KiB lost on a dying connection is safe,
725            // returning memory the kernel still touches to the
726            // allocator is not. `take` leaves each `Vec` empty, so the
727            // caller's `send_buf` stays a valid (if reallocating) Vec.
728            std::mem::forget(std::mem::take(&mut self.recv_buf));
729            std::mem::forget(std::mem::take(self.send_buf));
730            warn!(
731                in_flight = self.in_flight,
732                "io_uring teardown drain did not complete; leaking replication buffers"
733            );
734        }
735    }
736}
737
738impl InflightUring<'_> {
739    /// Reap CQEs until the kernel owes none, bounded by
740    /// [`crate::uring_teardown::DRAIN_TIMEOUT`]. `true` means
741    /// `in_flight` reached zero and the
742    /// buffers are provably free of kernel references; `false` means
743    /// the caller must not release them. See [`crate::uring_teardown`]
744    /// for why this polls rather than blocking in `submit_and_wait`.
745    fn drain(&mut self) -> bool {
746        // Flush SQEs that were pushed but never submitted: until they
747        // reach the kernel, no CQE is ever coming for them.
748        loop {
749            match self.ring.submit() {
750                Ok(_) => break,
751                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
752                Err(_) => return false,
753            }
754        }
755        let mut backoff = DrainBackoff::new();
756        while self.in_flight > 0 {
757            for _ in self.ring.completion() {
758                self.in_flight = self.in_flight.saturating_sub(1);
759            }
760            if self.in_flight > 0 && !backoff.wait() {
761                return false;
762            }
763        }
764        true
765    }
766}
767
768/// Live streaming loop using async RECV/SEND via io_uring. A single RECV is always
769/// in-flight for ack frames; SEND is submitted when the replication ring
770/// has data. Both complete via the memory-mapped CQ with zero syscalls
771/// in the hot path. In-flight operations and both kernel-visible
772/// buffers are owned by an [`InflightUring`] guard, so no exit path can
773/// release memory the kernel is still reading or writing.
774fn live_stream_uring(
775    writer: TcpStream,
776    repl_consumer: &mut ReplicationConsumer,
777    ctx: &SlotContext<'_>,
778    heartbeat_interval: std::time::Duration,
779    send_buf: &mut Vec<u8>,
780    last_send: &mut std::time::Instant,
781    sent: &mut SentHighWater,
782) -> io::Result<()> {
783    let SlotContext {
784        cursors,
785        shutdown,
786        evict_flag,
787        metrics,
788        slot_idx,
789        batch_size,
790        busy_spin,
791        durability_mode,
792        // Only used during handshake/catch-up (handle_replica_connection).
793        journal_path: _,
794        authorized_keys: _,
795        replica_ready: _,
796        active_flag: _,
797        heartbeat_secs: _,
798        fence_state: _,
799        replicas_connected: _,
800        authenticated: _,
801    } = ctx;
802    let slot_idx = *slot_idx;
803    let batch_size = *batch_size;
804    let busy_spin = *busy_spin;
805
806    use io_uring::{IoUring, opcode, types};
807    use std::os::unix::io::AsRawFd;
808
809    const TOKEN_RECV: u64 = 0;
810    const TOKEN_SEND: u64 = 1;
811
812    let tcp_fd = writer.as_raw_fd();
813
814    let ring: IoUring = IoUring::builder()
815        .setup_single_issuer()
816        .build(8)
817        .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
818
819    ring.submitter()
820        .register_files(&[tcp_fd])
821        .map_err(|e| io::Error::other(format!("io_uring register_files: {e}")))?;
822
823    // Pin io-wq workers to core 0 (keep them off pipeline cores).
824    {
825        let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
826        unsafe { libc::CPU_SET(0, &mut cpuset) };
827        let _ = ring.submitter().register_iowq_aff(&cpuset);
828    }
829
830    // Ring, both kernel-visible buffers, and the in-flight count live
831    // in the quiescing guard from here on: every exit below (shutdown,
832    // eviction, `?`, unwind) must drain the kernel before any of these
833    // allocations can be released. The loop keeps using them through
834    // disjoint field borrows.
835    let mut uring = InflightUring {
836        ring,
837        recv_buf: vec![0u8; 4096],
838        send_buf,
839        in_flight: 0,
840        tcp_fd,
841    };
842    let (ring, recv_buf, send_buf) = (&mut uring.ring, &mut uring.recv_buf, &mut *uring.send_buf);
843    // Accumulation buffer for partial ack frame parsing.
844    let mut parse_buf: Vec<u8> = Vec::with_capacity(MAX_CONTROL_FRAME + 4);
845    // RECV is always resubmitted after CQE processing — no explicit
846    // tracking needed. The io_uring kernel guarantees ordering.
847    let mut send_in_flight = false;
848    let mut send_offset: usize = 0;
849    let mut idle_spins: u32 = 0;
850    let mut heartbeat_timer = melin_app::amortized_timer::AmortizedTimer::new();
851
852    // Diagnostic (RUST_LOG=debug): per-slot TCP_INFO snapshot once a
853    // second, slow-SEND detection (CQE elapsed >= threshold), and a
854    // TCP_INFO capture at the evict-exit point. Amortized so the
855    // per-iteration cost is a single `AND` + predictable branch.
856    let mut info_log_timer = melin_app::amortized_timer::AmortizedTimer::new();
857    let mut send_submit_ts: Option<std::time::Instant> = None;
858    const SLOW_SEND_THRESHOLD_MS: u128 = 5;
859
860    // Submit initial RECV.
861    let sqe = opcode::Recv::new(
862        types::Fixed(0),
863        recv_buf.as_mut_ptr(),
864        recv_buf.len() as u32,
865    )
866    .build()
867    .user_data(TOKEN_RECV);
868    // SAFETY: `recv_buf` is owned by the `InflightUring` guard, which
869    // drains this operation's CQE before the buffer can be freed on any
870    // exit path. The ring is single-threaded — only this task
871    // pushes/reaps on it.
872    unsafe { ring.submission().push(&sqe).expect("SQ full") };
873    uring.in_flight += 1;
874
875    loop {
876        // --- Check flags ---
877        if shutdown.load(Ordering::Relaxed) {
878            return Ok(());
879        }
880        if evict_flag.load(Ordering::Relaxed) {
881            // Capture the TCP state at the moment of eviction — the
882            // critical frame for comparing an evicted slot's teardown
883            // against the still-live slot's socket state when
884            // diagnosing post-eviction regressions.
885            super::log_tcp_info(tcp_fd, "evict_exit", slot_idx);
886            info!(slot = slot_idx, "handler exiting: evicted by journal stage");
887            return Ok(());
888        }
889
890        // --- Drain replication ring into send_buf (memory, non-blocking) ---
891        //
892        // Ring chunks are wire-ready `InputBatch` frames produced by the
893        // journal stage (phase 3 of the unified-pipeline plan), so the
894        // sender is a passthrough — no decode + re-encode here.
895        if !send_in_flight {
896            let mut coalesced = 0;
897            while coalesced < batch_size {
898                if let Some((meta, data)) = repl_consumer.try_read() {
899                    send_buf.extend_from_slice(data);
900                    repl_consumer.commit();
901                    sent.advance(meta.end_sequence);
902                    coalesced += 1;
903                } else {
904                    break;
905                }
906            }
907
908            if coalesced > 0 {
909                // Submit SEND for the coalesced buffer.
910                let sqe =
911                    opcode::Send::new(types::Fixed(0), send_buf.as_ptr(), send_buf.len() as u32)
912                        .build()
913                        .user_data(TOKEN_SEND);
914                // SAFETY: `send_buf` is owned by this task and pinned
915                // by `send_in_flight = true` below — it isn't mutated
916                // until the matching TOKEN_SEND CQE clears the flag,
917                // and the `InflightUring` guard drains the CQE before
918                // any exit path can release it. The ring is
919                // single-threaded.
920                unsafe { ring.submission().push(&sqe).expect("SQ full") };
921                uring.in_flight += 1;
922                send_in_flight = true;
923                send_offset = 0;
924                *last_send = std::time::Instant::now();
925                send_submit_ts = Some(*last_send);
926                idle_spins = 0;
927                heartbeat_timer = melin_app::amortized_timer::AmortizedTimer::new();
928            } else {
929                // Heartbeat check: amortized when spinning (mask keeps the
930                // clock read at ~10/s at 10M iter/s). In yield mode the loop
931                // already pays a syscall per iteration, so the clock read is
932                // free and must not be skipped — see AmortizedTimer docs.
933                let spinning = busy_spin || idle_spins < 1000;
934                if heartbeat_timer.tick(heartbeat_interval, spinning).is_some() {
935                    encode_heartbeat(
936                        sent.get(),
937                        durability_mode.load(Ordering::Relaxed),
938                        send_buf,
939                    );
940                    let sqe = opcode::Send::new(
941                        types::Fixed(0),
942                        send_buf.as_ptr(),
943                        send_buf.len() as u32,
944                    )
945                    .build()
946                    .user_data(TOKEN_SEND);
947                    // SAFETY: same as the coalesced send above —
948                    // `send_buf` is owned and pinned by `send_in_flight`
949                    // until the TOKEN_SEND CQE clears it, and the guard
950                    // drains the CQE on every exit path.
951                    unsafe { ring.submission().push(&sqe).expect("SQ full") };
952                    uring.in_flight += 1;
953                    send_in_flight = true;
954                    send_offset = 0;
955                    *last_send = std::time::Instant::now();
956                    send_submit_ts = Some(*last_send);
957                }
958            }
959        }
960
961        // Periodic TCP_INFO dump — debug level. Amortized so the
962        // per-iteration cost is a single `AND` + predictable branch.
963        if info_log_timer
964            .tick(
965                std::time::Duration::from_secs(1),
966                busy_spin || idle_spins < 1000,
967            )
968            .is_some()
969        {
970            super::log_tcp_info(tcp_fd, "live_stream", slot_idx);
971        }
972
973        // --- Submit SQEs to kernel (non-blocking) ---
974        // Skip the syscall when no new SQEs were pushed — an empty
975        // io_uring_enter still costs ~200 ns of mode-switch overhead and
976        // showed up as 6 % of total CPU in profiles of the sender loop.
977        let pending = ring.submission().len();
978        if pending > 0 {
979            ring.submit()
980                .map_err(|e| io::Error::other(format!("io_uring submit: {e}")))?;
981        }
982
983        // --- Collect CQEs (must drain before pushing new SQEs) ---
984        // Collecting into a small stack array avoids the CQ borrow
985        // conflicting with SQ pushes during processing.
986        let mut cqes: [(u64, i32); 4] = [(0, 0); 4];
987        let mut cqe_count = 0;
988        for cqe in ring.completion() {
989            // Every reaped CQE settles one operation the kernel owed —
990            // counted even if it overflows the recording array (it
991            // cannot: at most a RECV and a SEND are ever in flight).
992            // `saturating_sub` keeps a miscount from wrapping into a
993            // drain that never terminates; the assert catches the
994            // miscount itself rather than letting it hide in release.
995            debug_assert!(
996                uring.in_flight > 0,
997                "CQE reaped with no operation recorded in flight"
998            );
999            uring.in_flight = uring.in_flight.saturating_sub(1);
1000            if cqe_count < cqes.len() {
1001                cqes[cqe_count] = (cqe.user_data(), cqe.result());
1002                cqe_count += 1;
1003            }
1004        }
1005
1006        let any_cqe = cqe_count > 0;
1007        for &(token, result) in &cqes[..cqe_count] {
1008            idle_spins = 0;
1009            match token {
1010                TOKEN_RECV => {
1011                    if result <= 0 {
1012                        return Err(io::Error::other(format!(
1013                            "replica disconnected (recv returned {result})"
1014                        )));
1015                    }
1016                    let n = result as usize;
1017                    parse_buf.extend_from_slice(&recv_buf[..n]);
1018
1019                    // Extract complete ack frames from parse_buf.
1020                    let mut cursor = 0;
1021                    while cursor + 4 <= parse_buf.len() {
1022                        let frame_len = u32::from_le_bytes(
1023                            parse_buf[cursor..cursor + 4]
1024                                .try_into()
1025                                .expect("bounds checked: 4-byte slice"),
1026                        ) as usize;
1027                        if frame_len == 0 || frame_len > MAX_CONTROL_FRAME {
1028                            return Err(io::Error::other(format!(
1029                                "invalid ack frame length: {frame_len}"
1030                            )));
1031                        }
1032                        if cursor + 4 + frame_len > parse_buf.len() {
1033                            break; // Incomplete frame.
1034                        }
1035                        let payload = &parse_buf[cursor + 4..cursor + 4 + frame_len];
1036                        if let Ok(ReplicaMessage::Ack(ack)) = decode_replica_message(payload) {
1037                            // Eviction on violation: returning Err tears the
1038                            // connection down; the accept loop's cleanup
1039                            // disengages the cursors and frees the slot.
1040                            cursors
1041                                .record_ack(slot_idx, &ack, sent.get())
1042                                .map_err(io::Error::other)?;
1043                            metrics.ack_latency_us[slot_idx]
1044                                .store(last_send.elapsed().as_micros() as u64, Ordering::Relaxed);
1045                        }
1046                        cursor += 4 + frame_len;
1047                    }
1048                    // Compact parse_buf.
1049                    if cursor > 0 {
1050                        let remaining = parse_buf.len() - cursor;
1051                        parse_buf.copy_within(cursor.., 0);
1052                        parse_buf.truncate(remaining);
1053                    }
1054
1055                    // Resubmit RECV.
1056                    let sqe = opcode::Recv::new(
1057                        types::Fixed(0),
1058                        recv_buf.as_mut_ptr(),
1059                        recv_buf.len() as u32,
1060                    )
1061                    .build()
1062                    .user_data(TOKEN_RECV);
1063                    // SAFETY: `recv_buf` is owned by the guard; only
1064                    // one RECV is ever in flight (we just consumed the
1065                    // previous CQE), and the guard drains this one on
1066                    // every exit path. Ring is single-threaded.
1067                    unsafe { ring.submission().push(&sqe).expect("SQ full") };
1068                    uring.in_flight += 1;
1069                }
1070
1071                TOKEN_SEND => {
1072                    if result < 0 {
1073                        return Err(io::Error::other(format!("send error (returned {result})")));
1074                    }
1075                    let sent = result as usize;
1076                    send_offset += sent;
1077                    if send_offset >= send_buf.len() {
1078                        // Fully sent. Measure end-to-end SEND latency: a
1079                        // healthy io_uring TCP SEND completes in
1080                        // microseconds; > threshold implies the kernel
1081                        // waited on cwnd / peer window / retransmit.
1082                        if let Some(ts) = send_submit_ts.take() {
1083                            let elapsed = ts.elapsed();
1084                            if elapsed.as_millis() >= SLOW_SEND_THRESHOLD_MS {
1085                                tracing::debug!(
1086                                    slot = slot_idx,
1087                                    elapsed_us = elapsed.as_micros() as u64,
1088                                    bytes = send_buf.len(),
1089                                    "slow SEND completion"
1090                                );
1091                                super::log_tcp_info(tcp_fd, "slow_send", slot_idx);
1092                            }
1093                        }
1094                        metrics.bytes_sent[slot_idx]
1095                            .fetch_add(send_buf.len() as u64, Ordering::Relaxed);
1096                        send_buf.clear();
1097                        send_offset = 0;
1098                        send_in_flight = false;
1099                    } else {
1100                        // Partial send — resubmit remainder.
1101                        let sqe = opcode::Send::new(
1102                            types::Fixed(0),
1103                            send_buf[send_offset..].as_ptr(),
1104                            (send_buf.len() - send_offset) as u32,
1105                        )
1106                        .build()
1107                        .user_data(TOKEN_SEND);
1108                        // SAFETY: `send_buf` is owned and still pinned
1109                        // (`send_in_flight` stays true across partial
1110                        // sends), and the guard drains the CQE on every
1111                        // exit path; ring is single-threaded.
1112                        unsafe { ring.submission().push(&sqe).expect("SQ full") };
1113                        uring.in_flight += 1;
1114                    }
1115                }
1116
1117                _ => {}
1118            }
1119        }
1120
1121        // --- Idle wait ---
1122        if !any_cqe && send_buf.is_empty() {
1123            if busy_spin || idle_spins < 1000 {
1124                idle_spins = idle_spins.wrapping_add(1);
1125                std::hint::spin_loop();
1126            } else {
1127                std::thread::yield_now();
1128            }
1129        }
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136    use crate::uring_teardown::DRAIN_TIMEOUT;
1137    use io_uring::{IoUring, opcode, types};
1138
1139    /// Connected AF_UNIX stream pair standing in for the replication
1140    /// socket. Raw fds rather than `UnixStream` because the guard takes
1141    /// a `RawFd` and the test needs to outlive it.
1142    fn socketpair() -> (libc::c_int, libc::c_int) {
1143        let mut fds = [0 as libc::c_int; 2];
1144        // SAFETY: `fds` is a valid two-element array for the kernel to
1145        // fill in; the fds are owned by the caller from here.
1146        let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
1147        assert_eq!(rc, 0, "socketpair: {}", io::Error::last_os_error());
1148        (fds[0], fds[1])
1149    }
1150
1151    /// SAFETY: both fds are owned by the caller and not used again.
1152    fn close_pair(a: libc::c_int, b: libc::c_int) {
1153        // Return value dropped deliberately: nothing can be done about
1154        // a failing close on a test teardown path.
1155        unsafe {
1156            libc::close(a);
1157            libc::close(b);
1158        }
1159    }
1160
1161    /// Guard over `fd` with a RECV already submitted.
1162    ///
1163    /// Panics rather than skipping if the host denies io_uring:
1164    /// [`live_stream_uring`] is the only live-stream path there is, so
1165    /// a host that cannot build this ring cannot replicate at all. A
1166    /// test that quietly passed there would report coverage for a
1167    /// teardown it never ran.
1168    fn guard_with_pending_recv(fd: libc::c_int, send_buf: &mut Vec<u8>) -> InflightUring<'_> {
1169        let ring = IoUring::builder()
1170            .setup_single_issuer()
1171            .build(8)
1172            .expect("io_uring is a hard requirement for replication");
1173        ring.submitter()
1174            .register_files(&[fd])
1175            .expect("register_files");
1176        let mut uring = InflightUring {
1177            ring,
1178            recv_buf: vec![0u8; 4096],
1179            send_buf,
1180            in_flight: 0,
1181            tcp_fd: fd,
1182        };
1183        let sqe = opcode::Recv::new(
1184            types::Fixed(0),
1185            uring.recv_buf.as_mut_ptr(),
1186            uring.recv_buf.len() as u32,
1187        )
1188        .build()
1189        .user_data(0);
1190        // SAFETY: `recv_buf` belongs to the guard being built here, and
1191        // the guard drains this operation before releasing it. The ring
1192        // is only ever touched from this thread.
1193        unsafe { uring.ring.submission().push(&sqe).expect("SQ full") };
1194        uring.in_flight = 1;
1195        uring.ring.submit().expect("submit initial RECV");
1196        uring
1197    }
1198
1199    /// The assumption the whole teardown design rests on: a
1200    /// `shutdown(2)` — not an AsyncCancel, which some hosts filter —
1201    /// is enough to make the kernel hand back a RECV that nobody is
1202    /// ever going to write to.
1203    #[test]
1204    fn drain_reaps_a_pending_recv_after_shutdown() {
1205        let (a, b) = socketpair();
1206        let mut send_buf = Vec::new();
1207        let mut uring = guard_with_pending_recv(a, &mut send_buf);
1208        // The peer never writes, so the only thing that can complete
1209        // this RECV is the shutdown.
1210        unsafe { libc::shutdown(a, libc::SHUT_RDWR) };
1211        assert!(
1212            uring.drain(),
1213            "drain hit the deadline with a RECV in flight"
1214        );
1215        assert_eq!(uring.in_flight, 0, "kernel still owes a CQE");
1216        drop(uring);
1217        close_pair(a, b);
1218    }
1219
1220    /// `Drop` must quiesce the ring on its own — including issuing the
1221    /// wake — and must return promptly rather than burning the whole
1222    /// [`DRAIN_TIMEOUT`], which is what a teardown that silently failed
1223    /// to wake the operation would look like.
1224    #[test]
1225    fn drop_quiesces_a_pending_recv_without_hanging() {
1226        let (a, b) = socketpair();
1227        let mut send_buf = vec![0u8; 64];
1228        let uring = guard_with_pending_recv(a, &mut send_buf);
1229        let started = std::time::Instant::now();
1230        drop(uring);
1231        let elapsed = started.elapsed();
1232        assert!(
1233            elapsed < DRAIN_TIMEOUT / 4,
1234            "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1235        );
1236        // A clean drain releases the buffers; only a drain that cannot
1237        // prove the kernel is done leaks them.
1238        assert_eq!(send_buf.len(), 64, "send_buf leaked despite a clean drain");
1239        close_pair(a, b);
1240    }
1241}