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