Skip to main content

laminar_core/shuffle/transport/
mod.rs

1//! Cross-node shuffle transport over Tonic gRPC client-streaming.
2//!
3//! A leading `Hello` binds each stream to certified process incarnations and an
4//! assignment. Per-peer and per-node byte admission propagates backpressure to
5//! the HTTP/2 stream.
6
7use super::message::ShuffleMessage;
8use super::ShuffleTopologyFence;
9#[cfg(feature = "cluster")]
10use crate::checkpoint::CheckpointAttempt;
11
12/// Secondary queue and holdover item bound; byte semaphores are the primary
13/// admission control in cluster mode.
14const SHUFFLE_RECV_QUEUE: usize = 256;
15const MAX_STAGE_NAME_BYTES: usize = 4096;
16
17/// Peer identifier on the wire; matches `cluster::discovery::NodeId`'s inner type.
18pub type ShufflePeerId = u64;
19
20const SCOPE_CANCELLED: &str = "shuffle assignment or recovery scope was cancelled";
21const NONCANONICAL_BARRIER: &str =
22    "shuffle checkpoint barrier must use one nonzero canonical checkpoint ID";
23
24fn validate_checkpoint_barrier(
25    barrier: crate::checkpoint::CheckpointBarrier,
26) -> std::io::Result<()> {
27    if barrier.is_canonical() {
28        Ok(())
29    } else {
30        Err(std::io::Error::new(
31            std::io::ErrorKind::InvalidInput,
32            NONCANONICAL_BARRIER,
33        ))
34    }
35}
36
37fn validate_frontier(stage: &str, watermark: Option<i64>) -> std::io::Result<()> {
38    if stage.is_empty() || stage.len() > MAX_STAGE_NAME_BYTES {
39        return Err(std::io::Error::new(
40            std::io::ErrorKind::InvalidInput,
41            "shuffle stage scope is empty or too long",
42        ));
43    }
44    if watermark == Some(i64::MIN) {
45        return Err(std::io::Error::new(
46            std::io::ErrorKind::InvalidInput,
47            "shuffle frontier uses the reserved uninitialized watermark",
48        ));
49    }
50    Ok(())
51}
52
53#[derive(Debug)]
54struct ScopeCancelled;
55
56impl std::fmt::Display for ScopeCancelled {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_str(SCOPE_CANCELLED)
59    }
60}
61
62impl std::error::Error for ScopeCancelled {}
63
64#[cfg(feature = "cluster")]
65#[derive(Debug)]
66struct MayHaveAdmittedShuffleFrame(std::io::Error);
67
68#[cfg(feature = "cluster")]
69impl std::fmt::Display for MayHaveAdmittedShuffleFrame {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        self.0.fmt(f)
72    }
73}
74
75#[cfg(feature = "cluster")]
76impl std::error::Error for MayHaveAdmittedShuffleFrame {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        Some(&self.0)
79    }
80}
81
82#[cfg(feature = "cluster")]
83fn may_have_admitted_shuffle_frame(error: std::io::Error) -> std::io::Error {
84    std::io::Error::new(error.kind(), MayHaveAdmittedShuffleFrame(error))
85}
86
87#[cfg(feature = "cluster")]
88fn scope_cancelled_io() -> std::io::Error {
89    std::io::Error::new(std::io::ErrorKind::ConnectionAborted, ScopeCancelled)
90}
91
92/// Whether an outbound shuffle operation was cancelled by an assignment or recovery scope
93/// transition. Generic connection cancellation is deliberately not classified as a scope change.
94#[must_use]
95pub fn is_scope_cancelled(error: &std::io::Error) -> bool {
96    error
97        .get_ref()
98        .and_then(|source| source.downcast_ref::<ScopeCancelled>())
99        .is_some()
100}
101
102/// Whether a failed outbound operation may already have entered the peer's ordered send queue.
103#[cfg(feature = "cluster")]
104#[must_use]
105pub fn shuffle_send_may_have_been_admitted(error: &std::io::Error) -> bool {
106    error
107        .get_ref()
108        .and_then(|source| source.downcast_ref::<MayHaveAdmittedShuffleFrame>())
109        .is_some()
110}
111
112/// Gossip KV key under which a receiver publishes its listener address for peer
113/// discovery.
114#[cfg(feature = "cluster")]
115pub const SHUFFLE_ADDR_KEY: &str = "shuffle:addr";
116
117#[cfg(feature = "cluster")]
118#[allow(
119    clippy::doc_markdown,
120    clippy::default_trait_access,
121    clippy::missing_const_for_fn,
122    clippy::must_use_candidate,
123    clippy::too_many_lines,
124    missing_docs
125)]
126pub(crate) mod shuffle_v1 {
127    tonic::include_proto!("laminar.shuffle.v1");
128}
129
130/// Inbound staging shared by both builds. Ordered controls stop the data drainer
131/// and remain under the same item bound as the receive queue.
132struct Holdover {
133    staged: parking_lot::Mutex<rustc_hash::FxHashMap<String, Vec<ReceivedBatch>>>,
134    frontiers: parking_lot::Mutex<Vec<ReceivedFrontierCut>>,
135    barriers: parking_lot::Mutex<BarrierHoldover>,
136    /// Shared across data and controls so repeatedly draining the bounded receive
137    /// queue cannot turn it into an unbounded secondary queue.
138    items: std::sync::atomic::AtomicUsize,
139    capacity: usize,
140}
141
142#[derive(Default)]
143struct BarrierHoldover {
144    staged: Vec<ReceivedShuffle>,
145    #[cfg(feature = "cluster")]
146    retired_through: Option<RetiredCheckpoint>,
147}
148
149#[cfg(feature = "cluster")]
150#[derive(Clone, Copy)]
151struct RetiredCheckpoint {
152    attempt: CheckpointAttempt,
153    assignment_digest: [u8; 32],
154}
155
156impl BarrierHoldover {
157    #[cfg(feature = "cluster")]
158    fn is_retired(
159        &self,
160        attempt: CheckpointAttempt,
161        assignment_digest: Option<[u8; 32]>,
162    ) -> std::io::Result<bool> {
163        let Some(retired) = self.retired_through else {
164            return Ok(false);
165        };
166        match attempt.checkpoint_id.cmp(&retired.attempt.checkpoint_id) {
167            std::cmp::Ordering::Less => Ok(true),
168            std::cmp::Ordering::Greater => Ok(false),
169            std::cmp::Ordering::Equal
170                if assignment_digest == Some(retired.assignment_digest) =>
171            {
172                Ok(true)
173            }
174            std::cmp::Ordering::Equal => Err(std::io::Error::new(
175                std::io::ErrorKind::InvalidData,
176                format!(
177                    "retired checkpoint barrier {attempt:?} has a different assignment digest from its durable terminal outcome"
178                ),
179            )),
180        }
181    }
182}
183
184impl Holdover {
185    fn new(capacity: usize) -> Self {
186        Self {
187            staged: parking_lot::Mutex::default(),
188            frontiers: parking_lot::Mutex::default(),
189            barriers: parking_lot::Mutex::default(),
190            items: std::sync::atomic::AtomicUsize::new(0),
191            capacity,
192        }
193    }
194
195    fn try_reserve_item(&self) -> bool {
196        self.items
197            .try_update(
198                std::sync::atomic::Ordering::AcqRel,
199                std::sync::atomic::Ordering::Acquire,
200                |items| (items < self.capacity).then_some(items + 1),
201            )
202            .is_ok()
203    }
204
205    fn release_items(&self, count: usize) {
206        if count == 0 {
207            return;
208        }
209        let released = self.items.try_update(
210            std::sync::atomic::Ordering::AcqRel,
211            std::sync::atomic::Ordering::Acquire,
212            |items| items.checked_sub(count),
213        );
214        debug_assert!(
215            released.is_ok(),
216            "shuffle holdover item accounting underflow"
217        );
218    }
219
220    #[cfg(feature = "cluster")]
221    fn barrier_attempt(received: &ReceivedShuffle) -> Option<CheckpointAttempt> {
222        let ShuffleMessage::Barrier(barrier) = received.message() else {
223            return None;
224        };
225        let attempt = CheckpointAttempt::new(barrier.epoch, barrier.checkpoint_id);
226        attempt.is_canonical().then_some(attempt)
227    }
228
229    #[cfg(feature = "cluster")]
230    fn is_retired_barrier(&self, received: &ReceivedShuffle) -> std::io::Result<bool> {
231        let Some(attempt) = Self::barrier_attempt(received) else {
232            return Ok(false);
233        };
234        self.barriers
235            .lock()
236            .is_retired(attempt, received.assignment_digest)
237    }
238
239    #[cfg(feature = "cluster")]
240    fn is_retired_checkpoint_barrier(
241        &self,
242        barrier: crate::checkpoint::CheckpointBarrier,
243        assignment_digest: [u8; 32],
244    ) -> std::io::Result<bool> {
245        let attempt = CheckpointAttempt::new(barrier.epoch, barrier.checkpoint_id);
246        if !attempt.is_canonical() {
247            return Err(std::io::Error::new(
248                std::io::ErrorKind::InvalidInput,
249                NONCANONICAL_BARRIER,
250            ));
251        }
252        self.barriers
253            .lock()
254            .is_retired(attempt, Some(assignment_digest))
255    }
256
257    fn has_staged_barriers(&self) -> bool {
258        !self.barriers.lock().staged.is_empty()
259    }
260
261    fn stage_barrier(&self, barrier: ReceivedShuffle) -> std::io::Result<bool> {
262        let ShuffleMessage::Barrier(value) = barrier.message() else {
263            return Ok(false);
264        };
265        if !value.is_canonical() {
266            return Err(std::io::Error::new(
267                std::io::ErrorKind::InvalidInput,
268                NONCANONICAL_BARRIER,
269            ));
270        }
271        let mut holdover = self.barriers.lock();
272        #[cfg(feature = "cluster")]
273        if let Some(attempt) = Self::barrier_attempt(&barrier) {
274            if holdover.is_retired(attempt, barrier.assignment_digest)? {
275                return Ok(false);
276            }
277        }
278        holdover.staged.push(barrier);
279        Ok(true)
280    }
281
282    fn take_staged_barriers(&self) -> Vec<ReceivedShuffle> {
283        std::mem::take(&mut self.barriers.lock().staged)
284    }
285
286    fn has_staged_frontiers(&self) -> bool {
287        !self.frontiers.lock().is_empty()
288    }
289
290    fn stage_frontier(&self, frontier: ReceivedFrontierCut) {
291        self.frontiers.lock().push(frontier);
292    }
293
294    fn take_staged_frontiers(&self) -> Vec<ReceivedFrontierCut> {
295        std::mem::take(&mut *self.frontiers.lock())
296    }
297
298    #[cfg(feature = "cluster")]
299    fn clear_staged_frontiers(&self) {
300        let frontiers = self.take_staged_frontiers();
301        let item_count = frontiers.iter().map(ReceivedFrontierCut::item_count).sum();
302        self.release_items(item_count);
303    }
304
305    #[cfg(feature = "cluster")]
306    fn retire_checkpoint_attempt(
307        &self,
308        attempt: CheckpointAttempt,
309        assignment_digest: [u8; 32],
310    ) -> std::io::Result<()> {
311        if !attempt.is_canonical() {
312            return Err(std::io::Error::new(
313                std::io::ErrorKind::InvalidInput,
314                "retired checkpoint attempt must use one nonzero canonical checkpoint ID",
315            ));
316        }
317
318        let removed = {
319            let mut holdover = self.barriers.lock();
320            let retired = match holdover.retired_through {
321                None => RetiredCheckpoint {
322                    attempt,
323                    assignment_digest,
324                },
325                Some(retired) if attempt.checkpoint_id > retired.attempt.checkpoint_id => {
326                    RetiredCheckpoint {
327                        attempt,
328                        assignment_digest,
329                    }
330                }
331                Some(retired) if attempt.checkpoint_id == retired.attempt.checkpoint_id => {
332                    if assignment_digest != retired.assignment_digest {
333                        return Err(std::io::Error::new(
334                            std::io::ErrorKind::InvalidData,
335                            format!(
336                                "checkpoint retirement {attempt:?} has a different assignment digest from its high-water"
337                            ),
338                        ));
339                    }
340                    retired
341                }
342                Some(retired) => retired,
343            };
344            for barrier in &holdover.staged {
345                let Some(candidate) = Self::barrier_attempt(barrier) else {
346                    continue;
347                };
348                if candidate.checkpoint_id == retired.attempt.checkpoint_id
349                    && barrier.assignment_digest != Some(retired.assignment_digest)
350                {
351                    return Err(std::io::Error::new(
352                        std::io::ErrorKind::InvalidData,
353                        format!(
354                            "staged checkpoint barrier {candidate:?} has a different assignment digest from its durable terminal outcome"
355                        ),
356                    ));
357                }
358            }
359            holdover.retired_through = Some(retired);
360            let staged_before = holdover.staged.len();
361            holdover.staged.retain(|barrier| {
362                !Self::barrier_attempt(barrier).is_some_and(|candidate| {
363                    candidate.checkpoint_id <= retired.attempt.checkpoint_id
364                })
365            });
366            staged_before - holdover.staged.len()
367        };
368        self.release_items(removed);
369        Ok(())
370    }
371}
372
373impl Default for Holdover {
374    fn default() -> Self {
375        Self::new(SHUFFLE_RECV_QUEUE)
376    }
377}
378
379/// A decoded shuffle batch together with the memory admission charged for its
380/// source IPC payload.
381///
382/// The admission remains charged while this value lives, so consumers must
383/// retain the envelope while processing shallow clones of the batch.
384pub struct ReceivedBatch {
385    batch: arrow_array::RecordBatch,
386    routed_vnodes: std::sync::Arc<[u32]>,
387    reservation: Option<std::sync::Arc<InboundReservation>>,
388    peer: ShufflePeerId,
389    sender_incarnation: uuid::Uuid,
390    receiver_incarnation: uuid::Uuid,
391    stream_id: uuid::Uuid,
392    assignment_version: u64,
393    recovery_gen: u64,
394    topology: Option<ShuffleTopologyFence>,
395    checkpoint_sequence: u64,
396}
397
398/// Opaque ownership of the memory admission charged to a decoded shuffle batch.
399/// The charge is released when the last clone is dropped.
400#[must_use]
401#[derive(Clone)]
402pub struct ShuffleBatchAdmission(Option<std::sync::Arc<InboundReservation>>);
403
404impl std::fmt::Debug for ShuffleBatchAdmission {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        f.debug_struct("ShuffleBatchAdmission")
407            .field("admitted", &self.0.is_some())
408            .finish()
409    }
410}
411
412impl ReceivedBatch {
413    /// Borrow the decoded batch without releasing its inbound admission.
414    #[must_use]
415    pub const fn batch(&self) -> &arrow_array::RecordBatch {
416        &self.batch
417    }
418
419    /// Canonical receiver-owned vnode set carried with this batch.
420    #[must_use]
421    pub fn routed_vnodes(&self) -> &[u32] {
422        &self.routed_vnodes
423    }
424
425    /// Clone the canonical vnode set without copying its contents.
426    #[must_use]
427    pub fn routed_vnodes_arc(&self) -> std::sync::Arc<[u32]> {
428        std::sync::Arc::clone(&self.routed_vnodes)
429    }
430
431    /// Peer and exact transport stream that delivered this batch.
432    #[must_use]
433    pub const fn peer(&self) -> ShufflePeerId {
434        self.peer
435    }
436
437    /// Sender process incarnation bound by the stream handshake.
438    #[must_use]
439    pub const fn sender_incarnation(&self) -> uuid::Uuid {
440        self.sender_incarnation
441    }
442
443    /// Receiver process incarnation bound by the stream handshake.
444    #[must_use]
445    pub const fn receiver_incarnation(&self) -> uuid::Uuid {
446        self.receiver_incarnation
447    }
448
449    /// Connection identity bound by the stream handshake.
450    #[must_use]
451    pub const fn stream_id(&self) -> uuid::Uuid {
452        self.stream_id
453    }
454
455    /// Assignment version bound by the stream handshake.
456    #[must_use]
457    pub const fn assignment_version(&self) -> u64 {
458        self.assignment_version
459    }
460
461    /// Recovery generation bound by the stream handshake.
462    #[must_use]
463    pub const fn recovery_gen(&self) -> u64 {
464        self.recovery_gen
465    }
466
467    /// Exact logical topology bound by the handshake; absent only on the legacy fabric.
468    #[must_use]
469    pub const fn topology_fence(&self) -> Option<ShuffleTopologyFence> {
470        self.topology
471    }
472
473    /// Zero-based logical-frame sequence for this data batch.
474    #[must_use]
475    pub const fn checkpoint_sequence(&self) -> u64 {
476        self.checkpoint_sequence
477    }
478
479    /// Split the decoded batch from its admission so a retaining consumer can keep the charge for
480    /// exactly as long as shallow Arrow views remain live. Route metadata is not returned; callers
481    /// that need it must inspect [`Self::routed_vnodes`] before consuming this envelope.
482    pub fn into_parts(self) -> (arrow_array::RecordBatch, ShuffleBatchAdmission) {
483        (self.batch, ShuffleBatchAdmission(self.reservation))
484    }
485}
486
487impl std::fmt::Debug for ReceivedBatch {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        f.debug_struct("ReceivedBatch")
490            .field("batch", &self.batch)
491            .field("routed_vnodes", &self.routed_vnodes)
492            .field("peer", &self.peer)
493            .field("sender_incarnation", &self.sender_incarnation)
494            .field("receiver_incarnation", &self.receiver_incarnation)
495            .field("stream_id", &self.stream_id)
496            .field("assignment_version", &self.assignment_version)
497            .field("recovery_gen", &self.recovery_gen)
498            .field("topology", &self.topology)
499            .field("checkpoint_sequence", &self.checkpoint_sequence)
500            .field("admitted", &self.reservation.is_some())
501            .finish()
502    }
503}
504
505/// A received shuffle message whose decoded memory remains admitted until the
506/// envelope is dropped. Consumers retain it while processing shallow clones of
507/// the message or its batch.
508pub struct ReceivedShuffle {
509    peer: ShufflePeerId,
510    message: ShuffleMessage,
511    reservation: Option<std::sync::Arc<InboundReservation>>,
512    sender_incarnation: uuid::Uuid,
513    receiver_incarnation: uuid::Uuid,
514    stream_id: uuid::Uuid,
515    assignment_version: u64,
516    assignment_digest: Option<[u8; 32]>,
517    recovery_gen: u64,
518    topology: Option<ShuffleTopologyFence>,
519    checkpoint_sequence: u64,
520}
521
522impl ReceivedShuffle {
523    /// Peer that sent this message.
524    #[must_use]
525    pub const fn peer(&self) -> ShufflePeerId {
526        self.peer
527    }
528
529    /// Borrow the message without releasing its inbound admission.
530    #[must_use]
531    pub const fn message(&self) -> &ShuffleMessage {
532        &self.message
533    }
534
535    /// Sender process incarnation bound by the stream handshake.
536    #[must_use]
537    pub const fn sender_incarnation(&self) -> uuid::Uuid {
538        self.sender_incarnation
539    }
540
541    /// Receiver process incarnation bound by the stream handshake.
542    #[must_use]
543    pub const fn receiver_incarnation(&self) -> uuid::Uuid {
544        self.receiver_incarnation
545    }
546
547    /// Connection identity bound by the stream handshake.
548    #[must_use]
549    pub const fn stream_id(&self) -> uuid::Uuid {
550        self.stream_id
551    }
552
553    /// Assignment version carried by the stream that delivered this message.
554    #[must_use]
555    pub const fn assignment_version(&self) -> u64 {
556        self.assignment_version
557    }
558
559    /// Exact checkpoint assignment-certificate digest carried by a barrier.
560    #[must_use]
561    pub const fn assignment_digest(&self) -> Option<[u8; 32]> {
562        self.assignment_digest
563    }
564
565    /// Recovery generation carried by the stream that delivered this message.
566    #[must_use]
567    pub const fn recovery_gen(&self) -> u64 {
568        self.recovery_gen
569    }
570
571    /// Exact logical topology bound by the handshake; absent only on the legacy fabric.
572    #[must_use]
573    pub const fn topology_fence(&self) -> Option<ShuffleTopologyFence> {
574        self.topology
575    }
576
577    /// Data and frontiers carry their logical-frame sequence. A barrier carries the exclusive
578    /// high-water sequence it closes.
579    #[must_use]
580    pub const fn checkpoint_sequence(&self) -> u64 {
581        self.checkpoint_sequence
582    }
583
584    /// Split the message from any decoded-data admission after its transport
585    /// scope has been validated.
586    pub fn into_parts(self) -> (ShuffleMessage, ShuffleBatchAdmission) {
587        (self.message, ShuffleBatchAdmission(self.reservation))
588    }
589}
590
591impl std::fmt::Debug for ReceivedShuffle {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        f.debug_struct("ReceivedShuffle")
594            .field("peer", &self.peer)
595            .field("message", &self.message)
596            .field("stream_id", &self.stream_id)
597            .field("assignment_version", &self.assignment_version)
598            .field("recovery_gen", &self.recovery_gen)
599            .field("topology", &self.topology)
600            .field("checkpoint_sequence", &self.checkpoint_sequence)
601            .field("admitted", &self.reservation.is_some())
602            .finish_non_exhaustive()
603    }
604}
605
606/// A frontier and the same-stage batches from its ordered peer stream that precede it.
607#[derive(Debug)]
608pub struct ReceivedFrontierCut {
609    preceding: Vec<ReceivedBatch>,
610    frontier: ReceivedShuffle,
611}
612
613impl ReceivedFrontierCut {
614    /// Batches that must be applied before the frontier.
615    #[must_use]
616    pub fn preceding(&self) -> &[ReceivedBatch] {
617        &self.preceding
618    }
619
620    /// Frontier closing the returned batch prefix.
621    #[must_use]
622    pub const fn frontier(&self) -> &ReceivedShuffle {
623        &self.frontier
624    }
625
626    /// Consume the cut in application order: batches first, then frontier.
627    #[must_use]
628    pub fn into_parts(self) -> (Vec<ReceivedBatch>, ReceivedShuffle) {
629        (self.preceding, self.frontier)
630    }
631
632    fn item_count(&self) -> usize {
633        self.preceding.len() + 1
634    }
635
636    fn stage(&self) -> &str {
637        let ShuffleMessage::Frontier { stage, .. } = self.frontier.message() else {
638            unreachable!("frontier cut contains a frontier message");
639        };
640        stage
641    }
642}
643
644fn take_frontier_prefix(
645    staged: &mut rustc_hash::FxHashMap<String, Vec<ReceivedBatch>>,
646    stage: &str,
647    frontier: &ReceivedShuffle,
648) -> Vec<ReceivedBatch> {
649    let Some(batches) = staged.remove(stage) else {
650        return Vec::new();
651    };
652    let (preceding, remaining): (Vec<_>, Vec<_>) = batches.into_iter().partition(|batch| {
653        batch.peer == frontier.peer
654            && batch.sender_incarnation == frontier.sender_incarnation
655            && batch.receiver_incarnation == frontier.receiver_incarnation
656            && batch.stream_id == frontier.stream_id
657            && batch.assignment_version == frontier.assignment_version
658            && batch.recovery_gen == frontier.recovery_gen
659            && batch.topology == frontier.topology
660            && batch.checkpoint_sequence < frontier.checkpoint_sequence
661    });
662    if !remaining.is_empty() {
663        staged.insert(stage.to_owned(), remaining);
664    }
665    preceding
666}
667
668#[cfg(feature = "cluster")]
669struct InboundReservation {
670    node: tokio::sync::OwnedSemaphorePermit,
671    peer: tokio::sync::OwnedSemaphorePermit,
672    wire_bytes: usize,
673}
674
675#[cfg(not(feature = "cluster"))]
676struct InboundReservation;
677
678#[cfg(feature = "cluster")]
679mod grpc {
680    mod delivery_loss;
681    mod flow;
682    mod handshake;
683    mod pipeline_identity;
684    mod topology;
685    pub use flow::{is_shuffle_send_stalled, ShufflePeerFlow};
686    use flow::{OutboundReservation, PeerSendCounters, SendFlow};
687    use std::collections::hash_map::Entry;
688    use std::collections::VecDeque;
689    use std::io;
690    use std::net::SocketAddr;
691    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
692    use std::sync::{Arc, OnceLock};
693    use topology::parse_topology_fence;
694
695    use arrow_array::RecordBatch;
696    use bytes::Bytes;
697    use crossfire::{mpsc, AsyncRx, MAsyncTx};
698    use futures::StreamExt as _;
699    use parking_lot::{Mutex, RwLock};
700    use rustc_hash::FxHashMap;
701    use tokio::sync::{OwnedSemaphorePermit, Semaphore};
702    use tokio::task::JoinHandle;
703    use tokio_util::sync::CancellationToken;
704    use tonic::transport::{Channel, Server};
705    use tonic::Request;
706    use uuid::Uuid;
707
708    use super::shuffle_v1::shuffle_frame;
709    use super::shuffle_v1::shuffle_transport_client::ShuffleTransportClient;
710    use super::shuffle_v1::shuffle_transport_server::{ShuffleTransport, ShuffleTransportServer};
711    use super::shuffle_v1::{
712        Barrier, Frontier as WireFrontier, HandshakeRequest, HandshakeResponse, Hello, RoutedData,
713        ShuffleFrame, ShuffleSummary,
714    };
715    use super::{
716        is_scope_cancelled, may_have_admitted_shuffle_frame, scope_cancelled_io,
717        take_frontier_prefix, validate_checkpoint_barrier, validate_frontier, CheckpointAttempt,
718        Holdover, InboundReservation, ReceivedBatch, ReceivedFrontierCut, ReceivedShuffle,
719        ShuffleMessage, ShufflePeerId, ShuffleTopologyFence, MAX_STAGE_NAME_BYTES,
720        NONCANONICAL_BARRIER, SCOPE_CANCELLED, SHUFFLE_ADDR_KEY, SHUFFLE_RECV_QUEUE,
721    };
722    use crate::checkpoint::{CheckpointAssignmentFence, CheckpointBarrier};
723    use crate::cluster::control::{ClusterKv, LeaseDeadline};
724    use crate::serialization::{serialize_batch_stream_bounded, BatchStreamDecoder};
725
726    const SEND_QUEUE: usize = 256;
727
728    const OUTBOUND_PEER_BUDGET_BYTES: usize = 32 * 1024 * 1024;
729    const CHECKPOINTED_CONTROL_PEER_BUDGET_BYTES: usize = 256 * 1024;
730    const CHECKPOINTED_CONTROL_NODE_BUDGET_BYTES: usize = 4 * 1024 * 1024;
731    const OUTBOUND_NODE_BUDGET_BYTES: usize = crate::shuffle::SHUFFLE_NODE_BUDGET_BYTES;
732    const INBOUND_PEER_BUDGET_BYTES: usize = 32 * 1024 * 1024;
733    const INBOUND_NODE_BUDGET_BYTES: usize = crate::shuffle::SHUFFLE_NODE_BUDGET_BYTES;
734    const MAX_SOURCE_SCHEMA_MEMORY_BYTES: usize = 256 * 1024;
735    /// The encoded schema is the canonical protocol bound. It must fit entirely in fragment zero
736    /// so a receiver can verify it before allocating an Arrow decoder.
737    const MAX_SCHEMA_WIRE_BYTES: usize = 512 * 1024;
738    /// Decoding uses fresh, right-sized strings and maps, but retain explicit allocator headroom
739    /// above the sender bound so an accepted schema cannot fail only because capacities differ.
740    const MAX_DECODED_SCHEMA_MEMORY_BYTES: usize = 1024 * 1024;
741    const MAX_DECODED_ARRAY_STRUCTURE_BYTES: usize = 2 * MAX_DECODED_SCHEMA_MEMORY_BYTES;
742    const MAX_ROUTE_METADATA_BYTES: usize =
743        crate::state::MAX_KEY_GROUP_COUNT as usize * std::mem::size_of::<u32>();
744    /// Frame envelopes, collection allocations, and protobuf bookkeeping beyond exact payload,
745    /// schema, stage, and route bytes.
746    const FRAME_METADATA_BYTES: usize = 64 * 1024;
747    const RETAINED_BATCH_ENVELOPE_BYTES: usize = 4 * 1024;
748    const INBOUND_BATCH_METADATA_BYTES: usize = MAX_DECODED_SCHEMA_MEMORY_BYTES
749        + MAX_DECODED_ARRAY_STRUCTURE_BYTES
750        + (2 * MAX_ROUTE_METADATA_BYTES)
751        + (2 * MAX_STAGE_NAME_BYTES)
752        + FRAME_METADATA_BYTES;
753    const MAX_WIRE_PAYLOAD_BYTES: usize = 1024 * 1024;
754    const _: () = assert!(MAX_SCHEMA_WIRE_BYTES + 8 <= MAX_WIRE_PAYLOAD_BYTES);
755    const MAX_FRAGMENTS: usize =
756        crate::shuffle::message::MAX_PAYLOAD_BYTES / MAX_WIRE_PAYLOAD_BYTES;
757    const BLOCKING_IPC_THRESHOLD_BYTES: usize = 512 * 1024;
758    const FRONTIER_WORKSPACE_BYTES: usize = 2 * MAX_STAGE_NAME_BYTES + 1024;
759    /// One wire fragment plus protobuf metadata. Logical Arrow payloads are reassembled under a
760    /// separate 16 MiB cap.
761    const MAX_SHUFFLE_MESSAGE_BYTES: usize = 2 * 1024 * 1024;
762    const OUTBOUND_DATA_WORKSPACE_BYTES: usize = crate::shuffle::message::MAX_PAYLOAD_BYTES
763        + crate::shuffle::ROUTE_MAX_BATCH_BYTES
764        + MAX_SOURCE_SCHEMA_MEMORY_BYTES
765        + (2 * MAX_ROUTE_METADATA_BYTES)
766        + (2 * MAX_STAGE_NAME_BYTES)
767        + FRAME_METADATA_BYTES;
768    // Tonic may own one decoded-but-not-yet-admitted frame per active stream. Keep this memory
769    // finite while reserving the full certified roster.
770    const UNADMITTED_FRAME_BUDGET_BYTES: usize = 256 * 1024 * 1024;
771    const MAX_ACTIVE_STREAMS: usize = UNADMITTED_FRAME_BUDGET_BYTES / MAX_SHUFFLE_MESSAGE_BYTES;
772    const _: () = assert!(MAX_ACTIVE_STREAMS >= crate::checkpoint::MAX_CHECKPOINT_PARTICIPANTS - 1);
773    const MAX_TRACKED_PEERS: usize = 4096;
774    const MAX_PENDING_HANDSHAKES: usize = 4096;
775    const HANDSHAKE_TOKEN_TTL: std::time::Duration = std::time::Duration::from_secs(30);
776    const PROCESS_LEASE_EXPIRED: &str = "shuffle process lease is no longer live";
777
778    type InboundRx = AsyncRx<mpsc::Array<Inbound>>;
779    type InboundTx = MAsyncTx<mpsc::Array<Inbound>>;
780
781    fn io_err<E: std::fmt::Display>(e: E) -> io::Error {
782        io::Error::other(e.to_string())
783    }
784
785    fn scope_cancelled_status() -> tonic::Status {
786        tonic::Status::cancelled(SCOPE_CANCELLED)
787    }
788
789    fn status_io(status: tonic::Status) -> io::Error {
790        if status.code() == tonic::Code::Cancelled && status.message() == SCOPE_CANCELLED {
791            scope_cancelled_io()
792        } else {
793            io_err(status)
794        }
795    }
796
797    fn process_lease_expired_io() -> io::Error {
798        io::Error::new(io::ErrorKind::PermissionDenied, PROCESS_LEASE_EXPIRED)
799    }
800
801    fn process_lease_expired_status() -> tonic::Status {
802        tonic::Status::failed_precondition(PROCESS_LEASE_EXPIRED)
803    }
804
805    struct ProcessLeaseGate {
806        deadline: OnceLock<Arc<LeaseDeadline>>,
807        cancelled: CancellationToken,
808        watcher: Mutex<Option<tokio::task::AbortHandle>>,
809    }
810
811    impl Default for ProcessLeaseGate {
812        fn default() -> Self {
813            Self {
814                deadline: OnceLock::new(),
815                cancelled: CancellationToken::new(),
816                watcher: Mutex::new(None),
817            }
818        }
819    }
820
821    impl ProcessLeaseGate {
822        fn is_installed_deadline(&self, deadline: &Arc<LeaseDeadline>) -> bool {
823            self.deadline
824                .get()
825                .is_some_and(|current| Arc::ptr_eq(current, deadline))
826        }
827
828        fn install(&self, deadline: Arc<LeaseDeadline>) -> io::Result<()> {
829            if !deadline.is_live() {
830                return Err(process_lease_expired_io());
831            }
832            let mut watcher_slot = self.watcher.lock();
833            if let Some(current) = self.deadline.get() {
834                return if Arc::ptr_eq(current, &deadline) {
835                    Ok(())
836                } else {
837                    Err(io::Error::new(
838                        io::ErrorKind::AlreadyExists,
839                        "shuffle process lease deadline is already installed",
840                    ))
841                };
842            }
843            let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
844                io::Error::other(format!(
845                    "shuffle process lease requires a Tokio runtime: {error}"
846                ))
847            })?;
848            self.deadline.set(Arc::clone(&deadline)).map_err(|_| {
849                io::Error::new(
850                    io::ErrorKind::AlreadyExists,
851                    "shuffle process lease deadline is already installed",
852                )
853            })?;
854            let cancelled = self.cancelled.clone();
855            let watcher = runtime.spawn(async move {
856                deadline.wait_until_expired().await;
857                cancelled.cancel();
858            });
859            *watcher_slot = Some(watcher.abort_handle());
860            Ok(())
861        }
862
863        fn install_pair(
864            first: &Self,
865            second: &Self,
866            deadline: Arc<LeaseDeadline>,
867        ) -> io::Result<()> {
868            let mut first_watcher = first.watcher.lock();
869            let mut second_watcher = second.watcher.lock();
870            if !deadline.is_live() {
871                return Err(process_lease_expired_io());
872            }
873            for gate in [first, second] {
874                if gate
875                    .deadline
876                    .get()
877                    .is_some_and(|current| !Arc::ptr_eq(current, &deadline))
878                {
879                    return Err(io::Error::new(
880                        io::ErrorKind::AlreadyExists,
881                        "shuffle process lease deadline is already installed",
882                    ));
883                }
884            }
885
886            let install_first = first.deadline.get().is_none();
887            let install_second = second.deadline.get().is_none();
888            let runtime = if install_first || install_second {
889                Some(tokio::runtime::Handle::try_current().map_err(|error| {
890                    io::Error::other(format!(
891                        "shuffle process lease requires a Tokio runtime: {error}"
892                    ))
893                })?)
894            } else {
895                None
896            };
897
898            if install_first {
899                assert!(
900                    first.deadline.set(Arc::clone(&deadline)).is_ok(),
901                    "first shuffle lease gate changed while its watcher lock was held"
902                );
903            }
904            if install_second {
905                assert!(
906                    second.deadline.set(Arc::clone(&deadline)).is_ok(),
907                    "second shuffle lease gate changed while its watcher lock was held"
908                );
909            }
910
911            if let Some(runtime) = runtime {
912                if install_first {
913                    let cancelled = first.cancelled.clone();
914                    let first_deadline = Arc::clone(&deadline);
915                    let watcher = runtime.spawn(async move {
916                        first_deadline.wait_until_expired().await;
917                        cancelled.cancel();
918                    });
919                    *first_watcher = Some(watcher.abort_handle());
920                }
921                if install_second {
922                    let cancelled = second.cancelled.clone();
923                    let watcher = runtime.spawn(async move {
924                        deadline.wait_until_expired().await;
925                        cancelled.cancel();
926                    });
927                    *second_watcher = Some(watcher.abort_handle());
928                }
929            }
930            Ok(())
931        }
932
933        fn require_live_io(&self) -> io::Result<()> {
934            match self.deadline.get() {
935                Some(deadline) if deadline.is_live() => Ok(()),
936                Some(_) => Err(process_lease_expired_io()),
937                None => Err(io::Error::new(
938                    io::ErrorKind::PermissionDenied,
939                    "shuffle process lease deadline is not installed",
940                )),
941            }
942        }
943
944        fn require_live_status(&self) -> Result<(), tonic::Status> {
945            match self.deadline.get() {
946                Some(deadline) if deadline.is_live() => Ok(()),
947                Some(_) => Err(process_lease_expired_status()),
948                None => Err(tonic::Status::failed_precondition(
949                    "shuffle process lease deadline is not installed",
950                )),
951            }
952        }
953
954        async fn wait_until_lost(&self) {
955            let Some(deadline) = self.deadline.get() else {
956                return;
957            };
958            if !deadline.is_live() {
959                return;
960            }
961            tokio::select! {
962                biased;
963                () = self.cancelled.cancelled() => {}
964                () = deadline.wait_until_expired() => {}
965            }
966        }
967
968        fn scope_token(&self, active: bool) -> CancellationToken {
969            let token = self.cancelled.child_token();
970            if !active || self.require_live_io().is_err() {
971                token.cancel();
972            }
973            token
974        }
975
976        #[cfg(test)]
977        fn install_live_for_test(&self) {
978            self.deadline
979                .set(Arc::new(LeaseDeadline::live_for(
980                    std::time::Duration::from_secs(60),
981                )))
982                .expect("test process lease is installed once");
983        }
984    }
985
986    impl Drop for ProcessLeaseGate {
987        fn drop(&mut self) {
988            if let Some(watcher) = self.watcher.get_mut().take() {
989                watcher.abort();
990            }
991        }
992    }
993
994    fn cancelled_token() -> CancellationToken {
995        let token = CancellationToken::new();
996        token.cancel();
997        token
998    }
999
1000    fn rotate_scope_token(
1001        slot: &RwLock<CancellationToken>,
1002        process_lease: &ProcessLeaseGate,
1003        active: bool,
1004    ) {
1005        let mut token = slot.write();
1006        token.cancel();
1007        *token = process_lease.scope_token(active);
1008    }
1009
1010    /// A message prepared before its sequence is fixed and inserted into the ordered queue.
1011    enum PreparedMessage {
1012        Barrier(CheckpointBarrier),
1013        Frontier {
1014            stage: String,
1015            watermark: Option<i64>,
1016            idle: bool,
1017        },
1018        Data {
1019            stage: String,
1020            routed_vnodes: Vec<u32>,
1021            arrow_ipc: Bytes,
1022        },
1023    }
1024
1025    struct Outbound {
1026        gen: u64,
1027        assignment_version: u64,
1028        /// Data/frontier: this frame's sequence. `Barrier`: ordered frames enqueued so far.
1029        seq: u64,
1030        msg: PreparedMessage,
1031        /// Present only for checkpoint barriers admitted by `fan_out_barrier`.
1032        assignment_digest: Option<[u8; 32]>,
1033        _budget: OutboundReservation,
1034    }
1035
1036    struct Encoded {
1037        frames: VecDeque<ShuffleFrame>,
1038        _budget: OutboundReservation,
1039    }
1040
1041    struct Inbound {
1042        peer: ShufflePeerId,
1043        msg: ShuffleMessage,
1044        budget: Option<Arc<InboundReservation>>,
1045        fence: StreamFence,
1046        assignment_digest: Option<[u8; 32]>,
1047        checkpoint_sequence: u64,
1048    }
1049
1050    impl Inbound {
1051        fn into_received(self) -> ReceivedShuffle {
1052            ReceivedShuffle {
1053                peer: self.peer,
1054                message: self.msg,
1055                reservation: self.budget,
1056                sender_incarnation: self.fence.sender_incarnation,
1057                receiver_incarnation: self.fence.receiver_incarnation,
1058                stream_id: self.fence.stream_id,
1059                assignment_version: self.fence.assignment_version,
1060                assignment_digest: self.assignment_digest,
1061                recovery_gen: self.fence.recovery_gen,
1062                topology: self.fence.topology,
1063                checkpoint_sequence: self.checkpoint_sequence,
1064            }
1065        }
1066    }
1067
1068    struct InboundBudget {
1069        node: Arc<Semaphore>,
1070        peers: Mutex<FxHashMap<ShufflePeerId, Arc<Semaphore>>>,
1071    }
1072
1073    impl InboundBudget {
1074        fn new(node_capacity: usize) -> Self {
1075            Self {
1076                node: Arc::new(Semaphore::new(node_capacity)),
1077                peers: Mutex::new(FxHashMap::default()),
1078            }
1079        }
1080
1081        async fn reserve_frame(
1082            &self,
1083            peer: ShufflePeerId,
1084            wire_bytes: usize,
1085            cancel: &CancellationToken,
1086        ) -> Result<InboundReservation, tonic::Status> {
1087            let bytes = wire_bytes
1088                .checked_add(crate::shuffle::ROUTE_MAX_BATCH_BYTES)
1089                .and_then(|bytes| bytes.checked_add(INBOUND_BATCH_METADATA_BYTES))
1090                // Reassembly owns its full preallocated Vec while the current prost Bytes frame
1091                // remains live in `run_stream`.
1092                .and_then(|bytes| bytes.checked_add(MAX_WIRE_PAYLOAD_BYTES))
1093                .ok_or_else(|| tonic::Status::resource_exhausted("shuffle frame is too large"))?;
1094            if wire_bytes == 0
1095                || wire_bytes > crate::shuffle::message::MAX_PAYLOAD_BYTES
1096                || bytes > INBOUND_PEER_BUDGET_BYTES
1097            {
1098                return Err(tonic::Status::resource_exhausted(
1099                    "shuffle frame exceeds its inbound byte budget",
1100                ));
1101            }
1102            let permits = u32::try_from(bytes)
1103                .map_err(|_| tonic::Status::resource_exhausted("shuffle frame is too large"))?;
1104            let peer_budget = {
1105                let mut peers = self.peers.lock();
1106                peers.retain(|known_peer, budget| {
1107                    *known_peer == peer
1108                        || Arc::strong_count(budget) > 1
1109                        || budget.available_permits() != INBOUND_PEER_BUDGET_BYTES
1110                });
1111                if let Some(budget) = peers.get(&peer) {
1112                    Arc::clone(budget)
1113                } else {
1114                    if peers.len() >= MAX_TRACKED_PEERS {
1115                        return Err(tonic::Status::resource_exhausted("too many shuffle peers"));
1116                    }
1117                    let budget = Arc::new(Semaphore::new(INBOUND_PEER_BUDGET_BYTES));
1118                    peers.insert(peer, Arc::clone(&budget));
1119                    budget
1120                }
1121            };
1122            let peer_permit = tokio::select! {
1123                biased;
1124                () = cancel.cancelled() => return Err(scope_cancelled_status()),
1125                permit = peer_budget.acquire_many_owned(permits) => {
1126                    permit.map_err(|_| tonic::Status::unavailable("shuffle peer budget closed"))?
1127                }
1128            };
1129            let node_permit = tokio::select! {
1130                biased;
1131                () = cancel.cancelled() => return Err(scope_cancelled_status()),
1132                permit = Arc::clone(&self.node).acquire_many_owned(permits) => {
1133                    permit.map_err(|_| tonic::Status::unavailable("shuffle node budget closed"))?
1134                }
1135            };
1136            Ok(InboundReservation {
1137                node: node_permit,
1138                peer: peer_permit,
1139                wire_bytes,
1140            })
1141        }
1142
1143        fn validate_decoded(batches: &[RecordBatch]) -> Result<usize, tonic::Status> {
1144            let bytes = batches.iter().try_fold(0usize, |total, batch| {
1145                let batch_bytes = crate::shuffle::routing::logical_batch_bytes(batch)
1146                    .map_err(|error| tonic::Status::invalid_argument(error.to_string()))?;
1147                total.checked_add(batch_bytes).ok_or_else(|| {
1148                    tonic::Status::resource_exhausted("decoded shuffle payload size overflow")
1149                })
1150            })?;
1151            if bytes > crate::shuffle::ROUTE_MAX_BATCH_BYTES {
1152                return Err(tonic::Status::resource_exhausted(format!(
1153                    "decoded shuffle payload is {bytes} bytes; limit is {}",
1154                    crate::shuffle::ROUTE_MAX_BATCH_BYTES
1155                )));
1156            }
1157            Ok(bytes)
1158        }
1159    }
1160
1161    impl InboundReservation {
1162        fn retain_decoded(
1163            &mut self,
1164            decoded_bytes: usize,
1165            metadata_bytes: usize,
1166        ) -> Result<(), tonic::Status> {
1167            let retained_bytes = self
1168                .wire_bytes
1169                .checked_add(decoded_bytes)
1170                .and_then(|bytes| bytes.checked_add(metadata_bytes))
1171                .ok_or_else(|| tonic::Status::internal("shuffle inbound accounting overflow"))?;
1172            let node_bytes = self.node.num_permits();
1173            let peer_bytes = self.peer.num_permits();
1174            if node_bytes != peer_bytes || retained_bytes > node_bytes {
1175                return Err(tonic::Status::internal(
1176                    "shuffle inbound reservation accounting mismatch",
1177                ));
1178            }
1179
1180            let excess_bytes = node_bytes - retained_bytes;
1181            if excess_bytes != 0 {
1182                let node_excess = self
1183                    .node
1184                    .split(excess_bytes)
1185                    .expect("validated node reservation split");
1186                let peer_excess = self
1187                    .peer
1188                    .split(excess_bytes)
1189                    .expect("validated peer reservation split");
1190                drop((node_excess, peer_excess));
1191            }
1192            Ok(())
1193        }
1194    }
1195
1196    fn decode_ipc_payload(
1197        decoder: &mut BatchStreamDecoder,
1198        payload: Vec<u8>,
1199    ) -> Result<RecordBatch, arrow_schema::ArrowError> {
1200        let mut batches = decoder.decode_chunk(payload)?;
1201        decoder.ensure_message_boundary()?;
1202        if batches.len() != 1 {
1203            return Err(arrow_schema::ArrowError::IpcError(format!(
1204                "logical shuffle payload decoded {} record batches; expected exactly one",
1205                batches.len()
1206            )));
1207        }
1208        Ok(batches.pop().expect("validated one decoded batch"))
1209    }
1210
1211    async fn decode_ipc_payload_isolated<F>(
1212        payload: Vec<u8>,
1213        budget: InboundReservation,
1214        before_blocking_decode: F,
1215    ) -> Result<(RecordBatch, InboundReservation), String>
1216    where
1217        F: FnOnce() + Send + 'static,
1218    {
1219        if payload.len() >= BLOCKING_IPC_THRESHOLD_BYTES {
1220            tokio::task::spawn_blocking(move || {
1221                before_blocking_decode();
1222                let mut decoder = BatchStreamDecoder::new();
1223                let batch =
1224                    decode_ipc_payload(&mut decoder, payload).map_err(|error| error.to_string())?;
1225                Ok((batch, budget))
1226            })
1227            .await
1228            .map_err(|error| format!("shuffle decoder task: {error}"))?
1229        } else {
1230            let mut decoder = BatchStreamDecoder::new();
1231            let batch =
1232                decode_ipc_payload(&mut decoder, payload).map_err(|error| error.to_string())?;
1233            Ok((batch, budget))
1234        }
1235    }
1236
1237    fn schema_memory_size(schema: &arrow_schema::Schema) -> usize {
1238        let fields = schema
1239            .fields()
1240            .iter()
1241            .fold(0usize, |bytes, field| bytes.saturating_add(field.size()));
1242        let metadata = schema.metadata().iter().fold(
1243            schema
1244                .metadata()
1245                .capacity()
1246                .saturating_mul(std::mem::size_of::<(String, String)>()),
1247            |bytes, (key, value)| {
1248                bytes
1249                    .saturating_add(key.capacity())
1250                    .saturating_add(value.capacity())
1251            },
1252        );
1253        std::mem::size_of_val(schema)
1254            .saturating_add(
1255                schema
1256                    .fields()
1257                    .len()
1258                    .saturating_mul(std::mem::size_of::<arrow_schema::FieldRef>()),
1259            )
1260            .saturating_add(fields)
1261            .saturating_add(metadata)
1262    }
1263
1264    fn outbound_workspace_bytes(msg: &ShuffleMessage) -> io::Result<usize> {
1265        match msg {
1266            ShuffleMessage::Barrier(barrier) => {
1267                validate_checkpoint_barrier(*barrier)?;
1268                Ok(1024)
1269            }
1270            ShuffleMessage::Frontier {
1271                stage, watermark, ..
1272            } => {
1273                validate_frontier(stage, *watermark)?;
1274                Ok(FRONTIER_WORKSPACE_BYTES)
1275            }
1276            ShuffleMessage::Data {
1277                stage,
1278                routed_vnodes,
1279                batch,
1280            } => {
1281                if stage.is_empty() || stage.len() > MAX_STAGE_NAME_BYTES {
1282                    return Err(io::Error::new(
1283                        io::ErrorKind::InvalidInput,
1284                        "shuffle stage scope is empty or too long",
1285                    ));
1286                }
1287                let batch_bytes =
1288                    crate::shuffle::routing::logical_batch_bytes(batch).map_err(|error| {
1289                        io::Error::new(io::ErrorKind::InvalidInput, error.to_string())
1290                    })?;
1291                if batch_bytes > crate::shuffle::ROUTE_MAX_BATCH_BYTES {
1292                    return Err(io::Error::new(
1293                        io::ErrorKind::InvalidInput,
1294                        format!(
1295                            "shuffle batch is {batch_bytes} bytes; limit is {}",
1296                            crate::shuffle::ROUTE_MAX_BATCH_BYTES
1297                        ),
1298                    ));
1299                }
1300                if batch.num_rows() > crate::shuffle::ROUTE_MAX_BATCH_ROWS {
1301                    return Err(io::Error::new(
1302                        io::ErrorKind::InvalidInput,
1303                        "shuffle batch exceeds the row-count bound",
1304                    ));
1305                }
1306                let schema_bytes = schema_memory_size(batch.schema().as_ref());
1307                if schema_bytes > MAX_SOURCE_SCHEMA_MEMORY_BYTES {
1308                    return Err(io::Error::new(
1309                        io::ErrorKind::InvalidInput,
1310                        format!(
1311                            "shuffle schema is {schema_bytes} bytes; limit is {MAX_SOURCE_SCHEMA_MEMORY_BYTES}"
1312                        ),
1313                    ));
1314                }
1315                let routes_are_canonical = routed_vnodes.windows(2).all(|pair| pair[0] < pair[1]);
1316                if !routes_are_canonical
1317                    || routed_vnodes.is_empty()
1318                    || routed_vnodes.len()
1319                        > usize::try_from(crate::state::MAX_KEY_GROUP_COUNT).unwrap_or(usize::MAX)
1320                {
1321                    return Err(io::Error::new(
1322                        io::ErrorKind::InvalidInput,
1323                        "shuffle route metadata is empty or non-canonical",
1324                    ));
1325                }
1326                Ok(OUTBOUND_DATA_WORKSPACE_BYTES)
1327            }
1328        }
1329    }
1330
1331    fn encode_outbound_data(
1332        stage: String,
1333        routed_vnodes: Vec<u32>,
1334        batch: RecordBatch,
1335        logical_bytes: usize,
1336        schema_bytes: usize,
1337        mut budget: OutboundReservation,
1338    ) -> io::Result<(PreparedMessage, OutboundReservation)> {
1339        let initial_capacity = logical_bytes
1340            .saturating_add(schema_bytes)
1341            .saturating_add(FRAME_METADATA_BYTES)
1342            .min(crate::shuffle::message::MAX_PAYLOAD_BYTES);
1343        let payload = serialize_batch_stream_bounded(
1344            &batch,
1345            crate::shuffle::message::MAX_PAYLOAD_BYTES,
1346            initial_capacity,
1347        )
1348        .map_err(|error| {
1349            io::Error::new(
1350                io::ErrorKind::InvalidData,
1351                format!("shuffle IPC encode: {error}"),
1352            )
1353        })?;
1354        // Encoding is complete; release the shallow batch clone before workspace admission is
1355        // reduced to the allocations retained by the queued wire message.
1356        drop(batch);
1357        let payload_len = payload.len();
1358        let payload_capacity = payload.capacity();
1359        if payload_len == 0
1360            || payload_len > crate::shuffle::message::MAX_PAYLOAD_BYTES
1361            || payload_capacity > crate::shuffle::message::MAX_PAYLOAD_BYTES
1362        {
1363            return Err(io::Error::new(
1364                io::ErrorKind::InvalidInput,
1365                format!(
1366                    "shuffle IPC payload uses {payload_len} bytes ({payload_capacity} allocated); limit is {}",
1367                    crate::shuffle::message::MAX_PAYLOAD_BYTES
1368                ),
1369            ));
1370        }
1371        validate_ipc_schema_header(&payload, payload_len)
1372            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
1373        let route_bytes = routed_vnodes
1374            .capacity()
1375            .checked_mul(std::mem::size_of::<u32>())
1376            .ok_or_else(|| io::Error::other("shuffle route accounting overflow"))?;
1377        let retained_bytes = payload_capacity
1378            .checked_add(stage.capacity())
1379            .and_then(|bytes| bytes.checked_add(route_bytes))
1380            .and_then(|bytes| bytes.checked_add(FRAME_METADATA_BYTES))
1381            .ok_or_else(|| io::Error::other("shuffle outbound accounting overflow"))?;
1382        budget.shrink_to(retained_bytes)?;
1383        Ok((
1384            PreparedMessage::Data {
1385                stage,
1386                routed_vnodes,
1387                arrow_ipc: Bytes::from(payload),
1388            },
1389            budget,
1390        ))
1391    }
1392
1393    async fn prepare_outbound_message_with_hook<F>(
1394        msg: &ShuffleMessage,
1395        budget: OutboundReservation,
1396        before_blocking_encode: F,
1397    ) -> io::Result<(PreparedMessage, OutboundReservation)>
1398    where
1399        F: FnOnce() + Send + 'static,
1400    {
1401        match msg {
1402            ShuffleMessage::Barrier(barrier) => Ok((PreparedMessage::Barrier(*barrier), budget)),
1403            ShuffleMessage::Frontier {
1404                stage,
1405                watermark,
1406                idle,
1407            } => Ok((
1408                PreparedMessage::Frontier {
1409                    stage: stage.clone(),
1410                    watermark: *watermark,
1411                    idle: *idle,
1412                },
1413                budget,
1414            )),
1415            ShuffleMessage::Data {
1416                stage,
1417                routed_vnodes,
1418                batch,
1419            } => {
1420                let stage = stage.clone();
1421                let routed_vnodes = routed_vnodes.to_vec();
1422                let batch = batch.clone();
1423                let logical_bytes =
1424                    crate::shuffle::routing::logical_batch_bytes(&batch).map_err(|error| {
1425                        io::Error::new(io::ErrorKind::InvalidInput, error.to_string())
1426                    })?;
1427                let schema_bytes = schema_memory_size(batch.schema().as_ref());
1428                let offload = logical_bytes >= BLOCKING_IPC_THRESHOLD_BYTES;
1429                if offload {
1430                    tokio::task::spawn_blocking(move || {
1431                        before_blocking_encode();
1432                        encode_outbound_data(
1433                            stage,
1434                            routed_vnodes,
1435                            batch,
1436                            logical_bytes,
1437                            schema_bytes,
1438                            budget,
1439                        )
1440                    })
1441                    .await
1442                    .map_err(|error| io::Error::other(format!("shuffle encoder task: {error}")))?
1443                } else {
1444                    encode_outbound_data(
1445                        stage,
1446                        routed_vnodes,
1447                        batch,
1448                        logical_bytes,
1449                        schema_bytes,
1450                        budget,
1451                    )
1452                }
1453            }
1454        }
1455    }
1456
1457    async fn prepare_outbound_message(
1458        msg: &ShuffleMessage,
1459        budget: OutboundReservation,
1460    ) -> io::Result<(PreparedMessage, OutboundReservation)> {
1461        prepare_outbound_message_with_hook(msg, budget, || {}).await
1462    }
1463
1464    /// Full identity of one ordered connection. Sequence continuity is scoped to both process
1465    /// incarnations and the assignment; `stream_id` distinguishes reconnects inside that scope.
1466    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1467    struct StreamFence {
1468        sender_node_id: ShufflePeerId,
1469        sender_incarnation: Uuid,
1470        receiver_incarnation: Uuid,
1471        stream_id: Uuid,
1472        assignment_version: u64,
1473        assignment_certificate_digest: [u8; 32],
1474        recovery_gen: u64,
1475        topology: Option<ShuffleTopologyFence>,
1476    }
1477
1478    /// Locally admitted assignment authority. The digest binds both the ordered owner map and
1479    /// every participant's exact process incarnation; a scalar version alone is not authority.
1480    #[derive(Debug)]
1481    struct InstalledAssignment {
1482        fence: CheckpointAssignmentFence,
1483        topology: Option<ShuffleTopologyFence>,
1484        digest: [u8; 32],
1485        owners: Arc<[ShufflePeerId]>,
1486    }
1487
1488    #[derive(Clone)]
1489    struct ScopeLease {
1490        assignment: Arc<InstalledAssignment>,
1491        recovery_gen: u64,
1492        cancel: CancellationToken,
1493    }
1494
1495    impl ScopeLease {
1496        fn matches_fence(&self, fence: &StreamFence) -> bool {
1497            !self.cancel.is_cancelled()
1498                && self.assignment.fence.assignment_version == fence.assignment_version
1499                && self.assignment.digest == fence.assignment_certificate_digest
1500                && self.recovery_gen == fence.recovery_gen
1501                && self.assignment.topology == fence.topology
1502        }
1503    }
1504
1505    impl InstalledAssignment {
1506        fn for_process(
1507            fence: &CheckpointAssignmentFence,
1508            owners: &[ShufflePeerId],
1509            local_id: ShufflePeerId,
1510            local_incarnation: Uuid,
1511        ) -> io::Result<Arc<Self>> {
1512            if !fence.is_canonical()
1513                || !fence.matches_owner_map(owners)
1514                || owners.iter().any(|owner| !fence.contains(*owner))
1515                || fence.participant_incarnation(local_id) != Some(local_incarnation)
1516            {
1517                return Err(io::Error::new(
1518                    io::ErrorKind::InvalidInput,
1519                    "shuffle assignment scope does not bind this process and exact owner map",
1520                ));
1521            }
1522            Ok(Arc::new(Self {
1523                digest: fence.digest(),
1524                topology: None,
1525                fence: fence.clone(),
1526                owners: Arc::from(owners),
1527            }))
1528        }
1529
1530        fn certifies(&self, node_id: ShufflePeerId, incarnation: Uuid) -> bool {
1531            self.fence.participant_incarnation(node_id) == Some(incarnation)
1532        }
1533
1534        fn matches_stream_sender(&self, fence: &StreamFence) -> bool {
1535            self.fence.assignment_version == fence.assignment_version
1536                && self.digest == fence.assignment_certificate_digest
1537                && self.topology == fence.topology
1538                && self.certifies(fence.sender_node_id, fence.sender_incarnation)
1539        }
1540
1541        fn owns_vnode(&self, node_id: ShufflePeerId, vnode: u32) -> bool {
1542            usize::try_from(vnode)
1543                .ok()
1544                .and_then(|vnode| self.owners.get(vnode))
1545                .is_some_and(|owner| *owner == node_id)
1546        }
1547    }
1548
1549    struct PendingHandshake {
1550        fence: StreamFence,
1551        issued_at: std::time::Instant,
1552    }
1553
1554    #[derive(Default)]
1555    struct PendingHandshakes(Mutex<FxHashMap<ShufflePeerId, PendingHandshake>>);
1556
1557    impl PendingHandshakes {
1558        fn clear(&self) {
1559            self.0.lock().clear();
1560        }
1561    }
1562
1563    struct ActiveStreamEntry {
1564        owner: Arc<()>,
1565        cancel: CancellationToken,
1566    }
1567
1568    #[derive(Default)]
1569    struct ActiveStreamRegistry {
1570        streams: Mutex<FxHashMap<ShufflePeerId, ActiveStreamEntry>>,
1571    }
1572
1573    impl ActiveStreamRegistry {
1574        fn replace(
1575            self: &Arc<Self>,
1576            fence: &StreamFence,
1577            parent_cancel: &CancellationToken,
1578        ) -> ActiveStreamLease {
1579            let key = fence.sender_node_id;
1580            let cancel = parent_cancel.child_token();
1581            let owner = Arc::new(());
1582            let previous = self.streams.lock().insert(
1583                key,
1584                ActiveStreamEntry {
1585                    owner: Arc::clone(&owner),
1586                    cancel: cancel.clone(),
1587                },
1588            );
1589            if let Some(previous) = previous {
1590                previous.cancel.cancel();
1591            }
1592            ActiveStreamLease {
1593                registry: Arc::clone(self),
1594                key,
1595                owner,
1596                cancel,
1597                permit: None,
1598            }
1599        }
1600    }
1601
1602    struct ActiveStreamLease {
1603        registry: Arc<ActiveStreamRegistry>,
1604        key: ShufflePeerId,
1605        owner: Arc<()>,
1606        cancel: CancellationToken,
1607        permit: Option<OwnedSemaphorePermit>,
1608    }
1609
1610    impl ActiveStreamLease {
1611        async fn acquire_permit(&mut self, permits: &Arc<Semaphore>) -> Result<(), tonic::Status> {
1612            let permit = tokio::select! {
1613                biased;
1614                () = self.cancel.cancelled() => return Err(tonic::Status::cancelled(
1615                    "shuffle stream was superseded",
1616                )),
1617                permit = Arc::clone(permits).acquire_owned() => permit.map_err(|_| {
1618                    tonic::Status::unavailable("shuffle stream admission closed")
1619                })?,
1620            };
1621            self.permit = Some(permit);
1622            Ok(())
1623        }
1624    }
1625
1626    impl Drop for ActiveStreamLease {
1627        fn drop(&mut self) {
1628            self.cancel.cancel();
1629            self.permit.take();
1630            let mut streams = self.registry.streams.lock();
1631            if streams
1632                .get(&self.key)
1633                .is_some_and(|entry| Arc::ptr_eq(&entry.owner, &self.owner))
1634            {
1635                streams.remove(&self.key);
1636            }
1637        }
1638    }
1639
1640    struct FragmentAssembly {
1641        stage: String,
1642        routed_vnodes: Vec<u32>,
1643        recovery_gen: u64,
1644        seq: u64,
1645        fragment_count: u32,
1646        total_payload_bytes: usize,
1647        next_fragment: u32,
1648        payload: Vec<u8>,
1649        budget: InboundReservation,
1650    }
1651
1652    struct CompleteData {
1653        stage: String,
1654        routed_vnodes: Vec<u32>,
1655        seq: u64,
1656        arrow_ipc: Vec<u8>,
1657        budget: InboundReservation,
1658    }
1659
1660    fn validate_ipc_schema_header(
1661        payload: &[u8],
1662        total_payload_bytes: usize,
1663    ) -> Result<(), String> {
1664        const CONTINUATION_MARKER: [u8; 4] = [0xff; 4];
1665
1666        if payload.len() < 8 || payload[..4] != CONTINUATION_MARKER {
1667            return Err("shuffle IPC must start with a modern Arrow schema message".into());
1668        }
1669        let metadata_len = usize::try_from(u32::from_le_bytes(
1670            payload[4..8]
1671                .try_into()
1672                .expect("validated Arrow IPC prefix length"),
1673        ))
1674        .map_err(|_| "shuffle IPC schema length is invalid".to_string())?;
1675        if metadata_len == 0 || metadata_len > MAX_SCHEMA_WIRE_BYTES {
1676            return Err("shuffle IPC schema message exceeds its wire bound".into());
1677        }
1678        let end = 8usize
1679            .checked_add(metadata_len)
1680            .ok_or_else(|| "shuffle IPC schema length overflow".to_string())?;
1681        if end > payload.len() || end > total_payload_bytes {
1682            return Err("shuffle IPC schema message is not complete in fragment zero".into());
1683        }
1684        let message = arrow_ipc::root_as_message(&payload[8..end])
1685            .map_err(|error| format!("invalid shuffle IPC schema message: {error}"))?;
1686        if message.header_type() != arrow_ipc::MessageHeader::Schema
1687            || message.header_as_schema().is_none()
1688            || message.bodyLength() != 0
1689        {
1690            return Err("shuffle IPC leading message is not a bodyless schema".into());
1691        }
1692        Ok(())
1693    }
1694
1695    fn validate_fragment(fragment: &RoutedData) -> Result<usize, String> {
1696        let fragment_count = usize::try_from(fragment.fragment_count)
1697            .map_err(|_| "invalid shuffle fragment count".to_string())?;
1698        let total_payload_bytes = usize::try_from(fragment.total_payload_bytes)
1699            .map_err(|_| "invalid shuffle payload size".to_string())?;
1700        if fragment_count == 0
1701            || fragment_count > MAX_FRAGMENTS
1702            || total_payload_bytes == 0
1703            || total_payload_bytes > crate::shuffle::message::MAX_PAYLOAD_BYTES
1704            || fragment.arrow_ipc.is_empty()
1705            || fragment.arrow_ipc.len() > MAX_WIRE_PAYLOAD_BYTES
1706            || fragment_count != total_payload_bytes.div_ceil(MAX_WIRE_PAYLOAD_BYTES)
1707            || usize::try_from(fragment.fragment_index).unwrap_or(usize::MAX) >= fragment_count
1708        {
1709            return Err("invalid shuffle fragment bounds".into());
1710        }
1711        let index = usize::try_from(fragment.fragment_index)
1712            .map_err(|_| "invalid shuffle fragment index".to_string())?;
1713        let expected_len = if index + 1 == fragment_count {
1714            total_payload_bytes - index * MAX_WIRE_PAYLOAD_BYTES
1715        } else {
1716            MAX_WIRE_PAYLOAD_BYTES
1717        };
1718        if fragment.arrow_ipc.len() != expected_len {
1719            return Err("shuffle fragment length does not match its declared payload".into());
1720        }
1721        if index == 0 {
1722            validate_ipc_schema_header(&fragment.arrow_ipc, total_payload_bytes)?;
1723            let routes_are_canonical = fragment
1724                .routed_vnodes
1725                .windows(2)
1726                .all(|pair| pair[0] < pair[1]);
1727            if fragment.stage.is_empty()
1728                || fragment.stage.len() > MAX_STAGE_NAME_BYTES
1729                || !routes_are_canonical
1730                || fragment.routed_vnodes.len()
1731                    > usize::try_from(crate::state::MAX_KEY_GROUP_COUNT).unwrap_or(usize::MAX)
1732                || fragment.routed_vnodes.is_empty()
1733            {
1734                return Err("shuffle fragment-zero metadata is empty or non-canonical".into());
1735            }
1736        } else if !fragment.stage.is_empty() || !fragment.routed_vnodes.is_empty() {
1737            return Err("shuffle continuation fragment repeated logical metadata".into());
1738        }
1739        Ok(total_payload_bytes)
1740    }
1741
1742    fn retained_batch_metadata_bytes(
1743        stage: &String,
1744        routed_vnodes: &[u32],
1745        batch: &RecordBatch,
1746    ) -> Result<usize, tonic::Status> {
1747        let schema_bytes = schema_memory_size(batch.schema().as_ref());
1748        if schema_bytes > MAX_DECODED_SCHEMA_MEMORY_BYTES {
1749            return Err(tonic::Status::resource_exhausted(
1750                "decoded shuffle schema exceeds its memory bound",
1751            ));
1752        }
1753        let structure_bytes = batch.columns().iter().try_fold(
1754            std::mem::size_of::<RecordBatch>()
1755                + batch
1756                    .num_columns()
1757                    .saturating_mul(std::mem::size_of::<arrow_array::ArrayRef>()),
1758            |total, column| {
1759                let array_bytes = column
1760                    .get_array_memory_size()
1761                    .saturating_sub(column.get_buffer_memory_size());
1762                total.checked_add(array_bytes).ok_or_else(|| {
1763                    tonic::Status::internal("shuffle array structure accounting overflow")
1764                })
1765            },
1766        )?;
1767        if structure_bytes > MAX_DECODED_ARRAY_STRUCTURE_BYTES {
1768            return Err(tonic::Status::resource_exhausted(
1769                "decoded shuffle array structure exceeds its memory bound",
1770            ));
1771        }
1772        let route_bytes = routed_vnodes
1773            .len()
1774            .checked_mul(std::mem::size_of::<u32>())
1775            .ok_or_else(|| tonic::Status::internal("shuffle route accounting overflow"))?;
1776        let metadata_bytes = schema_bytes
1777            .checked_add(structure_bytes)
1778            .and_then(|bytes| bytes.checked_add(stage.capacity()))
1779            .and_then(|bytes| bytes.checked_add(route_bytes))
1780            .and_then(|bytes| bytes.checked_add(RETAINED_BATCH_ENVELOPE_BYTES))
1781            .ok_or_else(|| tonic::Status::internal("shuffle metadata accounting overflow"))?;
1782        if metadata_bytes > INBOUND_BATCH_METADATA_BYTES {
1783            return Err(tonic::Status::resource_exhausted(
1784                "decoded shuffle metadata exceeds its admission bound",
1785            ));
1786        }
1787        Ok(metadata_bytes)
1788    }
1789
1790    fn push_fragment(
1791        assembly: &mut Option<FragmentAssembly>,
1792        fragment: &RoutedData,
1793        budget: Option<InboundReservation>,
1794    ) -> Result<Option<CompleteData>, String> {
1795        let total_payload_bytes = validate_fragment(fragment)?;
1796
1797        if fragment.fragment_index == 0 {
1798            if assembly.is_some() {
1799                return Err("shuffle fragments interleaved across logical frames".into());
1800            }
1801            let budget = budget.ok_or_else(|| {
1802                "shuffle fragment zero was not admitted by byte budget".to_string()
1803            })?;
1804            *assembly = Some(FragmentAssembly {
1805                stage: fragment.stage.clone(),
1806                routed_vnodes: fragment.routed_vnodes.clone(),
1807                recovery_gen: fragment.recovery_gen,
1808                seq: fragment.seq,
1809                fragment_count: fragment.fragment_count,
1810                total_payload_bytes,
1811                next_fragment: 0,
1812                payload: Vec::with_capacity(total_payload_bytes),
1813                budget,
1814            });
1815        } else if budget.is_some() {
1816            return Err("shuffle continuation fragment carried a new byte reservation".into());
1817        }
1818        let current = assembly
1819            .as_mut()
1820            .ok_or_else(|| "shuffle fragment arrived without fragment zero".to_string())?;
1821        let first_metadata_changed = fragment.fragment_index == 0
1822            && (current.stage != fragment.stage || current.routed_vnodes != fragment.routed_vnodes);
1823        if first_metadata_changed
1824            || current.recovery_gen != fragment.recovery_gen
1825            || current.seq != fragment.seq
1826            || current.fragment_count != fragment.fragment_count
1827            || current.total_payload_bytes != total_payload_bytes
1828            || current.next_fragment != fragment.fragment_index
1829        {
1830            return Err("shuffle fragment metadata or order changed mid-frame".into());
1831        }
1832        current.payload.extend_from_slice(&fragment.arrow_ipc);
1833        current.next_fragment += 1;
1834        if current.next_fragment != current.fragment_count {
1835            return Ok(None);
1836        }
1837        let complete = assembly.take().expect("completed fragment assembly exists");
1838        if complete.payload.len() != complete.total_payload_bytes {
1839            return Err("reassembled shuffle payload length mismatch".into());
1840        }
1841        Ok(Some(CompleteData {
1842            stage: complete.stage,
1843            routed_vnodes: complete.routed_vnodes,
1844            seq: complete.seq,
1845            arrow_ipc: complete.payload,
1846            budget: complete.budget,
1847        }))
1848    }
1849
1850    fn parse_uuid(raw: &[u8], field: &str) -> Result<Uuid, tonic::Status> {
1851        let value = Uuid::from_slice(raw)
1852            .map_err(|_| tonic::Status::invalid_argument(format!("invalid {field} UUID")))?;
1853        if value.is_nil() {
1854            return Err(tonic::Status::invalid_argument(format!("nil {field} UUID")));
1855        }
1856        Ok(value)
1857    }
1858
1859    fn parse_certificate_digest(raw: &[u8], field: &str) -> Result<[u8; 32], tonic::Status> {
1860        let digest: [u8; 32] = raw
1861            .try_into()
1862            .map_err(|_| tonic::Status::invalid_argument(format!("invalid {field}")))?;
1863        if digest == [0; 32] {
1864            return Err(tonic::Status::invalid_argument(format!("zero {field}")));
1865        }
1866        Ok(digest)
1867    }
1868
1869    fn hello_for(node_id: ShufflePeerId, fence: &StreamFence) -> Hello {
1870        Hello {
1871            node_id,
1872            sender_incarnation: fence.sender_incarnation.as_bytes().to_vec(),
1873            receiver_incarnation: fence.receiver_incarnation.as_bytes().to_vec(),
1874            stream_id: fence.stream_id.as_bytes().to_vec(),
1875            assignment_version: fence.assignment_version,
1876            recovery_gen: fence.recovery_gen,
1877            assignment_certificate_digest: fence.assignment_certificate_digest.to_vec(),
1878            topology_version: fence.topology.map_or(0, ShuffleTopologyFence::version),
1879            topology_manifest_sha256: fence
1880                .topology
1881                .map_or_else(Vec::new, |t| t.manifest_sha256().to_vec()),
1882        }
1883    }
1884
1885    fn fence_from_hello(hello: &Hello) -> Result<StreamFence, tonic::Status> {
1886        if hello.node_id == 0 || hello.assignment_version == 0 {
1887            return Err(tonic::Status::failed_precondition(
1888                "shuffle stream requires assigned nodes and a nonzero assignment version",
1889            ));
1890        }
1891        Ok(StreamFence {
1892            sender_node_id: hello.node_id,
1893            sender_incarnation: parse_uuid(&hello.sender_incarnation, "sender incarnation")?,
1894            receiver_incarnation: parse_uuid(&hello.receiver_incarnation, "receiver incarnation")?,
1895            stream_id: parse_uuid(&hello.stream_id, "stream id")?,
1896            assignment_version: hello.assignment_version,
1897            assignment_certificate_digest: parse_certificate_digest(
1898                &hello.assignment_certificate_digest,
1899                "assignment certificate digest",
1900            )?,
1901            recovery_gen: hello.recovery_gen,
1902            topology: parse_topology_fence(
1903                hello.topology_version,
1904                &hello.topology_manifest_sha256,
1905            )?,
1906        })
1907    }
1908
1909    /// Frame one admitted, self-contained logical message without copying its IPC allocation.
1910    fn frame_message(out: Outbound) -> Result<Encoded, tonic::Status> {
1911        let Outbound {
1912            gen,
1913            assignment_version,
1914            seq,
1915            msg,
1916            assignment_digest,
1917            _budget: budget,
1918        } = out;
1919        let frames = match msg {
1920            PreparedMessage::Barrier(b) => {
1921                validate_checkpoint_barrier(b)
1922                    .map_err(|error| tonic::Status::invalid_argument(error.to_string()))?;
1923                let assignment_digest = assignment_digest.ok_or_else(|| {
1924                    tonic::Status::failed_precondition(
1925                        "shuffle checkpoint barrier has no assignment certificate",
1926                    )
1927                })?;
1928                VecDeque::from([ShuffleFrame {
1929                    kind: Some(shuffle_frame::Kind::Barrier(Barrier {
1930                        checkpoint_id: b.checkpoint_id,
1931                        epoch: b.epoch,
1932                        flags: b.flags,
1933                        last_seq: seq,
1934                        assignment_version,
1935                        assignment_digest: assignment_digest.to_vec(),
1936                        recovery_gen: gen,
1937                    })),
1938                }])
1939            }
1940            PreparedMessage::Data {
1941                mut stage,
1942                routed_vnodes,
1943                arrow_ipc,
1944            } => {
1945                let total = arrow_ipc.len();
1946                if total == 0 || total > crate::shuffle::message::MAX_PAYLOAD_BYTES {
1947                    return Err(tonic::Status::resource_exhausted(format!(
1948                        "shuffle IPC payload is {total} bytes; limit is {}",
1949                        crate::shuffle::message::MAX_PAYLOAD_BYTES
1950                    )));
1951                }
1952                let fragment_count = total.div_ceil(MAX_WIRE_PAYLOAD_BYTES);
1953                if fragment_count == 0 || fragment_count > MAX_FRAGMENTS {
1954                    return Err(tonic::Status::resource_exhausted(
1955                        "shuffle IPC payload needs too many fragments",
1956                    ));
1957                }
1958                let fragment_count = u32::try_from(fragment_count)
1959                    .map_err(|_| tonic::Status::resource_exhausted("too many fragments"))?;
1960                let total_payload_bytes = u32::try_from(total)
1961                    .map_err(|_| tonic::Status::resource_exhausted("payload is too large"))?;
1962                let mut routes = routed_vnodes;
1963                let mut frames = VecDeque::with_capacity(fragment_count as usize);
1964                for index in 0..fragment_count {
1965                    let index = index as usize;
1966                    let start = index * MAX_WIRE_PAYLOAD_BYTES;
1967                    let end = (start + MAX_WIRE_PAYLOAD_BYTES).min(total);
1968                    let first = index == 0;
1969                    frames.push_back(ShuffleFrame {
1970                        kind: Some(shuffle_frame::Kind::Data(RoutedData {
1971                            stage: if first {
1972                                std::mem::take(&mut stage)
1973                            } else {
1974                                String::new()
1975                            },
1976                            routed_vnodes: if first {
1977                                std::mem::take(&mut routes)
1978                            } else {
1979                                Vec::new()
1980                            },
1981                            arrow_ipc: arrow_ipc.slice(start..end),
1982                            recovery_gen: gen,
1983                            seq,
1984                            fragment_index: u32::try_from(index)
1985                                .expect("fragment count is bounded by MAX_FRAGMENTS"),
1986                            fragment_count,
1987                            total_payload_bytes,
1988                        })),
1989                    });
1990                }
1991                frames
1992            }
1993            PreparedMessage::Frontier {
1994                stage,
1995                watermark,
1996                idle,
1997            } => {
1998                validate_frontier(&stage, watermark)
1999                    .map_err(|error| tonic::Status::invalid_argument(error.to_string()))?;
2000                VecDeque::from([ShuffleFrame {
2001                    kind: Some(shuffle_frame::Kind::Frontier(WireFrontier {
2002                        stage,
2003                        watermark,
2004                        idle,
2005                        recovery_gen: gen,
2006                        seq,
2007                    })),
2008                }])
2009            }
2010        };
2011        Ok(Encoded {
2012            frames,
2013            _budget: budget,
2014        })
2015    }
2016
2017    /// One lazily-opened client-streaming call to a peer. The driver task
2018    /// frames queued messages and feeds the gRPC request stream, flipping
2019    /// `alive=false` on the first transport/connect error so the next `send_to`
2020    /// reconnects.
2021    struct PeerConn {
2022        tx: MAsyncTx<mpsc::Array<Outbound>>,
2023        byte_budget: Arc<Semaphore>,
2024        control_byte_budget: Arc<Semaphore>,
2025        counters: Arc<PeerSendCounters>,
2026        /// Sequence allocation and queue insertion must be one ordered operation. Without this,
2027        /// concurrent callers can enqueue sequence 1 before sequence 0.
2028        send_lock: tokio::sync::Mutex<()>,
2029        alive: Arc<AtomicBool>,
2030        driver: JoinHandle<()>,
2031        fence: StreamFence,
2032    }
2033
2034    impl PeerConn {
2035        fn is_alive(&self) -> bool {
2036            // `is_finished()` also catches a driver cancelled without flipping
2037            // `alive` (an in-process restart drops its runtime), which would
2038            // otherwise zombie: fails every send yet never reconnects.
2039            self.alive.load(Ordering::Acquire) && !self.driver.is_finished()
2040        }
2041    }
2042
2043    impl Drop for PeerConn {
2044        fn drop(&mut self) {
2045            self.driver.abort();
2046        }
2047    }
2048
2049    struct OpenCall {
2050        local_id: ShufflePeerId,
2051        peer: ShufflePeerId,
2052        addr: SocketAddr,
2053        sender_incarnation: Uuid,
2054        assignment_version: u64,
2055        assignment_certificate_digest: [u8; 32],
2056        expected_receiver_incarnation: Uuid,
2057        recovery_gen: u64,
2058        topology: Option<ShuffleTopologyFence>,
2059        pipeline_identity: Option<[u8; 32]>,
2060        current_assignment: Arc<AtomicU64>,
2061        current_recovery_gen: Arc<AtomicU64>,
2062        scope_cancel: CancellationToken,
2063        counters: Arc<PeerSendCounters>,
2064    }
2065
2066    type ConnectLock = Arc<tokio::sync::Mutex<()>>;
2067
2068    /// Lazy pool of ordered streams per peer.
2069    pub struct ShuffleSender {
2070        local_id: ShufflePeerId,
2071        sender_incarnation: Uuid,
2072        peers: Mutex<FxHashMap<ShufflePeerId, SocketAddr>>,
2073        pool: Mutex<FxHashMap<ShufflePeerId, Arc<PeerConn>>>,
2074        /// Serialises reconnects per peer without head-of-line blocking unrelated peers.
2075        connect_locks: Mutex<FxHashMap<ShufflePeerId, ConnectLock>>,
2076        kv: Option<Arc<dyn ClusterKv>>,
2077        assignment: Arc<RwLock<Option<Arc<InstalledAssignment>>>>,
2078        assignment_version: Arc<AtomicU64>,
2079        scope_cancel: Arc<RwLock<CancellationToken>>,
2080        process_lease: Arc<ProcessLeaseGate>,
2081        /// True only for a transient durable-authority outage. The exact retained certificate
2082        /// may be reactivated without resetting its sequence domain.
2083        assignment_suspended: AtomicBool,
2084        /// Stamped onto every outbound data message; bumped by a coordinated rewind.
2085        recovery_gen: Arc<AtomicU64>,
2086        topology_version: Arc<AtomicU64>,
2087        pipeline_identity: RwLock<Option<[u8; 32]>>,
2088        /// Ordered data/frontier frames enqueued per peer. Lives here, not on `PeerConn`, so it
2089        /// survives a reconnect that discards a queue and leaves a detectable gap.
2090        seqs: Mutex<FxHashMap<ShufflePeerId, u64>>,
2091        /// Flow totals per peer, kept here for the same reason so they survive reconnects, and
2092        /// the send stall timeout.
2093        flow: SendFlow,
2094        checkpointed_control_node_budget: Arc<Semaphore>,
2095        node_budget: Arc<Semaphore>,
2096        #[cfg(test)]
2097        post_enqueue_pause: Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
2098    }
2099
2100    impl std::fmt::Debug for ShuffleSender {
2101        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2102            f.debug_struct("ShuffleSender")
2103                .field("local_id", &self.local_id)
2104                .finish_non_exhaustive()
2105        }
2106    }
2107
2108    impl ShuffleSender {
2109        /// Empty sender; peers arrive via [`Self::register_peer`] or KV discovery.
2110        ///
2111        /// # Panics
2112        ///
2113        /// Panics when the node ID is zero or the process incarnation is nil.
2114        #[must_use]
2115        pub fn new(local_id: ShufflePeerId, incarnation: Uuid) -> Self {
2116            assert!(local_id != 0, "shuffle sender node id must be nonzero");
2117            assert!(
2118                !incarnation.is_nil(),
2119                "shuffle sender incarnation must be non-nil"
2120            );
2121            Self {
2122                local_id,
2123                sender_incarnation: incarnation,
2124                peers: Mutex::new(FxHashMap::default()),
2125                pool: Mutex::new(FxHashMap::default()),
2126                connect_locks: Mutex::new(FxHashMap::default()),
2127                kv: None,
2128                assignment: Arc::new(RwLock::new(None)),
2129                assignment_version: Arc::new(AtomicU64::new(0)),
2130                scope_cancel: Arc::new(RwLock::new(cancelled_token())),
2131                process_lease: Arc::new(ProcessLeaseGate::default()),
2132                assignment_suspended: AtomicBool::new(false),
2133                recovery_gen: Arc::new(AtomicU64::new(0)),
2134                topology_version: Arc::new(AtomicU64::new(0)),
2135                pipeline_identity: RwLock::new(None),
2136                seqs: Mutex::new(FxHashMap::default()),
2137                flow: SendFlow::default(),
2138                checkpointed_control_node_budget: Arc::new(Semaphore::new(
2139                    CHECKPOINTED_CONTROL_NODE_BUDGET_BYTES,
2140                )),
2141                node_budget: Arc::new(Semaphore::new(OUTBOUND_NODE_BUDGET_BYTES)),
2142                #[cfg(test)]
2143                post_enqueue_pause: Mutex::new(None),
2144            }
2145        }
2146
2147        #[cfg(test)]
2148        pub(crate) fn pause_next_send_after_enqueue_for_test(
2149            &self,
2150        ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
2151            let entered = Arc::new(tokio::sync::Notify::new());
2152            let release = Arc::new(tokio::sync::Notify::new());
2153            let replaced = self
2154                .post_enqueue_pause
2155                .lock()
2156                .replace((Arc::clone(&entered), Arc::clone(&release)));
2157            assert!(
2158                replaced.is_none(),
2159                "post-enqueue send pause already installed"
2160            );
2161            (entered, release)
2162        }
2163
2164        /// Node id bound into every outbound stream handshake.
2165        #[must_use]
2166        pub const fn local_id(&self) -> ShufflePeerId {
2167            self.local_id
2168        }
2169
2170        /// Bind outbound admission to this process's renewable cluster lease.
2171        ///
2172        /// This must be installed before an assignment certificate is activated.
2173        ///
2174        /// # Errors
2175        /// Returns an error for an expired lease, a different previously installed deadline, or
2176        /// an already-active assignment.
2177        pub fn install_process_lease_deadline(
2178            &self,
2179            deadline: Arc<LeaseDeadline>,
2180        ) -> io::Result<()> {
2181            let _assignment = self.assignment.write();
2182            if self.assignment_version.load(Ordering::Acquire) != 0
2183                && !self.process_lease.is_installed_deadline(&deadline)
2184            {
2185                return Err(io::Error::new(
2186                    io::ErrorKind::InvalidInput,
2187                    "shuffle process lease must be installed before assignment activation",
2188                ));
2189            }
2190            self.process_lease.install(deadline)
2191        }
2192
2193        /// Bind both directions of one local shuffle fabric to the same process lease.
2194        ///
2195        /// Validation and installation are serialized across both handles, so an incompatible
2196        /// receiver cannot leave only the sender bound.
2197        ///
2198        /// # Errors
2199        /// Returns an error without changing either handle when the deadline is expired, either
2200        /// handle is already bound to a different deadline, or an active assignment prevents a
2201        /// new binding.
2202        pub fn bind_process_lease_deadline_pair(
2203            &self,
2204            receiver: &ShuffleReceiver,
2205            deadline: Arc<LeaseDeadline>,
2206        ) -> io::Result<()> {
2207            let _sender_assignment = self.assignment.write();
2208            let _receiver_assignment = receiver.assignment.write();
2209            if self.assignment_version.load(Ordering::Acquire) != 0
2210                && !self.process_lease.is_installed_deadline(&deadline)
2211            {
2212                return Err(io::Error::new(
2213                    io::ErrorKind::InvalidInput,
2214                    "shuffle process lease must be installed before outbound assignment activation",
2215                ));
2216            }
2217            if receiver.assignment_version.load(Ordering::Acquire) != 0
2218                && !receiver.process_lease.is_installed_deadline(&deadline)
2219            {
2220                return Err(io::Error::new(
2221                    io::ErrorKind::InvalidInput,
2222                    "shuffle process lease must be installed before inbound assignment activation",
2223                ));
2224            }
2225            ProcessLeaseGate::install_pair(
2226                self.process_lease.as_ref(),
2227                receiver.process_lease.as_ref(),
2228                deadline,
2229            )
2230        }
2231
2232        #[cfg(test)]
2233        pub(crate) fn install_live_process_lease_for_test(&self) {
2234            self.process_lease.install_live_for_test();
2235        }
2236
2237        /// Install the exact assignment certificate accepted by outbound shuffle streams.
2238        /// Changing scope closes all pooled streams and resets their scoped sequences.
2239        ///
2240        /// # Errors
2241        /// Returns an error for a malformed certificate, a same-version certificate conflict,
2242        /// or a certificate that does not bind this exact sender process.
2243        pub fn install_assignment_fence(
2244            &self,
2245            fence: &CheckpointAssignmentFence,
2246            owners: &[ShufflePeerId],
2247        ) -> io::Result<bool> {
2248            let mut next = InstalledAssignment::for_process(
2249                fence,
2250                owners,
2251                self.local_id,
2252                self.sender_incarnation,
2253            )?;
2254            // Publish and reset under one scope lock and the same lock order used by connection
2255            // installation / sequence allocation. A new-scope frame cannot reuse sequence zero.
2256            let mut assignment = self.assignment.write();
2257            self.process_lease.require_live_io()?;
2258            if let Some(current) = assignment.as_ref() {
2259                next = Arc::new(InstalledAssignment {
2260                    fence: next.fence.clone(),
2261                    digest: next.digest,
2262                    owners: Arc::clone(&next.owners),
2263                    topology: current.topology,
2264                });
2265                if next.fence.assignment_version < current.fence.assignment_version {
2266                    return Ok(false);
2267                }
2268                if next.fence.assignment_version == current.fence.assignment_version {
2269                    if next.digest == current.digest
2270                        && next.fence == current.fence
2271                        && next.owners == current.owners
2272                    {
2273                        if self.assignment_version.load(Ordering::Acquire)
2274                            == next.fence.assignment_version
2275                        {
2276                            return Ok(false);
2277                        }
2278                        if self.assignment_suspended.load(Ordering::Acquire) {
2279                            rotate_scope_token(&self.scope_cancel, &self.process_lease, true);
2280                            self.pool.lock().clear();
2281                            self.connect_locks.lock().clear();
2282                            self.assignment_suspended.store(false, Ordering::Release);
2283                            self.assignment_version
2284                                .store(next.fence.assignment_version, Ordering::Release);
2285                            return Ok(true);
2286                        }
2287                        return Err(io::Error::new(
2288                            io::ErrorKind::InvalidData,
2289                            "an invalidated shuffle assignment requires a higher version",
2290                        ));
2291                    }
2292                    return Err(io::Error::new(
2293                        io::ErrorKind::InvalidData,
2294                        "conflicting shuffle assignment certificate for an installed version",
2295                    ));
2296                }
2297            }
2298            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
2299            let mut pool = self.pool.lock();
2300            let mut seqs = self.seqs.lock();
2301            pool.clear();
2302            seqs.clear();
2303            self.connect_locks.lock().clear();
2304            self.peers
2305                .lock()
2306                .retain(|peer, _| next.fence.contains(*peer));
2307            let version = next.fence.assignment_version;
2308            *assignment = Some(next);
2309            self.assignment_suspended.store(false, Ordering::Release);
2310            rotate_scope_token(&self.scope_cancel, &self.process_lease, true);
2311            self.assignment_version.store(version, Ordering::Release);
2312            Ok(true)
2313        }
2314
2315        /// Temporarily close outbound admission because the durable assignment authority could
2316        /// not be read. Unlike invalidation, this preserves the exact certificate and sequence
2317        /// counters so the same durable head can resume without manufacturing a delivery gap.
2318        pub fn suspend_assignment_fence(&self) {
2319            let assignment = self.assignment.write();
2320            if assignment.is_none() || self.assignment_version.load(Ordering::Acquire) == 0 {
2321                return;
2322            }
2323            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
2324            self.pool.lock().clear();
2325            self.connect_locks.lock().clear();
2326            self.assignment_suspended.store(true, Ordering::Release);
2327            self.assignment_version.store(0, Ordering::Release);
2328        }
2329
2330        /// Reject every stream while a newer assignment is being adopted but has not yet earned
2331        /// an owner-complete process certificate. The last certificate is retained only as a
2332        /// monotonic conflict floor; it is inactive while [`Self::assignment_version`] is zero.
2333        pub fn invalidate_assignment_fence(&self) {
2334            let _assignment = self.assignment.write();
2335            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
2336            let mut pool = self.pool.lock();
2337            let mut seqs = self.seqs.lock();
2338            self.assignment_suspended.store(false, Ordering::Release);
2339            self.assignment_version.store(0, Ordering::Release);
2340            pool.clear();
2341            seqs.clear();
2342            self.connect_locks.lock().clear();
2343        }
2344
2345        /// Assignment scope currently accepted for newly enqueued frames.
2346        #[must_use]
2347        pub fn assignment_version(&self) -> u64 {
2348            self.assignment_version.load(Ordering::Acquire)
2349        }
2350
2351        /// Digest of the exact assignment certificate currently active for outbound streams.
2352        #[must_use]
2353        pub fn active_assignment_digest(&self) -> Option<[u8; 32]> {
2354            let assignment = self.assignment.read();
2355            assignment.as_ref().and_then(|installed| {
2356                (self.assignment_version.load(Ordering::Acquire)
2357                    == installed.fence.assignment_version)
2358                    .then_some(installed.digest)
2359            })
2360        }
2361
2362        /// Advance the generation stamped onto outbound data frames. Called after a coordinated
2363        /// rewind so peers can discard anything this node produced before it.
2364        pub fn set_recovery_gen(&self, gen: u64) {
2365            // Recovery starts a new sequence domain. Publish, disconnect, and reset counters
2366            // under the same lock order used by connection installation.
2367            let _assignment = self.assignment.write();
2368            let previous = self.recovery_gen.load(Ordering::Acquire);
2369            if gen <= previous {
2370                return;
2371            }
2372            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
2373            let mut pool = self.pool.lock();
2374            let mut seqs = self.seqs.lock();
2375            self.recovery_gen.store(gen, Ordering::Release);
2376            pool.clear();
2377            seqs.clear();
2378            self.connect_locks.lock().clear();
2379            rotate_scope_token(
2380                &self.scope_cancel,
2381                &self.process_lease,
2382                self.assignment_version.load(Ordering::Acquire) != 0,
2383            );
2384        }
2385
2386        /// Recovery scope currently accepted for newly enqueued frames.
2387        #[must_use]
2388        pub fn recovery_gen(&self) -> u64 {
2389            self.recovery_gen.load(Ordering::Acquire)
2390        }
2391
2392        /// Process incarnation bound into every outbound stream handshake.
2393        #[must_use]
2394        pub const fn incarnation(&self) -> Uuid {
2395            self.sender_incarnation
2396        }
2397
2398        /// Consume a sequence without sending, modelling a frame the transport discarded.
2399        #[cfg(test)]
2400        pub fn burn_seq_for_test(&self, peer: ShufflePeerId) {
2401            *self.seqs.lock().entry(peer).or_insert(0) += 1;
2402        }
2403
2404        #[cfg(test)]
2405        pub(crate) fn tracked_resources_for_test(&self) -> (usize, usize, usize, usize) {
2406            (
2407                self.peers.lock().len(),
2408                self.pool.lock().len(),
2409                self.connect_locks.lock().len(),
2410                self.seqs.lock().len(),
2411            )
2412        }
2413
2414        /// Sender that falls back to `kv` discovery for peers not previously
2415        /// registered.
2416        #[must_use]
2417        pub fn with_kv(local_id: ShufflePeerId, kv: Arc<dyn ClusterKv>, incarnation: Uuid) -> Self {
2418            let mut s = Self::new(local_id, incarnation);
2419            s.kv = Some(kv);
2420            s
2421        }
2422
2423        /// Register (or update) a peer's shuffle address.
2424        pub fn register_peer(&self, peer: ShufflePeerId, addr: SocketAddr) {
2425            if peer == 0 || peer == self.local_id {
2426                return;
2427            }
2428            let mut peers = self.peers.lock();
2429            if !peers.contains_key(&peer) && peers.len() >= MAX_TRACKED_PEERS {
2430                tracing::warn!(peer, "shuffle peer address registry is full");
2431                return;
2432            }
2433            peers.insert(peer, addr);
2434        }
2435
2436        /// Send `msg` to `peer`, opening a client-streaming call if necessary.
2437        ///
2438        /// # Errors
2439        /// Returns `io::Error` when the peer is unregistered/undiscoverable, the
2440        /// endpoint cannot be built, or the per-peer stream has shut down.
2441        pub async fn send_to(&self, peer: ShufflePeerId, msg: &ShuffleMessage) -> io::Result<()> {
2442            self.send_to_inner(peer, msg, None, None, None, None).await
2443        }
2444
2445        /// Send only while the sender remains in `expected_assignment_version`.
2446        ///
2447        /// Routing paths use this to prevent data sliced with an old ownership publication from
2448        /// entering a stream opened under a newer assignment scope.
2449        ///
2450        /// # Errors
2451        /// Returns `io::Error` when the expected assignment is no longer current, in addition to
2452        /// the errors returned by [`Self::send_to`].
2453        pub async fn send_to_for_assignment(
2454            &self,
2455            peer: ShufflePeerId,
2456            expected_assignment_version: u64,
2457            msg: &ShuffleMessage,
2458        ) -> io::Result<()> {
2459            self.send_to_inner(
2460                peer,
2461                msg,
2462                Some(expected_assignment_version),
2463                None,
2464                None,
2465                None,
2466            )
2467            .await
2468        }
2469
2470        async fn send_to_inner(
2471            &self,
2472            peer: ShufflePeerId,
2473            msg: &ShuffleMessage,
2474            expected_assignment_version: Option<u64>,
2475            assignment_fence: Option<&CheckpointAssignmentFence>,
2476            topology: Option<ShuffleTopologyFence>,
2477            expected_recovery_generation: Option<u64>,
2478        ) -> io::Result<()> {
2479            if peer == 0 || peer == self.local_id {
2480                return Err(io::Error::new(
2481                    io::ErrorKind::InvalidInput,
2482                    "shuffle peer must be a different assigned node",
2483                ));
2484            }
2485            let admission_bytes = topology::outbound_admission_bytes(msg, assignment_fence)?;
2486            let scope = self.current_send_scope(
2487                expected_assignment_version,
2488                topology,
2489                expected_recovery_generation,
2490            )?;
2491            let conn = self.connection_for(peer, &scope).await?;
2492            let _send_guard = tokio::select! {
2493                biased;
2494                () = scope.cancel.cancelled() => return Err(scope_cancelled_io()),
2495                guard = conn.send_lock.lock() => guard,
2496            };
2497            self.validate_scope(&scope, expected_assignment_version)?;
2498            let assignment = &scope.assignment;
2499            if !assignment.matches_stream_sender(&conn.fence)
2500                || !assignment.certifies(peer, conn.fence.receiver_incarnation)
2501            {
2502                return Err(io::Error::new(
2503                    io::ErrorKind::ConnectionAborted,
2504                    "shuffle stream no longer matches the installed assignment certificate",
2505                ));
2506            }
2507            if let Some(fence) = assignment_fence {
2508                if *fence != assignment.fence
2509                    || fence.digest() != conn.fence.assignment_certificate_digest
2510                    || fence.participant_incarnation(self.local_id) != Some(self.sender_incarnation)
2511                    || fence.participant_incarnation(peer) != Some(conn.fence.receiver_incarnation)
2512                {
2513                    return Err(io::Error::new(
2514                        io::ErrorKind::ConnectionAborted,
2515                        "shuffle barrier stream incarnations differ from its assignment certificate",
2516                    ));
2517                }
2518            }
2519            let assignment_version = assignment.fence.assignment_version;
2520            self.validate_scope(&scope, expected_assignment_version)?;
2521            if assignment_version == 0 || conn.fence.assignment_version != assignment_version {
2522                return Err(io::Error::new(
2523                    io::ErrorKind::ConnectionAborted,
2524                    "shuffle assignment changed while opening the stream",
2525                ));
2526            }
2527            if conn.fence.recovery_gen != scope.recovery_gen {
2528                return Err(io::Error::new(
2529                    io::ErrorKind::ConnectionAborted,
2530                    "shuffle recovery generation changed while opening the stream",
2531                ));
2532            }
2533            let budget = self
2534                .reserve_outbound(&conn, peer, msg, admission_bytes, &scope.cancel)
2535                .await?;
2536            let (prepared, budget) = prepare_outbound_message(msg, budget).await?;
2537            self.validate_scope(&scope, expected_assignment_version)?;
2538            // Stamp ordered frames before enqueue. A frame the transport later discards leaves a
2539            // hole the peer can see; a barrier carries the running count.
2540            let gen = scope.recovery_gen;
2541            let seq = {
2542                // Parking-lot guards are deliberately scoped before the enqueue await. The
2543                // assignment read guard also serializes sequence allocation with scope rotation.
2544                let assignment = self.assignment.read();
2545                let current = assignment.as_ref().ok_or_else(scope_cancelled_io)?;
2546                self.validate_scope_locked(&scope, expected_assignment_version, current)?;
2547                let mut seqs = self.seqs.lock();
2548                self.validate_scope_locked(&scope, expected_assignment_version, current)?;
2549                if !scope.matches_fence(&conn.fence) {
2550                    return Err(io::Error::new(
2551                        io::ErrorKind::ConnectionAborted,
2552                        "shuffle assignment changed before sequence allocation",
2553                    ));
2554                }
2555                let counter = seqs.entry(peer).or_insert(0);
2556                match msg {
2557                    ShuffleMessage::Data { .. } | ShuffleMessage::Frontier { .. } => {
2558                        let seq = *counter;
2559                        *counter = counter.checked_add(1).ok_or_else(|| {
2560                            io::Error::other("shuffle delivery sequence exhausted")
2561                        })?;
2562                        seq
2563                    }
2564                    ShuffleMessage::Barrier(_) => *counter,
2565                }
2566            };
2567            let out = Outbound {
2568                gen,
2569                assignment_version,
2570                seq,
2571                msg: prepared,
2572                assignment_digest: assignment_fence.map(CheckpointAssignmentFence::digest),
2573                _budget: budget,
2574            };
2575            self.validate_scope(&scope, expected_assignment_version)?;
2576            self.enqueue(&conn, peer, msg, out, &scope.cancel).await?;
2577
2578            #[cfg(test)]
2579            let post_enqueue_pause = { self.post_enqueue_pause.lock().take() };
2580            #[cfg(test)]
2581            if let Some((entered, release)) = post_enqueue_pause {
2582                entered.notify_one();
2583                release.notified().await;
2584            }
2585
2586            self.process_lease
2587                .require_live_io()
2588                .map_err(may_have_admitted_shuffle_frame)
2589        }
2590
2591        fn current_assignment(&self) -> io::Result<Arc<InstalledAssignment>> {
2592            self.process_lease.require_live_io()?;
2593            let assignment = self.assignment.read().clone().ok_or_else(|| {
2594                io::Error::new(
2595                    io::ErrorKind::NotConnected,
2596                    "shuffle assignment certificate is not installed",
2597                )
2598            })?;
2599            if self.assignment_version.load(Ordering::Acquire)
2600                != assignment.fence.assignment_version
2601            {
2602                return Err(io::Error::new(
2603                    io::ErrorKind::NotConnected,
2604                    "shuffle assignment certificate is not active",
2605                ));
2606            }
2607            Ok(assignment)
2608        }
2609
2610        fn validate_scope(&self, scope: &ScopeLease, expected: Option<u64>) -> io::Result<()> {
2611            let assignment = self.assignment.read();
2612            let current = assignment.as_ref().ok_or_else(scope_cancelled_io)?;
2613            self.validate_scope_locked(scope, expected, current)
2614        }
2615
2616        fn validate_scope_locked(
2617            &self,
2618            scope: &ScopeLease,
2619            expected: Option<u64>,
2620            current: &Arc<InstalledAssignment>,
2621        ) -> io::Result<()> {
2622            self.process_lease.require_live_io()?;
2623            if scope.cancel.is_cancelled() {
2624                return Err(scope_cancelled_io());
2625            }
2626            if !Arc::ptr_eq(current, &scope.assignment)
2627                || self.assignment_version.load(Ordering::Acquire)
2628                    != scope.assignment.fence.assignment_version
2629                || self.recovery_gen.load(Ordering::Acquire) != scope.recovery_gen
2630            {
2631                return Err(scope_cancelled_io());
2632            }
2633            self.validate_expected_assignment(expected)
2634        }
2635
2636        /// Establish an exact-scope stream to every remote assignment participant without sending
2637        /// a frame or consuming a delivery sequence.
2638        ///
2639        /// # Errors
2640        /// Returns the first peer connection or handshake error after attempting the full roster.
2641        pub async fn establish_assignment_mesh(
2642            &self,
2643            assignment_fence: &CheckpointAssignmentFence,
2644        ) -> io::Result<()> {
2645            let installed = self.current_assignment()?;
2646            if *assignment_fence != installed.fence
2647                || assignment_fence.digest() != installed.digest
2648                || !assignment_fence.is_canonical()
2649                || assignment_fence.participant_incarnation(self.local_id)
2650                    != Some(self.sender_incarnation)
2651            {
2652                return Err(io::Error::new(
2653                    io::ErrorKind::InvalidInput,
2654                    "shuffle mesh does not match the installed assignment certificate",
2655                ));
2656            }
2657            let scope = self.current_scope(
2658                Some(assignment_fence.assignment_version),
2659                installed.topology,
2660            )?;
2661            let results = futures::future::join_all(
2662                assignment_fence
2663                    .participants
2664                    .iter()
2665                    .map(|participant| participant.node_id)
2666                    .filter(|peer| *peer != self.local_id)
2667                    .map(|peer| {
2668                        let peer_scope = scope.clone();
2669                        async move { (peer, self.connection_for(peer, &peer_scope).await) }
2670                    }),
2671            )
2672            .await;
2673            let mut first_error = None;
2674            for (peer, result) in results {
2675                if let Err(error) = result {
2676                    first_error.get_or_insert_with(|| {
2677                        io::Error::new(
2678                            error.kind(),
2679                            format!("shuffle assignment mesh peer {peer}: {error}"),
2680                        )
2681                    });
2682                }
2683            }
2684            if let Some(error) = first_error {
2685                return Err(error);
2686            }
2687            self.validate_scope(&scope, Some(assignment_fence.assignment_version))
2688        }
2689
2690        /// Ship `barrier` to every required peer. All peers are attempted, but
2691        /// any failure rejects the cut so the coordinator can abort promptly.
2692        ///
2693        /// # Errors
2694        /// Returns the first peer error after attempting the full fan-out.
2695        pub async fn fan_out_barrier(
2696            &self,
2697            peers: &[ShufflePeerId],
2698            barrier: CheckpointBarrier,
2699            assignment_fence: &CheckpointAssignmentFence,
2700        ) -> io::Result<()> {
2701            self.fan_out_barrier_for_topology(peers, barrier, assignment_fence, None)
2702                .await
2703        }
2704
2705        /// Fan out a barrier under an immutable graph topology binding.
2706        /// # Errors
2707        /// Rejects a different topology or any ordinary barrier/assignment/peer failure.
2708        pub async fn fan_out_barrier_for_topology(
2709            &self,
2710            peers: &[ShufflePeerId],
2711            barrier: CheckpointBarrier,
2712            assignment_fence: &CheckpointAssignmentFence,
2713            topology: Option<ShuffleTopologyFence>,
2714        ) -> io::Result<()> {
2715            validate_checkpoint_barrier(barrier)?;
2716            let installed = self.current_assignment()?;
2717            self.current_scope(Some(assignment_fence.assignment_version), topology)?;
2718            let expected_peers: Vec<_> = assignment_fence
2719                .participants
2720                .iter()
2721                .map(|participant| participant.node_id)
2722                .filter(|peer| *peer != self.local_id)
2723                .collect();
2724            let mut actual_peers = peers.to_vec();
2725            actual_peers.sort_unstable();
2726            let has_duplicates = actual_peers.windows(2).any(|pair| pair[0] == pair[1]);
2727            if *assignment_fence != installed.fence
2728                || assignment_fence.digest() != installed.digest
2729                || !assignment_fence.is_canonical()
2730                || !assignment_fence.contains(self.local_id)
2731                || assignment_fence.participant_incarnation(self.local_id)
2732                    != Some(self.sender_incarnation)
2733                || has_duplicates
2734                || actual_peers != expected_peers
2735            {
2736                return Err(io::Error::new(
2737                    io::ErrorKind::InvalidInput,
2738                    "shuffle barrier peers do not exactly cover the assignment roster",
2739                ));
2740            }
2741            self.validate_expected_assignment(Some(assignment_fence.assignment_version))?;
2742            let cid = barrier.checkpoint_id;
2743            let msg = ShuffleMessage::Barrier(barrier);
2744            let mut first_scope_cancel = None;
2745            let mut first_peer_error = None;
2746            let results = futures::future::join_all(peers.iter().map(|&peer| {
2747                let msg = &msg;
2748                async move {
2749                    (
2750                        peer,
2751                        self.send_to_inner(
2752                            peer,
2753                            msg,
2754                            Some(assignment_fence.assignment_version),
2755                            Some(assignment_fence),
2756                            topology,
2757                            None,
2758                        )
2759                        .await,
2760                    )
2761                }
2762            }))
2763            .await;
2764            for (peer, result) in results {
2765                match result {
2766                    Ok(()) => {}
2767                    Err(e) => {
2768                        tracing::warn!(
2769                            peer,
2770                            checkpoint_id = cid,
2771                            error = %e,
2772                            "shuffle barrier fan-out: required peer unreachable"
2773                        );
2774                        if is_scope_cancelled(&e) {
2775                            first_scope_cancel.get_or_insert(e);
2776                        } else {
2777                            first_peer_error.get_or_insert(e);
2778                        }
2779                    }
2780                }
2781            }
2782            if let Some(error) = first_peer_error.or(first_scope_cancel) {
2783                if is_scope_cancelled(&error) {
2784                    self.process_lease.require_live_io()?;
2785                }
2786                return Err(error);
2787            }
2788            Ok(())
2789        }
2790
2791        /// Resolve and cache `peer`'s address from the KV; `None` when unavailable.
2792        async fn discover_peer(&self, peer: ShufflePeerId) -> Option<SocketAddr> {
2793            let kv = self.kv.as_ref()?;
2794            let raw = kv
2795                .read_from(crate::cluster::discovery::NodeId(peer), SHUFFLE_ADDR_KEY)
2796                .await?;
2797            let addr = match raw.parse::<SocketAddr>() {
2798                Ok(a) => a,
2799                Err(_) => tokio::net::lookup_host(&raw).await.ok()?.next()?,
2800            };
2801            self.peers.lock().insert(peer, addr);
2802            Some(addr)
2803        }
2804
2805        async fn connection_for(
2806            &self,
2807            peer: ShufflePeerId,
2808            scope: &ScopeLease,
2809        ) -> io::Result<Arc<PeerConn>> {
2810            self.validate_scope(scope, None)?;
2811            let assignment = &scope.assignment;
2812            if !assignment.fence.contains(peer) || peer == self.local_id {
2813                return Err(io::Error::new(
2814                    io::ErrorKind::InvalidInput,
2815                    "shuffle peer is outside the installed assignment roster",
2816                ));
2817            }
2818            let assignment_version = assignment.fence.assignment_version;
2819            let recovery_gen = scope.recovery_gen;
2820            let key = peer;
2821            if let Some(existing) = self.pool.lock().get(&key).cloned() {
2822                if existing.is_alive()
2823                    && existing.fence.assignment_version == assignment_version
2824                    && existing.fence.assignment_certificate_digest == assignment.digest
2825                    && existing.fence.recovery_gen == recovery_gen
2826                    && existing.fence.topology == assignment.topology
2827                {
2828                    return Ok(existing);
2829                }
2830            }
2831            let connect_lock = self
2832                .connect_locks
2833                .lock()
2834                .entry(key)
2835                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
2836                .clone();
2837            let _connect_guard = tokio::select! {
2838                biased;
2839                () = self.process_lease.wait_until_lost() => {
2840                    return Err(process_lease_expired_io());
2841                }
2842                () = scope.cancel.cancelled() => return Err(scope_cancelled_io()),
2843                guard = connect_lock.lock() => guard,
2844            };
2845            self.validate_scope(scope, None)?;
2846            if let Some(existing) = self.pool.lock().get(&key).cloned() {
2847                if existing.is_alive()
2848                    && existing.fence.assignment_version == assignment_version
2849                    && existing.fence.assignment_certificate_digest == assignment.digest
2850                    && existing.fence.recovery_gen == recovery_gen
2851                    && existing.fence.topology == assignment.topology
2852                {
2853                    return Ok(existing);
2854                }
2855            }
2856            // Purge a dead entry so we reopen the call below.
2857            self.pool
2858                .lock()
2859                .retain(|pool_key, connection| *pool_key != key || connection.is_alive());
2860
2861            // Re-resolve on reconnect (peers may restart on a new port); fall back
2862            // to a statically registered address when there's no KV.
2863            let discovered = tokio::select! {
2864                biased;
2865                () = self.process_lease.wait_until_lost() => {
2866                    return Err(process_lease_expired_io());
2867                }
2868                () = scope.cancel.cancelled() => return Err(scope_cancelled_io()),
2869                discovered = self.discover_peer(peer) => discovered,
2870            };
2871            let addr = match discovered {
2872                Some(addr) => addr,
2873                None => self.peers.lock().get(&peer).copied().ok_or_else(|| {
2874                    io::Error::new(
2875                        io::ErrorKind::NotFound,
2876                        format!("peer {peer} has no registered shuffle address"),
2877                    )
2878                })?,
2879            };
2880
2881            tracing::debug!(peer, addr = %addr, "shuffle reconnecting to peer");
2882            let expected_receiver_incarnation = assignment
2883                .fence
2884                .participant_incarnation(peer)
2885                .ok_or_else(|| {
2886                    io::Error::new(
2887                        io::ErrorKind::PermissionDenied,
2888                        format!("shuffle peer {peer} is absent from the assignment certificate"),
2889                    )
2890                })?;
2891            let pipeline_identity = *self.pipeline_identity.read();
2892            let conn = Arc::new(
2893                open_call(OpenCall {
2894                    local_id: self.local_id,
2895                    peer,
2896                    addr,
2897                    sender_incarnation: self.sender_incarnation,
2898                    assignment_version,
2899                    assignment_certificate_digest: assignment.digest,
2900                    topology: assignment.topology,
2901                    pipeline_identity,
2902                    expected_receiver_incarnation,
2903                    recovery_gen,
2904                    current_assignment: Arc::clone(&self.assignment_version),
2905                    current_recovery_gen: Arc::clone(&self.recovery_gen),
2906                    counters: self.counters_for(peer),
2907                    scope_cancel: scope.cancel.clone(),
2908                })
2909                .await?,
2910            );
2911            self.validate_scope(scope, None)?;
2912            let mut pool = self.pool.lock();
2913            if scope.cancel.is_cancelled()
2914                || self.assignment_version.load(Ordering::Acquire) != assignment_version
2915                || self.recovery_gen.load(Ordering::Acquire) != recovery_gen
2916            {
2917                return Err(scope_cancelled_io());
2918            }
2919            pool.insert(key, Arc::clone(&conn));
2920            Ok(conn)
2921        }
2922
2923        #[cfg(test)]
2924        pub(crate) fn disconnect_peer_for_test(&self, peer: ShufflePeerId) {
2925            self.pool.lock().remove(&peer);
2926        }
2927
2928        #[cfg(test)]
2929        pub(crate) async fn hold_outbound_budget_for_test(
2930            &self,
2931            peer: ShufflePeerId,
2932        ) -> io::Result<OwnedSemaphorePermit> {
2933            let scope = self.current_scope(None, self.topology_fence())?;
2934            let conn = self.connection_for(peer, &scope).await?;
2935            Arc::clone(&conn.byte_budget)
2936                .acquire_many_owned(
2937                    u32::try_from(OUTBOUND_PEER_BUDGET_BYTES)
2938                        .expect("outbound test budget fits u32"),
2939                )
2940                .await
2941                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "test budget closed"))
2942        }
2943    }
2944
2945    /// Bind a proposed stream to the receiver's exact identity and scope.
2946    async fn negotiate_identity(
2947        client: &mut ShuffleTransportClient<Channel>,
2948        call: &OpenCall,
2949    ) -> io::Result<StreamFence> {
2950        let stream_id = Uuid::new_v4();
2951        let response = tokio::select! {
2952            biased;
2953            () = call.scope_cancel.cancelled() => return Err(scope_cancelled_io()),
2954            response = client.handshake(Request::new(HandshakeRequest {
2955                sender_node_id: call.local_id,
2956                sender_incarnation: call.sender_incarnation.as_bytes().to_vec(),
2957                stream_id: stream_id.as_bytes().to_vec(),
2958                assignment_version: call.assignment_version,
2959                recovery_gen: call.recovery_gen,
2960                assignment_certificate_digest: call.assignment_certificate_digest.to_vec(),
2961                topology_version: call.topology.map_or(0, ShuffleTopologyFence::version),
2962                topology_manifest_sha256: call.topology.map_or_else(Vec::new, |t| t.manifest_sha256().to_vec()),
2963                pipeline_identity_sha256: call.pipeline_identity.map_or_else(Vec::new, |d| d.to_vec()),
2964            })) => response.map_err(status_io)?.into_inner(),
2965        };
2966        let receiver_incarnation = parse_uuid(
2967            &response.receiver_incarnation,
2968            "handshake receiver incarnation",
2969        )
2970        .map_err(io_err)?;
2971        if response.receiver_node_id != call.peer
2972            || receiver_incarnation != call.expected_receiver_incarnation
2973            || response.sender_incarnation.as_slice() != call.sender_incarnation.as_bytes()
2974            || response.stream_id.as_slice() != stream_id.as_bytes()
2975            || response.assignment_version != call.assignment_version
2976            || response.assignment_certificate_digest.as_slice()
2977                != call.assignment_certificate_digest.as_slice()
2978            || response.recovery_gen != call.recovery_gen
2979            || parse_topology_fence(
2980                response.topology_version,
2981                &response.topology_manifest_sha256,
2982            )
2983            .map_err(io_err)?
2984                != call.topology
2985        {
2986            return Err(io::Error::new(
2987                io::ErrorKind::InvalidData,
2988                "shuffle handshake response did not match the requested stream identity",
2989            ));
2990        }
2991        Ok(StreamFence {
2992            sender_node_id: call.local_id,
2993            sender_incarnation: call.sender_incarnation,
2994            receiver_incarnation,
2995            stream_id,
2996            assignment_version: call.assignment_version,
2997            assignment_certificate_digest: call.assignment_certificate_digest,
2998            recovery_gen: call.recovery_gen,
2999            topology: call.topology,
3000        })
3001    }
3002
3003    /// Open a client-streaming call after the identity handshake completes.
3004    async fn open_call(call: OpenCall) -> io::Result<PeerConn> {
3005        let endpoint = crate::cluster::control::tls::client_endpoint(&call.addr.to_string())
3006            .map_err(io_err)?
3007            .tcp_nodelay(true);
3008        let channel = tokio::select! {
3009            biased;
3010            () = call.scope_cancel.cancelled() => return Err(scope_cancelled_io()),
3011            channel = endpoint.connect() => channel.map_err(io_err)?,
3012        };
3013        let mut client = ShuffleTransportClient::<Channel>::new(channel)
3014            .max_decoding_message_size(MAX_SHUFFLE_MESSAGE_BYTES)
3015            .max_encoding_message_size(MAX_SHUFFLE_MESSAGE_BYTES);
3016        let fence = negotiate_identity(&mut client, &call).await?;
3017        let OpenCall {
3018            addr,
3019            current_assignment,
3020            current_recovery_gen,
3021            scope_cancel,
3022            counters,
3023            ..
3024        } = call;
3025        let (tx, rx) = mpsc::bounded_async::<Outbound>(SEND_QUEUE);
3026        let alive = Arc::new(AtomicBool::new(true));
3027        let alive_for_driver = Arc::clone(&alive);
3028
3029        // Request stream: a `Hello` chained onto an unfold over the per-peer
3030        // receiver, serializing dequeued messages here in the driver task.
3031        let hello = ShuffleFrame {
3032            kind: Some(shuffle_frame::Kind::Hello(hello_for(
3033                fence.sender_node_id,
3034                &fence,
3035            ))),
3036        };
3037        let stream_fence = fence;
3038        let outbound = futures::stream::once(async move { hello }).chain(futures::stream::unfold(
3039            (
3040                rx,
3041                current_assignment,
3042                current_recovery_gen,
3043                scope_cancel.clone(),
3044                stream_fence,
3045                None::<Encoded>,
3046            ),
3047            |(
3048                rx,
3049                current_assignment,
3050                current_recovery_gen,
3051                scope_cancel,
3052                stream_fence,
3053                mut encoded,
3054            )| async move {
3055                loop {
3056                    if let Some(pending) = encoded.as_mut() {
3057                        if let Some(frame) = pending.frames.pop_front() {
3058                            return Some((
3059                                frame,
3060                                (
3061                                    rx,
3062                                    current_assignment,
3063                                    current_recovery_gen,
3064                                    scope_cancel,
3065                                    stream_fence,
3066                                    encoded,
3067                                ),
3068                            ));
3069                        }
3070                        let _ = encoded.take();
3071                    }
3072                    let out = tokio::select! {
3073                        biased;
3074                        () = scope_cancel.cancelled() => return None,
3075                        out = rx.recv() => out.ok()?,
3076                    };
3077                    if current_assignment.load(Ordering::Acquire) != stream_fence.assignment_version
3078                        || current_recovery_gen.load(Ordering::Acquire) != stream_fence.recovery_gen
3079                    {
3080                        return None;
3081                    }
3082                    match frame_message(out) {
3083                        Ok(message) => encoded = Some(message),
3084                        Err(error) => {
3085                            tracing::warn!(%error, "shuffle frame construction failed; closing stream");
3086                            return None;
3087                        }
3088                    }
3089                }
3090            },
3091        ));
3092
3093        let driver = tokio::spawn(async move {
3094            // A break here means frames already handed to tonic may never reach the peer. The
3095            // peer detects the hole from the sequence, so this only needs to be visible, not fatal.
3096            tokio::select! {
3097                biased;
3098                () = scope_cancel.cancelled() => {}
3099                result = client.shuffle(Request::new(outbound)) => {
3100                    if let Err(status) = result {
3101                        tracing::warn!(peer_addr = %addr, error = %status, "shuffle stream broke");
3102                    }
3103                }
3104            }
3105            alive_for_driver.store(false, Ordering::Release);
3106        });
3107
3108        Ok(PeerConn {
3109            tx,
3110            byte_budget: Arc::new(Semaphore::new(OUTBOUND_PEER_BUDGET_BYTES)),
3111            control_byte_budget: Arc::new(Semaphore::new(CHECKPOINTED_CONTROL_PEER_BUDGET_BYTES)),
3112            counters,
3113            send_lock: tokio::sync::Mutex::new(()),
3114            alive,
3115            driver,
3116            fence,
3117        })
3118    }
3119
3120    struct PendingInbound<'a> {
3121        slot: &'a Mutex<Option<Inbound>>,
3122        ready: &'a AtomicBool,
3123        inbound: Option<Inbound>,
3124    }
3125
3126    impl PendingInbound<'_> {
3127        fn take(mut self) -> Inbound {
3128            self.inbound.take().expect("pending shuffle inbound")
3129        }
3130    }
3131
3132    impl Drop for PendingInbound<'_> {
3133        fn drop(&mut self) {
3134            if let Some(inbound) = self.inbound.take() {
3135                let mut slot = self.slot.lock();
3136                let replaced = slot.replace(inbound);
3137                assert!(replaced.is_none(), "shuffle deferred receive slot occupied");
3138                self.ready.store(true, Ordering::Release);
3139            }
3140        }
3141    }
3142
3143    /// Inbound side of the shuffle fabric: a Tonic `ShuffleTransport` server that
3144    /// surfaces every received frame, attributed to its peer, on the bounded queue.
3145    pub struct ShuffleReceiver {
3146        local_id: ShufflePeerId,
3147        local_addr: SocketAddr,
3148        receiver_incarnation: Uuid,
3149        // `AsyncRx` is `Send` but `!Sync`, yet `Arc<ShuffleReceiver>` must be `Sync`.
3150        // Park it behind a `Mutex<Option<_>>` and hand it out via a take/return guard so
3151        // the single consumer never holds the guard across `.await`; the guard
3152        // restores it on drop so a cancelled `recv` can't strand it.
3153        rx: Mutex<Option<InboundRx>>,
3154        rx_returned: Arc<tokio::sync::Notify>,
3155        work_ready: Arc<tokio::sync::Notify>,
3156        deferred_recv: Mutex<Option<Inbound>>,
3157        deferred_recv_ready: AtomicBool,
3158        #[cfg(test)]
3159        recv_deferred_pause: Mutex<Option<Arc<tokio::sync::Notify>>>,
3160        #[cfg(test)]
3161        assignment_wait_pause: Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
3162        server: JoinHandle<()>,
3163        holdover: Arc<Holdover>,
3164        /// Monotonic notification that an in-band barrier reached the bounded receive queue.
3165        barrier_arrivals: Arc<AtomicU64>,
3166        barrier_reconciled: AtomicU64,
3167        /// Inbound data frames stamped below this are pre-rewind and discarded.
3168        recovery_gen: Arc<AtomicU64>,
3169        topology_version: Arc<AtomicU64>,
3170        pipeline_identity: Arc<RwLock<Option<[u8; 32]>>>,
3171        recovery_transition: Mutex<()>,
3172        assignment: Arc<RwLock<Option<Arc<InstalledAssignment>>>>,
3173        assignment_version: Arc<AtomicU64>,
3174        scope_cancel: Arc<RwLock<CancellationToken>>,
3175        process_lease: Arc<ProcessLeaseGate>,
3176        /// True only while an exact retained certificate is closed by a transient authority read
3177        /// failure. Delivery expectations remain intact for same-version reactivation.
3178        assignment_suspended: AtomicBool,
3179        assignment_resumed: tokio::sync::Notify,
3180        pending_handshakes: Arc<PendingHandshakes>,
3181        delivery: Arc<DeliveryTracker>,
3182        inbound_budget: Arc<InboundBudget>,
3183        #[cfg(test)]
3184        active_streams: Arc<Semaphore>,
3185    }
3186
3187    impl Drop for ShuffleReceiver {
3188        fn drop(&mut self) {
3189            // Abort the server task so the listener closes and in-flight peer
3190            // streams break — senders then observe the error and reconnect.
3191            self.server.abort();
3192        }
3193    }
3194
3195    impl std::fmt::Debug for ShuffleReceiver {
3196        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3197            f.debug_struct("ShuffleReceiver")
3198                .field("local_id", &self.local_id)
3199                .field("local_addr", &self.local_addr)
3200                .finish_non_exhaustive()
3201        }
3202    }
3203
3204    impl ShuffleReceiver {
3205        /// Bind on `addr` and start serving; the resolved address is at
3206        /// [`Self::local_addr`].
3207        ///
3208        /// # Errors
3209        /// Returns `io::Error` on bind failure.
3210        pub async fn bind(
3211            local_id: ShufflePeerId,
3212            addr: SocketAddr,
3213            receiver_incarnation: Uuid,
3214        ) -> io::Result<Self> {
3215            if local_id == 0 || receiver_incarnation.is_nil() {
3216                return Err(io::Error::new(
3217                    io::ErrorKind::InvalidInput,
3218                    "shuffle receiver requires a nonzero node and non-nil incarnation",
3219                ));
3220            }
3221            let listener = tokio::net::TcpListener::bind(addr).await?;
3222            let local_addr = listener.local_addr()?;
3223            let (tx, rx) = mpsc::bounded_async::<Inbound>(SHUFFLE_RECV_QUEUE);
3224            let work_ready = Arc::new(tokio::sync::Notify::new());
3225
3226            let recovery_gen = Arc::new(AtomicU64::new(0));
3227            let topology_version = Arc::new(AtomicU64::new(0));
3228            let pipeline_identity = Arc::new(RwLock::new(None));
3229            let assignment = Arc::new(RwLock::new(None));
3230            let assignment_version = Arc::new(AtomicU64::new(0));
3231            let scope_cancel = Arc::new(RwLock::new(cancelled_token()));
3232            let process_lease = Arc::new(ProcessLeaseGate::default());
3233            let pending_handshakes = Arc::new(PendingHandshakes::default());
3234            let delivery = Arc::new(DeliveryTracker::default());
3235            let barrier_arrivals = Arc::new(AtomicU64::new(0));
3236            let holdover = Arc::new(Holdover::new(SHUFFLE_RECV_QUEUE));
3237            let inbound_budget = Arc::new(InboundBudget::new(INBOUND_NODE_BUDGET_BYTES));
3238            let active_streams = Arc::new(Semaphore::new(MAX_ACTIVE_STREAMS));
3239            let active_stream_registry = Arc::new(ActiveStreamRegistry::default());
3240            let service = ShuffleService {
3241                local_id,
3242                receiver_incarnation,
3243                assignment: Arc::clone(&assignment),
3244                assignment_version: Arc::clone(&assignment_version),
3245                scope_cancel: Arc::clone(&scope_cancel),
3246                process_lease: Arc::clone(&process_lease),
3247                pending_handshakes: Arc::clone(&pending_handshakes),
3248                tx,
3249                work_ready: Arc::clone(&work_ready),
3250                recovery_gen: Arc::clone(&recovery_gen),
3251                topology_version: Arc::clone(&topology_version),
3252                pipeline_identity: Arc::clone(&pipeline_identity),
3253                delivery: Arc::clone(&delivery),
3254                barrier_arrivals: Arc::clone(&barrier_arrivals),
3255                holdover: Arc::clone(&holdover),
3256                inbound_budget: Arc::clone(&inbound_budget),
3257                active_streams: Arc::clone(&active_streams),
3258                active_stream_registry,
3259            };
3260            let incoming = futures::stream::unfold(listener, |listener| async move {
3261                let item = match listener.accept().await {
3262                    Ok((stream, _)) => {
3263                        let _ = stream.set_nodelay(true);
3264                        Ok(stream)
3265                    }
3266                    Err(e) => Err(e),
3267                };
3268                Some((item, listener))
3269            });
3270            // Apply TLS synchronously so a bad cert fails bind() rather than
3271            // silently never serving.
3272            let mut builder = Server::builder();
3273            if let Some(tls) = crate::cluster::control::tls::server_tls() {
3274                builder = builder
3275                    .tls_config(tls.clone())
3276                    .map_err(|e| io::Error::other(format!("cluster shuffle TLS config: {e}")))?;
3277            }
3278            let router = builder.add_service(
3279                ShuffleTransportServer::new(service)
3280                    .max_decoding_message_size(MAX_SHUFFLE_MESSAGE_BYTES)
3281                    .max_encoding_message_size(MAX_SHUFFLE_MESSAGE_BYTES),
3282            );
3283            let server = tokio::spawn(async move {
3284                let _ = router.serve_with_incoming(incoming).await;
3285            });
3286
3287            Ok(Self {
3288                local_id,
3289                local_addr,
3290                receiver_incarnation,
3291                rx: Mutex::new(Some(rx)),
3292                rx_returned: Arc::new(tokio::sync::Notify::new()),
3293                work_ready,
3294                deferred_recv: Mutex::new(None),
3295                deferred_recv_ready: AtomicBool::new(false),
3296                #[cfg(test)]
3297                recv_deferred_pause: Mutex::new(None),
3298                #[cfg(test)]
3299                assignment_wait_pause: Mutex::new(None),
3300                server,
3301                holdover,
3302                barrier_arrivals,
3303                barrier_reconciled: AtomicU64::new(0),
3304                recovery_gen,
3305                topology_version,
3306                pipeline_identity,
3307                recovery_transition: Mutex::new(()),
3308                assignment,
3309                assignment_version,
3310                scope_cancel,
3311                process_lease,
3312                assignment_suspended: AtomicBool::new(false),
3313                assignment_resumed: tokio::sync::Notify::new(),
3314                pending_handshakes,
3315                delivery,
3316                inbound_budget,
3317                #[cfg(test)]
3318                active_streams,
3319            })
3320        }
3321
3322        /// Bind inbound admission to this process's renewable cluster lease.
3323        ///
3324        /// This must be installed before an assignment certificate is activated.
3325        ///
3326        /// # Errors
3327        /// Returns an error for an expired lease, a different previously installed deadline, or
3328        /// an already-active assignment.
3329        pub fn install_process_lease_deadline(
3330            &self,
3331            deadline: Arc<LeaseDeadline>,
3332        ) -> io::Result<()> {
3333            let _assignment = self.assignment.write();
3334            if self.assignment_version.load(Ordering::Acquire) != 0
3335                && !self.process_lease.is_installed_deadline(&deadline)
3336            {
3337                return Err(io::Error::new(
3338                    io::ErrorKind::InvalidInput,
3339                    "shuffle process lease must be installed before assignment activation",
3340                ));
3341            }
3342            self.process_lease.install(deadline)
3343        }
3344
3345        /// Install the exact assignment certificate accepted by inbound streams. Existing
3346        /// streams are rejected on their next frame; the new delivery domain starts at zero.
3347        ///
3348        /// # Errors
3349        /// Returns an error for a malformed certificate, a same-version certificate conflict,
3350        /// or a certificate that does not bind this exact receiver process.
3351        pub fn install_assignment_fence(
3352            &self,
3353            fence: &CheckpointAssignmentFence,
3354            owners: &[ShufflePeerId],
3355        ) -> io::Result<bool> {
3356            let mut next = InstalledAssignment::for_process(
3357                fence,
3358                owners,
3359                self.local_id,
3360                self.receiver_incarnation,
3361            )?;
3362            let mut assignment = self.assignment.write();
3363            self.process_lease.require_live_io()?;
3364            if let Some(current) = assignment.as_ref() {
3365                next = Arc::new(InstalledAssignment {
3366                    fence: next.fence.clone(),
3367                    digest: next.digest,
3368                    owners: Arc::clone(&next.owners),
3369                    topology: current.topology,
3370                });
3371                if next.fence.assignment_version < current.fence.assignment_version {
3372                    return Ok(false);
3373                }
3374                if next.fence.assignment_version == current.fence.assignment_version {
3375                    if next.digest == current.digest
3376                        && next.fence == current.fence
3377                        && next.owners == current.owners
3378                    {
3379                        if self.assignment_version.load(Ordering::Acquire)
3380                            == next.fence.assignment_version
3381                        {
3382                            return Ok(false);
3383                        }
3384                        if self.assignment_suspended.load(Ordering::Acquire) {
3385                            rotate_scope_token(&self.scope_cancel, &self.process_lease, true);
3386                            self.pending_handshakes.clear();
3387                            self.assignment_suspended.store(false, Ordering::Release);
3388                            self.assignment_version
3389                                .store(next.fence.assignment_version, Ordering::Release);
3390                            self.assignment_resumed.notify_waiters();
3391                            return Ok(true);
3392                        }
3393                        return Err(io::Error::new(
3394                            io::ErrorKind::InvalidData,
3395                            "an invalidated shuffle assignment requires a higher version",
3396                        ));
3397                    }
3398                    return Err(io::Error::new(
3399                        io::ErrorKind::InvalidData,
3400                        "conflicting shuffle assignment certificate for an installed version",
3401                    ));
3402                }
3403            }
3404            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
3405            self.pending_handshakes.clear();
3406            self.delivery.reset_assignment();
3407            self.holdover.clear_staged_frontiers();
3408            let version = next.fence.assignment_version;
3409            *assignment = Some(next);
3410            self.assignment_suspended.store(false, Ordering::Release);
3411            rotate_scope_token(&self.scope_cancel, &self.process_lease, true);
3412            self.assignment_version.store(version, Ordering::Release);
3413            self.assignment_resumed.notify_waiters();
3414            Ok(true)
3415        }
3416
3417        /// Temporarily close inbound admission after a transient durable-authority read failure.
3418        /// Pending handshakes are discarded, while committed delivery expectations remain bound
3419        /// to the retained certificate for exact same-version reactivation.
3420        pub fn suspend_assignment_fence(&self) {
3421            let assignment = self.assignment.write();
3422            if assignment.is_none() || self.assignment_version.load(Ordering::Acquire) == 0 {
3423                return;
3424            }
3425            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
3426            self.pending_handshakes.clear();
3427            self.assignment_suspended.store(true, Ordering::Release);
3428            self.assignment_version.store(0, Ordering::Release);
3429        }
3430
3431        /// Reject all streams while an adopted owner map is awaiting its owner-complete process
3432        /// certificate. Pending tokens and sequence expectations cannot cross this boundary.
3433        pub fn invalidate_assignment_fence(&self) {
3434            let _assignment = self.assignment.write();
3435            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
3436            self.assignment_suspended.store(false, Ordering::Release);
3437            self.assignment_version.store(0, Ordering::Release);
3438            self.pending_handshakes.clear();
3439            self.delivery.reset_assignment();
3440            self.holdover.clear_staged_frontiers();
3441            self.assignment_resumed.notify_waiters();
3442        }
3443
3444        /// Assignment scope currently accepted for inbound streams and staged frames.
3445        #[must_use]
3446        pub fn assignment_version(&self) -> u64 {
3447            self.assignment_version.load(Ordering::Acquire)
3448        }
3449
3450        /// Digest of the exact assignment certificate currently active for inbound streams.
3451        #[must_use]
3452        pub fn active_assignment_digest(&self) -> Option<[u8; 32]> {
3453            let assignment = self.assignment.read();
3454            assignment.as_ref().and_then(|installed| {
3455                (self.assignment_version.load(Ordering::Acquire)
3456                    == installed.fence.assignment_version)
3457                    .then_some(installed.digest)
3458            })
3459        }
3460
3461        /// Advance the inbound recovery scope. Existing streams are rejected; the next exact
3462        /// handshake re-baselines sequence after the coordinated rewind.
3463        pub fn set_recovery_gen(&self, gen: u64) {
3464            let _transition = self.recovery_transition.lock();
3465            let _assignment = self.assignment.write();
3466            let previous = self.recovery_gen.load(Ordering::Acquire);
3467            if gen <= previous {
3468                return;
3469            }
3470            rotate_scope_token(&self.scope_cancel, &self.process_lease, false);
3471            self.pending_handshakes.clear();
3472            self.delivery.prepare_recovery(gen);
3473            self.holdover.clear_staged_frontiers();
3474            self.recovery_gen.store(gen, Ordering::Release);
3475            rotate_scope_token(
3476                &self.scope_cancel,
3477                &self.process_lease,
3478                self.assignment_version.load(Ordering::Acquire) != 0,
3479            );
3480            // The next admitted Hello carries the new generation and re-baselines only that peer.
3481            // Resetting all peers after publishing this atomic races new-generation delivery.
3482        }
3483
3484        /// Recovery scope currently accepted for inbound streams and staged frames.
3485        #[must_use]
3486        pub fn recovery_gen(&self) -> u64 {
3487            self.recovery_gen.load(Ordering::Acquire)
3488        }
3489
3490        /// Process incarnation bound into every accepted stream handshake.
3491        #[must_use]
3492        pub const fn incarnation(&self) -> Uuid {
3493            self.receiver_incarnation
3494        }
3495
3496        /// Node id bound into every accepted stream handshake.
3497        #[must_use]
3498        pub const fn local_id(&self) -> ShufflePeerId {
3499            self.local_id
3500        }
3501
3502        #[cfg(test)]
3503        pub(crate) fn active_streams_for_test(&self) -> usize {
3504            MAX_ACTIVE_STREAMS - self.active_streams.available_permits()
3505        }
3506
3507        #[cfg(test)]
3508        pub(crate) fn committed_sequence_for_test(&self, peer: ShufflePeerId) -> Option<u64> {
3509            self.delivery
3510                .peers
3511                .lock()
3512                .get(&peer)
3513                .map(|state| state.expected)
3514        }
3515
3516        #[cfg(test)]
3517        pub(crate) fn barrier_arrivals_for_test(&self) -> u64 {
3518            self.barrier_arrivals.load(Ordering::Acquire)
3519        }
3520
3521        #[cfg(test)]
3522        pub(super) fn assignment_fence_for_test(&self) -> CheckpointAssignmentFence {
3523            self.assignment
3524                .read()
3525                .as_ref()
3526                .expect("test assignment")
3527                .fence
3528                .clone()
3529        }
3530
3531        #[cfg(test)]
3532        pub(crate) fn pause_next_recv_after_defer_for_test(&self) -> Arc<tokio::sync::Notify> {
3533            let entered = Arc::new(tokio::sync::Notify::new());
3534            let replaced = self
3535                .recv_deferred_pause
3536                .lock()
3537                .replace(Arc::clone(&entered));
3538            assert!(
3539                replaced.is_none(),
3540                "shuffle receive pause already installed"
3541            );
3542            entered
3543        }
3544
3545        #[cfg(test)]
3546        pub(super) fn pause_next_assignment_wait_for_test(
3547            &self,
3548        ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
3549            let entered = Arc::new(tokio::sync::Notify::new());
3550            let release = Arc::new(tokio::sync::Notify::new());
3551            let replaced = self
3552                .assignment_wait_pause
3553                .lock()
3554                .replace((Arc::clone(&entered), Arc::clone(&release)));
3555            assert!(
3556                replaced.is_none(),
3557                "shuffle assignment wait pause already installed"
3558            );
3559            (entered, release)
3560        }
3561
3562        #[cfg(test)]
3563        pub(super) async fn wait_while_assignment_suspended_for_test(&self) -> bool {
3564            self.wait_while_assignment_suspended().await
3565        }
3566
3567        async fn wait_while_assignment_suspended(&self) -> bool {
3568            loop {
3569                let resumed = self.assignment_resumed.notified();
3570                tokio::pin!(resumed);
3571                resumed.as_mut().enable();
3572                if self.process_lease.require_live_io().is_err() {
3573                    return false;
3574                }
3575                if !self.assignment_suspended.load(Ordering::Acquire) {
3576                    return true;
3577                }
3578                #[cfg(test)]
3579                let assignment_wait_pause = { self.assignment_wait_pause.lock().take() };
3580                #[cfg(test)]
3581                if let Some((entered, release)) = assignment_wait_pause {
3582                    entered.notify_one();
3583                    release.notified().await;
3584                }
3585                tokio::select! {
3586                    biased;
3587                    () = self.process_lease.wait_until_lost() => return false,
3588                    () = &mut resumed => {}
3589                }
3590            }
3591        }
3592
3593        fn consumption_scope(
3594            &self,
3595        ) -> Option<parking_lot::RwLockReadGuard<'_, Option<Arc<InstalledAssignment>>>> {
3596            let assignment = self.assignment.read();
3597            (!self.assignment_suspended.load(Ordering::Acquire)).then_some(assignment)
3598        }
3599
3600        fn take_deferred_recv(&self) -> Option<Inbound> {
3601            if !self.deferred_recv_ready.load(Ordering::Acquire) {
3602                return None;
3603            }
3604            let mut slot = self.deferred_recv.lock();
3605            let inbound = slot.take();
3606            self.deferred_recv_ready.store(false, Ordering::Release);
3607            inbound
3608        }
3609
3610        /// Cumulative delivery-loss incidents. A value above the recovered floor means an epoch
3611        /// must not seal; exact missing-frame magnitude is diagnostic and never trusted for this
3612        /// non-wrapping correctness signal.
3613        #[must_use]
3614        pub fn delivery_loss_incidents(&self) -> Arc<AtomicU64> {
3615            Arc::clone(&self.delivery.delivery_loss_incidents)
3616        }
3617
3618        /// Delivery-loss incidents covered by a completed coordinated rewind. The callback uses
3619        /// this durable in-process floor so repaired incidents do not fault restored state again.
3620        #[must_use]
3621        pub fn recovered_delivery_loss_incidents(&self) -> Arc<AtomicU64> {
3622            Arc::clone(&self.delivery.recovered_delivery_loss_incidents)
3623        }
3624
3625        /// Whether an admitted delivery loss has not yet been repaired by coordinated recovery.
3626        #[must_use]
3627        pub fn has_unrecovered_delivery_loss(&self) -> bool {
3628            self.delivery
3629                .delivery_loss_incidents
3630                .load(Ordering::Acquire)
3631                > self
3632                    .delivery
3633                    .recovered_delivery_loss_incidents
3634                    .load(Ordering::Acquire)
3635        }
3636
3637        /// Mark the loss cutoff captured when `gen` began as repaired. Loss detected after the
3638        /// generation advanced remains above this floor and still faults the next checkpoint.
3639        ///
3640        /// Returns `false` when `gen` is not the currently prepared recovery generation.
3641        pub fn complete_recovery(&self, gen: u64) -> bool {
3642            self.recovery_gen.load(Ordering::Acquire) == gen && self.delivery.complete_recovery(gen)
3643        }
3644
3645        /// Bind and publish the listener's address into `kv` for peer discovery.
3646        ///
3647        /// # Errors
3648        /// Returns `io::Error` on bind failure.
3649        pub async fn bind_with_kv(
3650            local_id: ShufflePeerId,
3651            addr: SocketAddr,
3652            kv: Arc<dyn ClusterKv>,
3653            receiver_incarnation: Uuid,
3654        ) -> io::Result<Self> {
3655            let recv = Self::bind(local_id, addr, receiver_incarnation).await?;
3656            kv.write(SHUFFLE_ADDR_KEY, recv.local_addr.to_string())
3657                .await;
3658            Ok(recv)
3659        }
3660
3661        /// Local socket address the server is bound to.
3662        #[must_use]
3663        pub fn local_addr(&self) -> SocketAddr {
3664            self.local_addr
3665        }
3666
3667        /// Notification fired when shuffle work is ready for an execution cycle.
3668        #[must_use]
3669        pub fn work_ready_notify(&self) -> Arc<tokio::sync::Notify> {
3670            Arc::clone(&self.work_ready)
3671        }
3672
3673        /// Whether admitted work is still waiting in the receive queue.
3674        #[must_use]
3675        pub fn queued_work_ready(&self) -> bool {
3676            if self.deferred_recv_ready.load(Ordering::Acquire) {
3677                return true;
3678            }
3679            self.rx
3680                .lock()
3681                .as_ref()
3682                .is_some_and(|receiver| !receiver.is_empty())
3683        }
3684
3685        /// Revalidate a queued envelope at the consumer boundary. A stream can pass its final
3686        /// atomic check immediately before an assignment/recovery transition and enqueue after
3687        /// it, so wire admission alone is not a sufficient fold fence.
3688        fn retain_queued_scope(
3689            &self,
3690            peer: ShufflePeerId,
3691            sender_incarnation: Uuid,
3692            receiver_incarnation: Uuid,
3693            assignment_version: u64,
3694            recovery_gen: u64,
3695            topology: Option<ShuffleTopologyFence>,
3696        ) -> bool {
3697            if self.process_lease.require_live_io().is_err() {
3698                return false;
3699            }
3700            let Some(assignment) = self.consumption_scope() else {
3701                return false;
3702            };
3703            let Some(assignment) = assignment.as_ref() else {
3704                return false;
3705            };
3706            let current = self.retain_queued_scope_for_assignment(
3707                assignment,
3708                peer,
3709                sender_incarnation,
3710                receiver_incarnation,
3711                assignment_version,
3712                recovery_gen,
3713                topology,
3714            );
3715            if self.process_lease.require_live_io().is_err() {
3716                return false;
3717            }
3718            current
3719        }
3720
3721        fn retain_queued_scope_for_assignment(
3722            &self,
3723            assignment: &InstalledAssignment,
3724            peer: ShufflePeerId,
3725            sender_incarnation: Uuid,
3726            receiver_incarnation: Uuid,
3727            assignment_version: u64,
3728            recovery_gen: u64,
3729            topology: Option<ShuffleTopologyFence>,
3730        ) -> bool {
3731            if topology != assignment.topology {
3732                return false;
3733            }
3734            let current_recovery = self.recovery_gen.load(Ordering::Acquire);
3735            if receiver_incarnation != self.receiver_incarnation {
3736                self.delivery
3737                    .note_loss(peer, 1, "queued-receiver-incarnation");
3738                return false;
3739            }
3740            if recovery_gen < current_recovery {
3741                // A coordinated rewind deliberately abandons this generation. Replay regenerates
3742                // it, so dropping here is required but must not create a second recovery fault.
3743                return false;
3744            }
3745            let current_assignment = self.assignment_version.load(Ordering::Acquire);
3746            let current = recovery_gen == current_recovery
3747                && assignment_version == current_assignment
3748                && assignment.fence.assignment_version == assignment_version
3749                && assignment.certifies(peer, sender_incarnation)
3750                && assignment.certifies(self.local_id, receiver_incarnation)
3751                && self.delivery.matches_process_scope(
3752                    peer,
3753                    sender_incarnation,
3754                    receiver_incarnation,
3755                    assignment_version,
3756                    recovery_gen,
3757                    topology,
3758                );
3759            if !current {
3760                self.delivery.note_loss(peer, 1, "queued-stream-scope");
3761            }
3762            current
3763        }
3764
3765        fn retain_received(&self, received: &ReceivedShuffle) -> bool {
3766            match self.holdover.is_retired_barrier(received) {
3767                Ok(true) => false,
3768                Err(_) => {
3769                    self.delivery
3770                        .note_loss(received.peer, 1, "retired-barrier-assignment-digest");
3771                    false
3772                }
3773                Ok(false) => self.retain_queued_scope(
3774                    received.peer,
3775                    received.sender_incarnation,
3776                    received.receiver_incarnation,
3777                    received.assignment_version,
3778                    received.recovery_gen,
3779                    received.topology,
3780                ),
3781            }
3782        }
3783
3784        fn retain_batch(&self, received: &ReceivedBatch) -> bool {
3785            self.retain_queued_scope(
3786                received.peer,
3787                received.sender_incarnation,
3788                received.receiver_incarnation,
3789                received.assignment_version,
3790                received.recovery_gen,
3791                received.topology,
3792            )
3793        }
3794
3795        fn stage_frontier_if_current(&self, cut: ReceivedFrontierCut) -> bool {
3796            let frontier = cut.frontier();
3797            let assignment = self.assignment.read();
3798            let Some(assignment) = assignment.as_ref() else {
3799                return false;
3800            };
3801            let current = self.retain_queued_scope_for_assignment(
3802                assignment,
3803                frontier.peer,
3804                frontier.sender_incarnation,
3805                frontier.receiver_incarnation,
3806                frontier.assignment_version,
3807                frontier.recovery_gen,
3808                frontier.topology,
3809            ) && self.process_lease.require_live_io().is_ok();
3810            if current {
3811                self.holdover.stage_frontier(cut);
3812            }
3813            current
3814        }
3815
3816        /// Await the next `(peer_id, msg)`; `None` once the server stops and the
3817        /// queue drains. Concurrent callers serialise via `rx_returned`;
3818        /// cancellation-safe.
3819        pub async fn recv(&self) -> Option<ReceivedShuffle> {
3820            loop {
3821                if !self.wait_while_assignment_suspended().await {
3822                    return None;
3823                }
3824
3825                // The receiver lease serializes deferred and live queue admission. A
3826                // cancelled receive can therefore require at most one deferred slot.
3827                let taken = { self.rx.lock().take() };
3828                let Some(rx) = taken else {
3829                    tokio::select! {
3830                        biased;
3831                        () = self.process_lease.wait_until_lost() => return None,
3832                        () = self.rx_returned.notified() => {}
3833                    }
3834                    continue;
3835                };
3836                let mut guard = RxReturnGuard {
3837                    slot: &self.rx,
3838                    notify: &self.rx_returned,
3839                    rx: Some(rx),
3840                };
3841
3842                if let Some(inbound) = self.take_deferred_recv() {
3843                    let pending = PendingInbound {
3844                        slot: &self.deferred_recv,
3845                        ready: &self.deferred_recv_ready,
3846                        inbound: Some(inbound),
3847                    };
3848                    let received = pending.take().into_received();
3849                    drop(guard);
3850                    if self.retain_received(&received) {
3851                        return Some(received);
3852                    }
3853                    continue;
3854                }
3855                let inbound = tokio::select! {
3856                    biased;
3857                    () = self.process_lease.wait_until_lost() => return None,
3858                    result = guard.rx.as_mut()?.recv() => result.ok()?,
3859                };
3860                let pending = PendingInbound {
3861                    slot: &self.deferred_recv,
3862                    ready: &self.deferred_recv_ready,
3863                    inbound: Some(inbound),
3864                };
3865                #[cfg(test)]
3866                {
3867                    let pause = { self.recv_deferred_pause.lock().take() };
3868                    if let Some(entered) = pause {
3869                        entered.notify_one();
3870                        std::future::pending::<()>().await;
3871                    }
3872                }
3873                let received = pending.take().into_received();
3874                drop(guard);
3875                if self.retain_received(&received) {
3876                    return Some(received);
3877                }
3878            }
3879        }
3880
3881        /// Drain every currently available admitted message without blocking;
3882        /// empty if a `recv()` holds the receiver.
3883        #[must_use]
3884        pub fn drain_available(&self) -> Vec<ReceivedShuffle> {
3885            let mut out = Vec::new();
3886            {
3887                let slot = self.rx.lock();
3888                if let Some(rx) = slot.as_ref() {
3889                    if let Some(item) = self.take_deferred_recv() {
3890                        let received = item.into_received();
3891                        if self.retain_received(&received) {
3892                            out.push(received);
3893                        }
3894                    }
3895                    while let Ok(item) = rx.try_recv() {
3896                        let received = item.into_received();
3897                        if self.retain_received(&received) {
3898                            out.push(received);
3899                        }
3900                    }
3901                }
3902            }
3903            out
3904        }
3905
3906        /// Drain the inbound queue into `staged`: bucket data by stage and stop at the first
3907        /// ordered control. The control must be consumed before later data can pass it.
3908        /// Once the shared holdover is full, leave the bounded receive queue intact so transport
3909        /// admission backpressures peers.
3910        fn drain_inbound_into(&self, staged: &mut FxHashMap<String, Vec<ReceivedBatch>>) -> bool {
3911            if self.holdover.has_staged_barriers() || self.holdover.has_staged_frontiers() {
3912                return false;
3913            }
3914            let slot = self.rx.lock();
3915            let Some(rx) = slot.as_ref() else {
3916                return false;
3917            };
3918            while self.holdover.try_reserve_item() {
3919                let inbound = if let Some(deferred) = self.take_deferred_recv() {
3920                    deferred
3921                } else {
3922                    let Ok(inbound) = rx.try_recv() else {
3923                        self.holdover.release_items(1);
3924                        return true;
3925                    };
3926                    inbound
3927                };
3928                let received = inbound.into_received();
3929                if !self.retain_received(&received) {
3930                    self.holdover.release_items(1);
3931                    continue;
3932                }
3933                let ReceivedShuffle {
3934                    peer,
3935                    message,
3936                    reservation,
3937                    sender_incarnation,
3938                    receiver_incarnation,
3939                    stream_id,
3940                    assignment_version,
3941                    assignment_digest,
3942                    recovery_gen,
3943                    topology,
3944                    checkpoint_sequence,
3945                } = received;
3946                match message {
3947                    ShuffleMessage::Data {
3948                        stage: s,
3949                        routed_vnodes,
3950                        batch,
3951                    } => {
3952                        staged.entry(s).or_default().push(ReceivedBatch {
3953                            batch,
3954                            routed_vnodes,
3955                            reservation,
3956                            peer,
3957                            sender_incarnation,
3958                            receiver_incarnation,
3959                            stream_id,
3960                            assignment_version,
3961                            recovery_gen,
3962                            topology,
3963                            checkpoint_sequence,
3964                        });
3965                    }
3966                    ShuffleMessage::Frontier {
3967                        stage,
3968                        watermark,
3969                        idle,
3970                    } => {
3971                        let frontier = ReceivedShuffle {
3972                            peer,
3973                            message: ShuffleMessage::Frontier {
3974                                stage: stage.clone(),
3975                                watermark,
3976                                idle,
3977                            },
3978                            reservation,
3979                            sender_incarnation,
3980                            receiver_incarnation,
3981                            stream_id,
3982                            assignment_version,
3983                            assignment_digest,
3984                            recovery_gen,
3985                            topology,
3986                            checkpoint_sequence,
3987                        };
3988                        let preceding = take_frontier_prefix(staged, &stage, &frontier);
3989                        let cut = ReceivedFrontierCut {
3990                            preceding,
3991                            frontier,
3992                        };
3993                        let item_count = cut.item_count();
3994                        if self.stage_frontier_if_current(cut) {
3995                            return false;
3996                        }
3997                        self.holdover.release_items(item_count);
3998                    }
3999                    ShuffleMessage::Barrier(b) => {
4000                        let barrier = ReceivedShuffle {
4001                            peer,
4002                            message: ShuffleMessage::Barrier(b),
4003                            reservation,
4004                            sender_incarnation,
4005                            receiver_incarnation,
4006                            stream_id,
4007                            assignment_version,
4008                            assignment_digest,
4009                            recovery_gen,
4010                            topology,
4011                            checkpoint_sequence,
4012                        };
4013                        match self.holdover.stage_barrier(barrier) {
4014                            Ok(true) => return false,
4015                            Err(_) => self
4016                                .delivery
4017                                .note_loss(peer, 1, "barrier-holdover-protocol"),
4018                            Ok(false) => {}
4019                        }
4020                        self.holdover.release_items(1);
4021                    }
4022                }
4023            }
4024            false
4025        }
4026
4027        /// Non-blocking drain of checkpointed operator batches for `stage`.
4028        #[must_use]
4029        pub fn drain_checkpointed_data_for(&self, stage: &str) -> Vec<ReceivedBatch> {
4030            let mut staged = self.holdover.staged.lock();
4031            let _ = self.drain_inbound_into(&mut staged);
4032            let mut batches = staged.remove(stage).unwrap_or_default();
4033            self.holdover.release_items(batches.len());
4034            batches.retain(|batch| self.retain_batch(batch));
4035            batches
4036        }
4037
4038        /// Take the barriers stashed by [`Self::drain_checkpointed_data_for`].
4039        #[must_use]
4040        pub fn drain_staged_barriers(&self) -> Vec<ReceivedShuffle> {
4041            let mut barriers = self.holdover.take_staged_barriers();
4042            self.holdover.release_items(barriers.len());
4043            barriers.retain(|barrier| self.retain_received(barrier));
4044            barriers
4045        }
4046
4047        /// Take the frontier that stopped the checkpointed data drainer.
4048        #[must_use]
4049        pub fn drain_staged_frontiers(&self) -> Vec<ReceivedFrontierCut> {
4050            let mut frontiers = self.holdover.take_staged_frontiers();
4051            let item_count = frontiers.iter().map(ReceivedFrontierCut::item_count).sum();
4052            self.holdover.release_items(item_count);
4053            frontiers.retain(|cut| self.retain_received(cut.frontier()));
4054            frontiers
4055        }
4056
4057        /// Whether an in-band barrier currently blocks normal holdover draining.
4058        #[must_use]
4059        pub fn has_staged_checkpoint_barriers(&self) -> bool {
4060            self.holdover.has_staged_barriers()
4061        }
4062
4063        /// Move available inbound frames into the bounded holdover through the first barrier.
4064        /// Data remains keyed for its owning stage.
4065        #[must_use]
4066        pub fn stage_checkpointed_inbound(&self) -> bool {
4067            if self.holdover.has_staged_barriers() {
4068                return true;
4069            }
4070            if self.holdover.has_staged_frontiers() {
4071                return false;
4072            }
4073            let arrived = self.barrier_arrivals.load(Ordering::Acquire);
4074            if arrived == self.barrier_reconciled.load(Ordering::Acquire) {
4075                return false;
4076            }
4077            let mut staged = self.holdover.staged.lock();
4078            let exhausted = self.drain_inbound_into(&mut staged);
4079            let has_barrier = self.holdover.has_staged_barriers();
4080            if exhausted && !has_barrier {
4081                self.barrier_reconciled.store(arrived, Ordering::Release);
4082            }
4083            has_barrier
4084        }
4085
4086        /// Retire barrier markers through a certified terminal checkpoint without dropping data.
4087        /// # Errors
4088        /// Rejects a noncanonical attempt and exact markers whose assignment digest differs from
4089        /// the durable terminal outcome.
4090        pub fn retire_checkpoint_barriers(
4091            &self,
4092            attempt: CheckpointAttempt,
4093            assignment_digest: [u8; 32],
4094        ) -> io::Result<()> {
4095            self.holdover
4096                .retire_checkpoint_attempt(attempt, assignment_digest)
4097        }
4098
4099        /// Re-stash a peer barrier pulled while aligning a *different* checkpoint, so a
4100        /// lagging node still sees it when it reaches that checkpoint.
4101        pub fn stash_barrier(&self, barrier: ReceivedShuffle) {
4102            debug_assert!(matches!(barrier.message(), ShuffleMessage::Barrier(_)));
4103            if !self.retain_received(&barrier) {
4104                return;
4105            }
4106            if !self.holdover.try_reserve_item() {
4107                self.delivery
4108                    .note_loss(barrier.peer, 1, "barrier-holdover-capacity");
4109                return;
4110            }
4111            let peer = barrier.peer;
4112            match self.holdover.stage_barrier(barrier) {
4113                Ok(true) => {}
4114                Err(_) => {
4115                    self.holdover.release_items(1);
4116                    self.delivery
4117                        .note_loss(peer, 1, "barrier-holdover-protocol");
4118                }
4119                Ok(false) => self.holdover.release_items(1),
4120            }
4121        }
4122
4123        /// Drain every staged checkpointed operator batch.
4124        #[must_use]
4125        pub fn drain_checkpointed_staged(&self) -> Vec<(String, ReceivedBatch)> {
4126            let mut staged = self.holdover.staged.lock();
4127            let _ = self.drain_inbound_into(&mut staged);
4128            self.take_checkpointed_staged(&mut staged)
4129        }
4130
4131        /// Drain only the checkpointed batches already in the holdover. Unlike
4132        /// [`Self::drain_checkpointed_staged`], this does not consume the live receive queue, whose
4133        /// data/barrier order must remain visible to checkpoint alignment.
4134        ///
4135        /// # Errors
4136        /// Returns a typed cancellation while assignment consumption is suspended, a process
4137        /// lease error after this receiver loses execution authority, or `WouldBlock` when an
4138        /// ordered frontier cut must be consumed first. An error leaves the holdover untouched.
4139        pub fn drain_checkpointed_holdover(&self) -> io::Result<Vec<(String, ReceivedBatch)>> {
4140            let mut staged = self.holdover.staged.lock();
4141            self.process_lease.require_live_io()?;
4142            let Some(assignment) = self.consumption_scope() else {
4143                self.process_lease.require_live_io()?;
4144                return Err(scope_cancelled_io());
4145            };
4146            let Some(assignment) = assignment.as_ref() else {
4147                self.process_lease.require_live_io()?;
4148                return Err(scope_cancelled_io());
4149            };
4150            if self.assignment_version.load(Ordering::Acquire) == 0 {
4151                self.process_lease.require_live_io()?;
4152                return Err(scope_cancelled_io());
4153            }
4154            if self.holdover.has_staged_frontiers() {
4155                return Err(io::Error::new(
4156                    io::ErrorKind::WouldBlock,
4157                    "shuffle frontier cut must be consumed before checkpoint holdover transfer",
4158                ));
4159            }
4160
4161            let item_count = staged.values().map(Vec::len).sum();
4162            let drained = staged
4163                .drain()
4164                .flat_map(|(stage, batches)| {
4165                    batches.into_iter().filter_map(move |batch| {
4166                        let current = self.retain_queued_scope_for_assignment(
4167                            assignment,
4168                            batch.peer,
4169                            batch.sender_incarnation,
4170                            batch.receiver_incarnation,
4171                            batch.assignment_version,
4172                            batch.recovery_gen,
4173                            batch.topology,
4174                        );
4175                        current.then(|| (stage.clone(), batch))
4176                    })
4177                })
4178                .collect();
4179            self.holdover.release_items(item_count);
4180            Ok(drained)
4181        }
4182
4183        fn take_checkpointed_staged(
4184            &self,
4185            staged: &mut FxHashMap<String, Vec<ReceivedBatch>>,
4186        ) -> Vec<(String, ReceivedBatch)> {
4187            let item_count = staged.values().map(Vec::len).sum();
4188            let drained = staged
4189                .drain()
4190                .flat_map(|(stage, batches)| {
4191                    batches.into_iter().filter_map(move |batch| {
4192                        self.retain_batch(&batch).then(|| (stage.clone(), batch))
4193                    })
4194                })
4195                .collect();
4196            self.holdover.release_items(item_count);
4197            drained
4198        }
4199
4200        /// Empty the per-stage holdover, returning every buffered `(stage, batch)`.
4201        #[must_use]
4202        pub fn drain_all_staged(&self) -> Vec<(String, ReceivedBatch)> {
4203            let mut staged = self.holdover.staged.lock();
4204            let frontiers = self.holdover.take_staged_frontiers();
4205            let item_count = staged.values().map(Vec::len).sum::<usize>()
4206                + frontiers
4207                    .iter()
4208                    .map(ReceivedFrontierCut::item_count)
4209                    .sum::<usize>();
4210            let mut drained: Vec<_> = staged
4211                .drain()
4212                .flat_map(|(stage, batches)| {
4213                    batches.into_iter().filter_map(move |batch| {
4214                        self.retain_batch(&batch).then(|| (stage.clone(), batch))
4215                    })
4216                })
4217                .collect();
4218            for cut in frontiers {
4219                let stage = cut.stage().to_owned();
4220                drained.extend(
4221                    cut.preceding
4222                        .into_iter()
4223                        .filter(|batch| self.retain_batch(batch))
4224                        .map(|batch| (stage.clone(), batch)),
4225                );
4226            }
4227            self.holdover.release_items(item_count);
4228            drained
4229        }
4230    }
4231
4232    /// Returns the receiver to the slot on drop so a cancelled `recv()` future
4233    /// doesn't strand it; wakes the next parked waiter.
4234    struct RxReturnGuard<'a> {
4235        slot: &'a Mutex<Option<InboundRx>>,
4236        notify: &'a tokio::sync::Notify,
4237        rx: Option<InboundRx>,
4238    }
4239
4240    impl Drop for RxReturnGuard<'_> {
4241        fn drop(&mut self) {
4242            if let Some(rx) = self.rx.take() {
4243                *self.slot.lock() = Some(rx);
4244                // notify_one stores a permit; notify_waiters can lose wakeups.
4245                self.notify.notify_one();
4246            }
4247        }
4248    }
4249
4250    /// What we expect next from a peer, carried across reconnects. Assignment and recovery
4251    /// transitions reset both sender and receiver to zero under their respective scope locks.
4252    struct PeerSeq {
4253        fence: StreamFence,
4254        expected: u64,
4255    }
4256
4257    #[derive(Debug, Clone, Copy)]
4258    struct DataReservation {
4259        fence: StreamFence,
4260        seq: u64,
4261        expected: u64,
4262    }
4263
4264    #[derive(Debug, Clone, Copy)]
4265    struct BarrierReservation {
4266        fence: StreamFence,
4267        last_seq: u64,
4268        expected: u64,
4269    }
4270
4271    /// Resolves a prepared logical data or frontier frame exactly once. An unresolved admission
4272    /// is loss unless its exact assignment/recovery lifetime was deliberately cancelled.
4273    struct DataAdmission<'a> {
4274        tracker: &'a DeliveryTracker,
4275        reservation: Option<DataReservation>,
4276        cancel: &'a CancellationToken,
4277    }
4278
4279    impl<'a> DataAdmission<'a> {
4280        fn new(
4281            tracker: &'a DeliveryTracker,
4282            reservation: DataReservation,
4283            cancel: &'a CancellationToken,
4284        ) -> Self {
4285            Self {
4286                tracker,
4287                reservation: Some(reservation),
4288                cancel,
4289            }
4290        }
4291
4292        fn commit_after_enqueue(mut self) -> Result<(), tonic::Status> {
4293            let reservation = self.reservation.take().expect("unresolved data admission");
4294            self.tracker.commit_data(reservation)
4295        }
4296
4297        fn cancel(mut self) {
4298            let _ = self.reservation.take();
4299        }
4300    }
4301
4302    impl Drop for DataAdmission<'_> {
4303        fn drop(&mut self) {
4304            if let Some(reservation) = self.reservation.take() {
4305                if !self.cancel.is_cancelled() {
4306                    self.tracker.abort_data(reservation);
4307                }
4308            }
4309        }
4310    }
4311
4312    /// Tracks delivery per peer and counts frames that were sent but never arrived.
4313    #[derive(Default)]
4314    struct DeliveryTracker {
4315        peers: Mutex<FxHashMap<ShufflePeerId, PeerSeq>>,
4316        ingress: Mutex<FxHashMap<ShufflePeerId, Arc<tokio::sync::Mutex<()>>>>,
4317        delivery_loss_incidents: Arc<AtomicU64>,
4318        recovered_delivery_loss_incidents: Arc<AtomicU64>,
4319        pending_recovery: Mutex<Option<(u64, u64)>>,
4320        completed_recovery_gen: AtomicU64,
4321        topology_version: AtomicU64,
4322    }
4323
4324    impl DeliveryTracker {
4325        // The publisher holds peers from the loss-fence audit through scope publication. Late
4326        // predecessor admissions therefore cannot race that audit or fault the target domain.
4327
4328        fn reset_assignment(&self) {
4329            self.peers.lock().clear();
4330            self.ingress.lock().clear();
4331        }
4332
4333        fn ingress_lock(
4334            &self,
4335            peer: ShufflePeerId,
4336        ) -> Result<Arc<tokio::sync::Mutex<()>>, tonic::Status> {
4337            let mut ingress = self.ingress.lock();
4338            if let Some(lock) = ingress.get(&peer) {
4339                return Ok(Arc::clone(lock));
4340            }
4341            if ingress.len() >= MAX_TRACKED_PEERS {
4342                return Err(tonic::Status::resource_exhausted(
4343                    "too many shuffle ingress peers",
4344                ));
4345            }
4346            let lock = Arc::new(tokio::sync::Mutex::new(()));
4347            ingress.insert(peer, Arc::clone(&lock));
4348            Ok(lock)
4349        }
4350
4351        fn reject_protocol(&self, peer: ShufflePeerId, reason: &str) -> tonic::Status {
4352            self.note_loss(peer, 1, "identity");
4353            tonic::Status::failed_precondition(reason.to_string())
4354        }
4355
4356        /// Reserve a sequence without acknowledging delivery. `None` is an already committed
4357        /// duplicate. The caller must hold this peer's ingress lock through `commit_data`.
4358        fn prepare_data(
4359            &self,
4360            fence: &StreamFence,
4361            seq: u64,
4362        ) -> Result<Option<DataReservation>, tonic::Status> {
4363            let peers = self.peers.lock();
4364            if !self.topology_is_current(fence) {
4365                return Err(scope_cancelled_status());
4366            }
4367            if seq == u64::MAX {
4368                return Err(
4369                    self.reject_protocol(fence.sender_node_id, "shuffle sequence exhausted")
4370                );
4371            }
4372            let Some(state) = peers.get(&fence.sender_node_id) else {
4373                return Err(self.reject_protocol(
4374                    fence.sender_node_id,
4375                    "shuffle data arrived before its exact Hello",
4376                ));
4377            };
4378            if state.fence != *fence {
4379                return Err(self.reject_protocol(
4380                    fence.sender_node_id,
4381                    "shuffle data stream identity was superseded",
4382                ));
4383            }
4384            if seq < state.expected {
4385                return Ok(None);
4386            }
4387            Ok(Some(DataReservation {
4388                fence: *fence,
4389                seq,
4390                expected: state.expected,
4391            }))
4392        }
4393
4394        /// Commit only after every decoded/sliced batch has entered the consumer queue.
4395        fn commit_data(&self, reservation: DataReservation) -> Result<(), tonic::Status> {
4396            let mut peers = self.peers.lock();
4397            if !self.topology_is_current(&reservation.fence) {
4398                return Err(scope_cancelled_status());
4399            }
4400            let next = reservation.seq.checked_add(1).ok_or_else(|| {
4401                self.reject_protocol(
4402                    reservation.fence.sender_node_id,
4403                    "shuffle sequence exhausted",
4404                )
4405            })?;
4406            let Some(state) = peers.get_mut(&reservation.fence.sender_node_id) else {
4407                return Err(self.reject_protocol(
4408                    reservation.fence.sender_node_id,
4409                    "shuffle delivery state disappeared before commit",
4410                ));
4411            };
4412            if state.fence != reservation.fence || state.expected != reservation.expected {
4413                return Err(self.reject_protocol(
4414                    reservation.fence.sender_node_id,
4415                    "shuffle delivery scope changed before commit",
4416                ));
4417            }
4418            state.expected = next;
4419            let missing = reservation.seq - reservation.expected;
4420            if missing > 0 {
4421                self.note_loss(reservation.fence.sender_node_id, missing, "data");
4422            }
4423            Ok(())
4424        }
4425
4426        /// Resolve an admission that did not enqueue every slice. Advancing past the failed
4427        /// logical frame prevents its successor from counting the same hole a second time.
4428        fn abort_data(&self, reservation: DataReservation) {
4429            let mut peers = self.peers.lock();
4430            if !self.topology_is_current(&reservation.fence) {
4431                return;
4432            }
4433            let Some(state) = peers.get_mut(&reservation.fence.sender_node_id) else {
4434                return;
4435            };
4436            if state.fence != reservation.fence || state.expected != reservation.expected {
4437                return;
4438            }
4439            let Some(next) = reservation.seq.checked_add(1) else {
4440                self.note_loss(
4441                    reservation.fence.sender_node_id,
4442                    1,
4443                    "data-admission-sequence-exhausted",
4444                );
4445                return;
4446            };
4447            state.expected = next;
4448            let missing = reservation.seq - reservation.expected + 1;
4449            self.note_loss(reservation.fence.sender_node_id, missing, "data-admission");
4450        }
4451
4452        /// Validate a barrier without advancing delivery state. The caller commits immediately
4453        /// before queue publication so its loss fence is visible with the barrier.
4454        fn prepare_barrier(
4455            &self,
4456            fence: &StreamFence,
4457            last_seq: u64,
4458        ) -> Result<BarrierReservation, tonic::Status> {
4459            let peers = self.peers.lock();
4460            if !self.topology_is_current(fence) {
4461                return Err(scope_cancelled_status());
4462            }
4463            let Some(state) = peers.get(&fence.sender_node_id) else {
4464                return Err(self.reject_protocol(
4465                    fence.sender_node_id,
4466                    "shuffle barrier arrived before its exact Hello",
4467                ));
4468            };
4469            if state.fence != *fence {
4470                return Err(self.reject_protocol(
4471                    fence.sender_node_id,
4472                    "shuffle barrier stream identity was superseded",
4473                ));
4474            }
4475            if last_seq < state.expected {
4476                return Err(self.reject_protocol(
4477                    fence.sender_node_id,
4478                    "shuffle barrier high-water moved backwards",
4479                ));
4480            }
4481            Ok(BarrierReservation {
4482                fence: *fence,
4483                last_seq,
4484                expected: state.expected,
4485            })
4486        }
4487
4488        fn commit_barrier(&self, reservation: BarrierReservation) -> Result<(), tonic::Status> {
4489            let mut peers = self.peers.lock();
4490            if !self.topology_is_current(&reservation.fence) {
4491                return Err(scope_cancelled_status());
4492            }
4493            let Some(state) = peers.get_mut(&reservation.fence.sender_node_id) else {
4494                return Err(self.reject_protocol(
4495                    reservation.fence.sender_node_id,
4496                    "shuffle barrier state disappeared before commit",
4497                ));
4498            };
4499            if state.fence != reservation.fence || state.expected != reservation.expected {
4500                return Err(self.reject_protocol(
4501                    reservation.fence.sender_node_id,
4502                    "shuffle barrier scope changed before commit",
4503                ));
4504            }
4505            state.expected = reservation.last_seq;
4506            let missing = reservation.last_seq - reservation.expected;
4507            if missing > 0 {
4508                self.note_loss(reservation.fence.sender_node_id, missing, "barrier");
4509            }
4510            Ok(())
4511        }
4512    }
4513
4514    /// `ShuffleTransport` service: the producer end of the inbound queue shared by
4515    /// every peer stream.
4516    struct ShuffleService {
4517        local_id: ShufflePeerId,
4518        receiver_incarnation: Uuid,
4519        assignment: Arc<RwLock<Option<Arc<InstalledAssignment>>>>,
4520        assignment_version: Arc<AtomicU64>,
4521        scope_cancel: Arc<RwLock<CancellationToken>>,
4522        process_lease: Arc<ProcessLeaseGate>,
4523        pending_handshakes: Arc<PendingHandshakes>,
4524        tx: InboundTx,
4525        work_ready: Arc<tokio::sync::Notify>,
4526        recovery_gen: Arc<AtomicU64>,
4527        topology_version: Arc<AtomicU64>,
4528        pipeline_identity: Arc<RwLock<Option<[u8; 32]>>>,
4529        delivery: Arc<DeliveryTracker>,
4530        barrier_arrivals: Arc<AtomicU64>,
4531        holdover: Arc<Holdover>,
4532        inbound_budget: Arc<InboundBudget>,
4533        active_streams: Arc<Semaphore>,
4534        active_stream_registry: Arc<ActiveStreamRegistry>,
4535    }
4536
4537    fn active_receiver_scope(
4538        assignment: &RwLock<Option<Arc<InstalledAssignment>>>,
4539        assignment_version: &AtomicU64,
4540        recovery_gen: &AtomicU64,
4541        scope_cancel: &RwLock<CancellationToken>,
4542        process_lease: &ProcessLeaseGate,
4543    ) -> Result<ScopeLease, tonic::Status> {
4544        let assignment = assignment.read();
4545        let installed = assignment.as_ref().ok_or_else(|| {
4546            tonic::Status::failed_precondition("shuffle assignment certificate is not installed")
4547        })?;
4548        let version = assignment_version.load(Ordering::Acquire);
4549        let recovery_gen = recovery_gen.load(Ordering::Acquire);
4550        let cancel = scope_cancel.read().clone();
4551        process_lease.require_live_status()?;
4552        if version == 0 || version != installed.fence.assignment_version || cancel.is_cancelled() {
4553            return Err(scope_cancelled_status());
4554        }
4555        Ok(ScopeLease {
4556            assignment: Arc::clone(installed),
4557            recovery_gen,
4558            cancel,
4559        })
4560    }
4561
4562    #[tonic::async_trait]
4563    impl ShuffleTransport for ShuffleService {
4564        async fn handshake(
4565            &self,
4566            request: Request<HandshakeRequest>,
4567        ) -> Result<tonic::Response<HandshakeResponse>, tonic::Status> {
4568            let request = request.into_inner();
4569            let (fence, scope) = self.admit_handshake(&request)?;
4570            let now = std::time::Instant::now();
4571            let mut pending = self.pending_handshakes.0.lock();
4572            pending.retain(|_, handshake| {
4573                now.saturating_duration_since(handshake.issued_at) < HANDSHAKE_TOKEN_TTL
4574            });
4575            if pending.len() >= MAX_PENDING_HANDSHAKES {
4576                return Err(tonic::Status::resource_exhausted(
4577                    "too many unconsumed shuffle handshakes",
4578                ));
4579            }
4580            pending.insert(
4581                request.sender_node_id,
4582                PendingHandshake {
4583                    fence,
4584                    issued_at: now,
4585                },
4586            );
4587            drop(pending);
4588            if scope.cancel.is_cancelled() || self.process_lease.require_live_status().is_err() {
4589                self.pending_handshakes
4590                    .0
4591                    .lock()
4592                    .remove(&request.sender_node_id);
4593                return if scope.cancel.is_cancelled() {
4594                    Err(scope_cancelled_status())
4595                } else {
4596                    Err(process_lease_expired_status())
4597                };
4598            }
4599            Ok(tonic::Response::new(HandshakeResponse {
4600                receiver_node_id: self.local_id,
4601                receiver_incarnation: self.receiver_incarnation.as_bytes().to_vec(),
4602                sender_incarnation: fence.sender_incarnation.as_bytes().to_vec(),
4603                stream_id: fence.stream_id.as_bytes().to_vec(),
4604                assignment_version: scope.assignment.fence.assignment_version,
4605                recovery_gen: scope.recovery_gen,
4606                assignment_certificate_digest: scope.assignment.digest.to_vec(),
4607                topology_version: scope
4608                    .assignment
4609                    .topology
4610                    .map_or(0, ShuffleTopologyFence::version),
4611                topology_manifest_sha256: scope
4612                    .assignment
4613                    .topology
4614                    .map_or_else(Vec::new, |t| t.manifest_sha256().to_vec()),
4615            }))
4616        }
4617
4618        async fn shuffle(
4619            &self,
4620            request: Request<tonic::Streaming<ShuffleFrame>>,
4621        ) -> Result<tonic::Response<ShuffleSummary>, tonic::Status> {
4622            let summary = run_stream(self, request.into_inner()).await?;
4623            Ok(tonic::Response::new(summary))
4624        }
4625    }
4626
4627    fn consume_handshake_token(
4628        pending: &PendingHandshakes,
4629        fence: &StreamFence,
4630        now: std::time::Instant,
4631    ) -> bool {
4632        pending
4633            .0
4634            .lock()
4635            .remove(&fence.sender_node_id)
4636            .is_some_and(|token| {
4637                token.fence == *fence
4638                    && now.saturating_duration_since(token.issued_at) < HANDSHAKE_TOKEN_TTL
4639            })
4640    }
4641
4642    /// Consume the short-lived one-time handshake token and validate the leading `Hello`.
4643    async fn admit_stream(
4644        stream: &mut tonic::Streaming<ShuffleFrame>,
4645        receiver_incarnation: Uuid,
4646        assignment: &RwLock<Option<Arc<InstalledAssignment>>>,
4647        assignment_version: &AtomicU64,
4648        recovery_gen: &AtomicU64,
4649        scope_cancel: &RwLock<CancellationToken>,
4650        process_lease: &ProcessLeaseGate,
4651        pending_handshakes: &PendingHandshakes,
4652        delivery: &DeliveryTracker,
4653        active_streams: &Arc<Semaphore>,
4654        active_stream_registry: &Arc<ActiveStreamRegistry>,
4655    ) -> Result<
4656        (
4657            StreamFence,
4658            Arc<tokio::sync::Mutex<()>>,
4659            ScopeLease,
4660            ActiveStreamLease,
4661        ),
4662        tonic::Status,
4663    > {
4664        let scope = active_receiver_scope(
4665            assignment,
4666            assignment_version,
4667            recovery_gen,
4668            scope_cancel,
4669            process_lease,
4670        )?;
4671        let first = tokio::select! {
4672            biased;
4673            () = process_lease.wait_until_lost() => {
4674                return Err(process_lease_expired_status());
4675            }
4676            () = scope.cancel.cancelled() => return Err(scope_cancelled_status()),
4677            first = stream.message() => first?,
4678        }
4679        .ok_or_else(|| tonic::Status::invalid_argument("shuffle stream closed before Hello"))?;
4680        let Some(shuffle_frame::Kind::Hello(hello)) = first.kind else {
4681            return Err(tonic::Status::invalid_argument(
4682                "first shuffle frame must be Hello",
4683            ));
4684        };
4685        let fence = fence_from_hello(&hello)?;
4686        if fence.recovery_gen != scope.recovery_gen {
4687            return Err(tonic::Status::failed_precondition(
4688                "shuffle Hello targets a stale recovery generation",
4689            ));
4690        }
4691        if fence.receiver_incarnation != receiver_incarnation {
4692            return Err(tonic::Status::failed_precondition(
4693                "shuffle stream targets a stale receiver process",
4694            ));
4695        }
4696        if !scope.matches_fence(&fence) || !scope.assignment.matches_stream_sender(&fence) {
4697            return Err(tonic::Status::failed_precondition(
4698                "shuffle stream targets a stale or uncertified assignment",
4699            ));
4700        }
4701        if !consume_handshake_token(pending_handshakes, &fence, std::time::Instant::now()) {
4702            return Err(tonic::Status::failed_precondition(
4703                "shuffle stream did not consume its exact unexpired handshake",
4704            ));
4705        }
4706        let ingress = delivery.ingress_lock(fence.sender_node_id)?;
4707        let ingress_guard = tokio::select! {
4708            biased;
4709            () = process_lease.wait_until_lost() => {
4710                return Err(process_lease_expired_status());
4711            }
4712            () = scope.cancel.cancelled() => return Err(scope_cancelled_status()),
4713            guard = ingress.lock() => guard,
4714        };
4715        delivery.observe_hello(fence)?;
4716        let mut active_stream = active_stream_registry.replace(&fence, &scope.cancel);
4717        tokio::select! {
4718            biased;
4719            () = process_lease.wait_until_lost() => {
4720                return Err(process_lease_expired_status());
4721            }
4722            () = scope.cancel.cancelled() => return Err(scope_cancelled_status()),
4723            result = active_stream.acquire_permit(active_streams) => result?,
4724        }
4725        process_lease.require_live_status()?;
4726        drop(ingress_guard);
4727        Ok((fence, ingress, scope, active_stream))
4728    }
4729
4730    fn validate_active_stream_scope(
4731        assignment_version: &AtomicU64,
4732        recovery_gen: &AtomicU64,
4733        delivery: &DeliveryTracker,
4734        fence: &StreamFence,
4735        cancel: &CancellationToken,
4736        process_lease: &ProcessLeaseGate,
4737    ) -> Result<(), tonic::Status> {
4738        process_lease.require_live_status()?;
4739        if cancel.is_cancelled() {
4740            return Err(scope_cancelled_status());
4741        }
4742        if recovery_gen.load(Ordering::Acquire) != fence.recovery_gen {
4743            return Err(tonic::Status::failed_precondition(
4744                "shuffle recovery generation changed while admitting a frame",
4745            ));
4746        }
4747        if assignment_version.load(Ordering::Acquire) != fence.assignment_version {
4748            return Err(tonic::Status::failed_precondition(
4749                "shuffle assignment changed while admitting a frame",
4750            ));
4751        }
4752        delivery.validate_stream(fence)
4753    }
4754
4755    fn reject_stream_protocol(
4756        delivery: &DeliveryTracker,
4757        fence: &StreamFence,
4758        reason: &str,
4759    ) -> tonic::Status {
4760        let _peers = delivery.peers.lock();
4761        if !delivery.topology_is_current(fence) {
4762            return scope_cancelled_status();
4763        }
4764        delivery.reject_protocol(fence.sender_node_id, reason)
4765    }
4766
4767    /// Publish preceding ordered-frame completeness before making the barrier observable. A failed
4768    /// barrier enqueue cannot seal a checkpoint, while delaying this commit until after enqueue
4769    /// would let the consumer observe the barrier before its loss fence.
4770    async fn publish_barrier(
4771        tx: &InboundTx,
4772        work_ready: &tokio::sync::Notify,
4773        barrier_arrivals: &AtomicU64,
4774        holdover: &Holdover,
4775        assignment_version: &AtomicU64,
4776        recovery_gen: &AtomicU64,
4777        delivery: &DeliveryTracker,
4778        fence: StreamFence,
4779        barrier: CheckpointBarrier,
4780        assignment_digest: [u8; 32],
4781        last_seq: u64,
4782        cancel: &CancellationToken,
4783        process_lease: &ProcessLeaseGate,
4784    ) -> Result<bool, tonic::Status> {
4785        validate_active_stream_scope(
4786            assignment_version,
4787            recovery_gen,
4788            delivery,
4789            &fence,
4790            cancel,
4791            process_lease,
4792        )?;
4793        let retired = holdover
4794            .is_retired_checkpoint_barrier(barrier, assignment_digest)
4795            .map_err(|error| reject_stream_protocol(delivery, &fence, &error.to_string()))?;
4796        let reservation = delivery.prepare_barrier(&fence, last_seq)?;
4797        validate_active_stream_scope(
4798            assignment_version,
4799            recovery_gen,
4800            delivery,
4801            &fence,
4802            cancel,
4803            process_lease,
4804        )?;
4805        delivery.commit_barrier(reservation)?;
4806        if retired {
4807            return Ok(true);
4808        }
4809        tokio::select! {
4810            biased;
4811            () = cancel.cancelled() => Err(scope_cancelled_status()),
4812            result = tx.send(Inbound {
4813                peer: fence.sender_node_id,
4814                msg: ShuffleMessage::Barrier(barrier),
4815                budget: None,
4816                fence,
4817                assignment_digest: Some(assignment_digest),
4818                checkpoint_sequence: last_seq,
4819            }) => {
4820                let enqueued = result.is_ok();
4821                if enqueued {
4822                    barrier_arrivals.fetch_add(1, Ordering::Release);
4823                    work_ready.notify_one();
4824                }
4825                Ok(enqueued)
4826            },
4827        }
4828    }
4829
4830    /// Forward decoded frames onto the inbound queue and summarize on half-close.
4831    async fn run_stream(
4832        service: &ShuffleService,
4833        mut stream: tonic::Streaming<ShuffleFrame>,
4834    ) -> Result<ShuffleSummary, tonic::Status> {
4835        let ShuffleService {
4836            receiver_incarnation,
4837            assignment,
4838            assignment_version,
4839            scope_cancel,
4840            process_lease,
4841            pending_handshakes,
4842            tx,
4843            work_ready,
4844            barrier_arrivals,
4845            holdover,
4846            recovery_gen,
4847            delivery,
4848            inbound_budget,
4849            active_streams,
4850            active_stream_registry,
4851            ..
4852        } = service;
4853        let (fence, ingress, scope, active_stream) = admit_stream(
4854            &mut stream,
4855            *receiver_incarnation,
4856            assignment,
4857            assignment_version,
4858            recovery_gen,
4859            scope_cancel,
4860            process_lease,
4861            pending_handshakes,
4862            delivery,
4863            active_streams,
4864            active_stream_registry,
4865        )
4866        .await?;
4867        let stream_cancel = &active_stream.cancel;
4868        let peer = fence.sender_node_id;
4869
4870        let mut assembly = None;
4871        let mut frames_received = 0u64;
4872        loop {
4873            let frame = tokio::select! {
4874                biased;
4875                () = stream_cancel.cancelled() => return Err(scope_cancelled_status()),
4876                frame = stream.message() => frame?,
4877            };
4878            let Some(frame) = frame else {
4879                break;
4880            };
4881            process_lease.require_live_status()?;
4882            let Some(kind) = frame.kind else {
4883                return Err(reject_stream_protocol(
4884                    delivery,
4885                    &fence,
4886                    "empty shuffle frame",
4887                ));
4888            };
4889            if assignment_version.load(Ordering::Acquire) != fence.assignment_version {
4890                return Err(tonic::Status::failed_precondition(
4891                    "shuffle assignment changed while the stream was active",
4892                ));
4893            }
4894            if recovery_gen.load(Ordering::Acquire) != fence.recovery_gen {
4895                return Err(tonic::Status::failed_precondition(
4896                    "shuffle recovery generation changed while the stream was active",
4897                ));
4898            }
4899            match kind {
4900                shuffle_frame::Kind::Hello(_) => {
4901                    let _ingress_guard = tokio::select! {
4902                        biased;
4903                        () = stream_cancel.cancelled() => return Err(scope_cancelled_status()),
4904                        guard = ingress.lock() => guard,
4905                    };
4906                    return Err(reject_stream_protocol(
4907                        delivery,
4908                        &fence,
4909                        "shuffle Hello is valid only as the leading stream frame",
4910                    ));
4911                }
4912                shuffle_frame::Kind::Barrier(b) => {
4913                    frames_received += 1;
4914                    if assembly.is_some() {
4915                        return Err(reject_stream_protocol(
4916                            delivery,
4917                            &fence,
4918                            "shuffle barrier arrived before its preceding batch completed",
4919                        ));
4920                    }
4921                    let barrier = CheckpointBarrier {
4922                        checkpoint_id: b.checkpoint_id,
4923                        epoch: b.epoch,
4924                        flags: b.flags,
4925                    };
4926                    if !barrier.is_canonical() {
4927                        return Err(reject_stream_protocol(
4928                            delivery,
4929                            &fence,
4930                            NONCANONICAL_BARRIER,
4931                        ));
4932                    }
4933                    let assignment_digest: [u8; 32] =
4934                        b.assignment_digest.as_slice().try_into().map_err(|_| {
4935                            reject_stream_protocol(
4936                                delivery,
4937                                &fence,
4938                                "shuffle barrier assignment digest is not SHA-256 sized",
4939                            )
4940                        })?;
4941                    if b.assignment_version != fence.assignment_version
4942                        || b.recovery_gen != fence.recovery_gen
4943                        || assignment_digest != fence.assignment_certificate_digest
4944                    {
4945                        return Err(reject_stream_protocol(
4946                            delivery,
4947                            &fence,
4948                            "shuffle barrier differs from its stream assignment or recovery scope",
4949                        ));
4950                    }
4951                    let _ingress_guard = tokio::select! {
4952                        biased;
4953                        () = stream_cancel.cancelled() => return Err(scope_cancelled_status()),
4954                        guard = ingress.lock() => guard,
4955                    };
4956                    if !publish_barrier(
4957                        tx,
4958                        work_ready,
4959                        barrier_arrivals,
4960                        holdover,
4961                        assignment_version,
4962                        recovery_gen,
4963                        delivery,
4964                        fence,
4965                        barrier,
4966                        assignment_digest,
4967                        b.last_seq,
4968                        stream_cancel,
4969                        process_lease,
4970                    )
4971                    .await?
4972                    {
4973                        break;
4974                    }
4975                }
4976                shuffle_frame::Kind::Frontier(v) => {
4977                    frames_received += 1;
4978                    if assembly.is_some() {
4979                        return Err(reject_stream_protocol(
4980                            delivery,
4981                            &fence,
4982                            "shuffle frontier arrived before its preceding batch completed",
4983                        ));
4984                    }
4985                    if v.recovery_gen != fence.recovery_gen {
4986                        return Err(reject_stream_protocol(
4987                            delivery,
4988                            &fence,
4989                            "shuffle frontier generation differs from its stream handshake",
4990                        ));
4991                    }
4992                    validate_frontier(&v.stage, v.watermark).map_err(|error| {
4993                        reject_stream_protocol(delivery, &fence, &error.to_string())
4994                    })?;
4995                    let _ingress_guard = tokio::select! {
4996                        biased;
4997                        () = stream_cancel.cancelled() => return Err(scope_cancelled_status()),
4998                        guard = ingress.lock() => guard,
4999                    };
5000                    validate_active_stream_scope(
5001                        assignment_version,
5002                        recovery_gen,
5003                        delivery,
5004                        &fence,
5005                        stream_cancel,
5006                        process_lease,
5007                    )?;
5008                    let Some(reservation) = delivery.prepare_data(&fence, v.seq)? else {
5009                        continue;
5010                    };
5011                    let mut reservation =
5012                        Some(DataAdmission::new(delivery, reservation, stream_cancel));
5013                    let forwarded = forward_frontier(
5014                        tx,
5015                        work_ready,
5016                        fence,
5017                        v.stage,
5018                        v.watermark,
5019                        v.idle,
5020                        v.seq,
5021                        stream_cancel,
5022                        process_lease,
5023                    )
5024                    .await;
5025                    match forwarded {
5026                        Ok(true) => {
5027                            if let Some(admission) = reservation.take() {
5028                                admission.commit_after_enqueue()?;
5029                            }
5030                        }
5031                        Ok(false) => break,
5032                        Err(status) => {
5033                            if status.code() == tonic::Code::Cancelled {
5034                                if let Some(admission) = reservation.take() {
5035                                    admission.cancel();
5036                                }
5037                            }
5038                            return Err(status);
5039                        }
5040                    }
5041                    validate_active_stream_scope(
5042                        assignment_version,
5043                        recovery_gen,
5044                        delivery,
5045                        &fence,
5046                        stream_cancel,
5047                        process_lease,
5048                    )?;
5049                }
5050                shuffle_frame::Kind::Data(v) => {
5051                    frames_received += 1;
5052                    if v.recovery_gen != fence.recovery_gen {
5053                        return Err(reject_stream_protocol(
5054                            delivery,
5055                            &fence,
5056                            "shuffle data generation differs from its stream handshake",
5057                        ));
5058                    }
5059                    let total_payload_bytes = validate_fragment(&v)
5060                        .map_err(|error| reject_stream_protocol(delivery, &fence, &error))?;
5061                    if (v.fragment_index == 0 && assembly.is_some())
5062                        || (v.fragment_index != 0 && assembly.is_none())
5063                    {
5064                        return Err(reject_stream_protocol(
5065                            delivery,
5066                            &fence,
5067                            "shuffle fragments interleaved or started without fragment zero",
5068                        ));
5069                    }
5070                    let budget = if v.fragment_index == 0 {
5071                        Some(
5072                            inbound_budget
5073                                .reserve_frame(peer, total_payload_bytes, stream_cancel)
5074                                .await?,
5075                        )
5076                    } else {
5077                        None
5078                    };
5079                    process_lease.require_live_status()?;
5080                    if assignment_version.load(Ordering::Acquire) != fence.assignment_version
5081                        || recovery_gen.load(Ordering::Acquire) != fence.recovery_gen
5082                    {
5083                        return Err(tonic::Status::failed_precondition(
5084                            "shuffle scope changed while the frame awaited memory admission",
5085                        ));
5086                    }
5087                    let complete = match push_fragment(&mut assembly, &v, budget) {
5088                        Ok(Some(complete)) => complete,
5089                        Ok(None) => continue,
5090                        Err(error) => {
5091                            return Err(reject_stream_protocol(delivery, &fence, &error));
5092                        }
5093                    };
5094                    // Do not retain the final prost frame across blocking decode or queue
5095                    // publication; its payload can be another full wire fragment.
5096                    drop(v);
5097                    let CompleteData {
5098                        stage,
5099                        routed_vnodes,
5100                        seq,
5101                        arrow_ipc,
5102                        budget,
5103                    } = complete;
5104                    let (batch, budget) = decode_ipc_payload_isolated(arrow_ipc, budget, || {})
5105                        .await
5106                        .map_err(|error| {
5107                            reject_stream_protocol(
5108                                delivery,
5109                                &fence,
5110                                &format!("invalid shuffle IPC: {error}"),
5111                            )
5112                        })?;
5113                    let decoded_bytes = InboundBudget::validate_decoded(std::slice::from_ref(
5114                        &batch,
5115                    ))
5116                    .map_err(|status| reject_stream_protocol(delivery, &fence, status.message()))?;
5117                    let _ingress_guard = tokio::select! {
5118                        biased;
5119                        () = stream_cancel.cancelled() => return Err(scope_cancelled_status()),
5120                        guard = ingress.lock() => guard,
5121                    };
5122                    validate_active_stream_scope(
5123                        assignment_version,
5124                        recovery_gen,
5125                        delivery,
5126                        &fence,
5127                        stream_cancel,
5128                        process_lease,
5129                    )?;
5130                    let Some(reservation) = delivery.prepare_data(&fence, seq)? else {
5131                        continue; // already committed duplicate
5132                    };
5133                    let mut reservation =
5134                        Some(DataAdmission::new(delivery, reservation, stream_cancel));
5135                    let forwarded = forward_routed_batch(
5136                        tx,
5137                        work_ready,
5138                        fence,
5139                        service.local_id,
5140                        &scope.assignment,
5141                        stage,
5142                        routed_vnodes,
5143                        batch,
5144                        budget,
5145                        decoded_bytes,
5146                        seq,
5147                        stream_cancel,
5148                        process_lease,
5149                    )
5150                    .await;
5151                    match forwarded {
5152                        Ok(true) => {
5153                            if let Some(admission) = reservation.take() {
5154                                admission.commit_after_enqueue()?;
5155                            }
5156                        }
5157                        Ok(false) => break,
5158                        Err(status) => {
5159                            if status.code() == tonic::Code::Cancelled {
5160                                if let Some(admission) = reservation.take() {
5161                                    admission.cancel();
5162                                }
5163                            }
5164                            return Err(status);
5165                        }
5166                    }
5167                    validate_active_stream_scope(
5168                        assignment_version,
5169                        recovery_gen,
5170                        delivery,
5171                        &fence,
5172                        stream_cancel,
5173                        process_lease,
5174                    )?;
5175                }
5176            }
5177        }
5178        if assembly.is_some() {
5179            return Err(reject_stream_protocol(
5180                delivery,
5181                &fence,
5182                "shuffle stream ended mid-fragment",
5183            ));
5184        }
5185        tracing::debug!(peer, frames_received, "shuffle inbound stream ended");
5186        Ok(ShuffleSummary { frames_received })
5187    }
5188
5189    async fn forward_frontier(
5190        tx: &InboundTx,
5191        work_ready: &tokio::sync::Notify,
5192        fence: StreamFence,
5193        stage: String,
5194        watermark: Option<i64>,
5195        idle: bool,
5196        checkpoint_sequence: u64,
5197        cancel: &CancellationToken,
5198        process_lease: &ProcessLeaseGate,
5199    ) -> Result<bool, tonic::Status> {
5200        process_lease.require_live_status()?;
5201        validate_frontier(&stage, watermark)
5202            .map_err(|error| tonic::Status::invalid_argument(error.to_string()))?;
5203        tokio::select! {
5204            biased;
5205            () = cancel.cancelled() => Err(scope_cancelled_status()),
5206            result = tx.send(Inbound {
5207                peer: fence.sender_node_id,
5208                msg: ShuffleMessage::Frontier { stage, watermark, idle },
5209                budget: None,
5210                fence,
5211                assignment_digest: None,
5212                checkpoint_sequence,
5213            }) => {
5214                let enqueued = result.is_ok();
5215                if enqueued {
5216                    work_ready.notify_one();
5217                }
5218                Ok(enqueued)
5219            },
5220        }
5221    }
5222
5223    /// Forward one logical batch after validating certified vnode ownership. `Ok(false)` when the
5224    /// queue has closed.
5225    async fn forward_routed_batch(
5226        tx: &InboundTx,
5227        work_ready: &tokio::sync::Notify,
5228        fence: StreamFence,
5229        receiver_node_id: ShufflePeerId,
5230        assignment: &InstalledAssignment,
5231        stage: String,
5232        routed_vnodes: Vec<u32>,
5233        batch: RecordBatch,
5234        mut budget: InboundReservation,
5235        decoded_bytes: usize,
5236        checkpoint_sequence: u64,
5237        cancel: &CancellationToken,
5238        process_lease: &ProcessLeaseGate,
5239    ) -> Result<bool, tonic::Status> {
5240        process_lease.require_live_status()?;
5241        if routed_vnodes.is_empty() || !routed_vnodes.windows(2).all(|pair| pair[0] < pair[1]) {
5242            return Err(tonic::Status::invalid_argument(
5243                "shuffle route set is empty or non-canonical",
5244            ));
5245        }
5246        if let Some(vnode) = routed_vnodes
5247            .iter()
5248            .find(|vnode| !assignment.owns_vnode(receiver_node_id, **vnode))
5249        {
5250            return Err(tonic::Status::failed_precondition(format!(
5251                "shuffle vnode {vnode} is not owned by receiver {receiver_node_id}"
5252            )));
5253        }
5254        let routed_vnodes: Arc<[u32]> = routed_vnodes.into();
5255        let msg = ShuffleMessage::Data {
5256            stage,
5257            routed_vnodes,
5258            batch,
5259        };
5260        let ShuffleMessage::Data {
5261            stage,
5262            routed_vnodes,
5263            batch,
5264            ..
5265        } = &msg
5266        else {
5267            unreachable!("constructed data message")
5268        };
5269        let metadata_bytes = retained_batch_metadata_bytes(stage, routed_vnodes, batch)?;
5270        budget.retain_decoded(decoded_bytes, metadata_bytes)?;
5271        let budget = Arc::new(budget);
5272        tokio::select! {
5273            biased;
5274            () = cancel.cancelled() => Err(scope_cancelled_status()),
5275            result = tx.send(Inbound {
5276                peer: fence.sender_node_id,
5277                msg,
5278                budget: Some(budget),
5279                fence,
5280                assignment_digest: None,
5281                checkpoint_sequence,
5282            }) => {
5283                let enqueued = result.is_ok();
5284                if enqueued {
5285                    work_ready.notify_one();
5286                }
5287                Ok(enqueued)
5288            },
5289        }
5290    }
5291
5292    #[cfg(test)]
5293    mod delivery_tests;
5294
5295    #[cfg(test)]
5296    mod encode_tests;
5297
5298    #[cfg(test)]
5299    mod fragment_tests;
5300}
5301
5302#[cfg(feature = "cluster")]
5303pub use grpc::{is_shuffle_send_stalled, ShufflePeerFlow, ShuffleReceiver, ShuffleSender};
5304
5305#[cfg(not(feature = "cluster"))]
5306mod shim {
5307    use std::io;
5308    use std::net::SocketAddr;
5309    use std::sync::Arc;
5310
5311    use crossfire::{mpsc, AsyncRx, MAsyncTx};
5312    use parking_lot::Mutex;
5313    use rustc_hash::FxHashMap;
5314
5315    use super::{
5316        take_frontier_prefix, validate_checkpoint_barrier, validate_frontier, Holdover,
5317        ReceivedBatch, ReceivedFrontierCut, ReceivedShuffle, ShuffleMessage, ShufflePeerId,
5318        SHUFFLE_RECV_QUEUE,
5319    };
5320    use crate::checkpoint::{CheckpointAssignmentFence, CheckpointBarrier};
5321
5322    type InboundRx = AsyncRx<mpsc::Array<ReceivedShuffle>>;
5323    type InboundTx = MAsyncTx<mpsc::Array<ReceivedShuffle>>;
5324
5325    /// Outbound shuffle handle; without the cluster feature there is no transport.
5326    pub struct ShuffleSender {
5327        local_id: ShufflePeerId,
5328    }
5329
5330    impl std::fmt::Debug for ShuffleSender {
5331        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5332            f.debug_struct("ShuffleSender")
5333                .field("local_id", &self.local_id)
5334                .finish_non_exhaustive()
5335        }
5336    }
5337
5338    impl ShuffleSender {
5339        /// Empty sender (no peer fabric without the cluster feature).
5340        #[must_use]
5341        pub fn new(local_id: ShufflePeerId, _incarnation: uuid::Uuid) -> Self {
5342            Self { local_id }
5343        }
5344
5345        /// Node id retained by this networking-free handle.
5346        #[must_use]
5347        pub const fn local_id(&self) -> ShufflePeerId {
5348            self.local_id
5349        }
5350
5351        /// No peer fabric without the cluster feature; matches the cluster build's API.
5352        pub fn set_recovery_gen(&self, _gen: u64) {}
5353
5354        /// No peer fabric without the cluster feature; retain the clustered installation API.
5355        ///
5356        /// # Errors
5357        /// This networking-free implementation never errors.
5358        pub fn install_assignment_fence(
5359            &self,
5360            _fence: &CheckpointAssignmentFence,
5361            _owners: &[ShufflePeerId],
5362        ) -> io::Result<bool> {
5363            Ok(false)
5364        }
5365
5366        /// No cluster fabric exists, so there is no assignment authority to invalidate.
5367        pub fn invalidate_assignment_fence(&self) {}
5368
5369        /// No cluster fabric exists, so there is no assignment authority to suspend.
5370        pub fn suspend_assignment_fence(&self) {}
5371
5372        /// No cluster fabric exists, so no assignment scope is active.
5373        #[must_use]
5374        pub const fn assignment_version(&self) -> u64 {
5375            0
5376        }
5377
5378        /// No cluster fabric exists, so no assignment certificate is active.
5379        #[must_use]
5380        pub const fn active_assignment_digest(&self) -> Option<[u8; 32]> {
5381            None
5382        }
5383
5384        /// No cluster fabric exists, so no recovery scope is active.
5385        #[must_use]
5386        pub const fn recovery_gen(&self) -> u64 {
5387            0
5388        }
5389
5390        /// Preserve the cluster build's API; no address can create a transport in this build.
5391        pub fn register_peer(&self, _peer: ShufflePeerId, _addr: SocketAddr) {}
5392
5393        /// # Errors
5394        /// Always errors because the no-cluster build has no shuffle transport.
5395        pub fn send_to(
5396            &self,
5397            peer: ShufflePeerId,
5398            msg: &ShuffleMessage,
5399        ) -> std::future::Ready<io::Result<()>> {
5400            let validation = match msg {
5401                ShuffleMessage::Barrier(barrier) => validate_checkpoint_barrier(*barrier),
5402                ShuffleMessage::Frontier {
5403                    stage, watermark, ..
5404                } => validate_frontier(stage, *watermark),
5405                ShuffleMessage::Data { .. } => Ok(()),
5406            };
5407            if let Err(error) = validation {
5408                return std::future::ready(Err(error));
5409            }
5410            std::future::ready(Err(io::Error::new(
5411                io::ErrorKind::Unsupported,
5412                format!(
5413                    "node {} cannot send shuffle to peer {peer}: cluster transport is disabled",
5414                    self.local_id
5415                ),
5416            )))
5417        }
5418
5419        /// No cluster fabric exists, so only a local-only assignment has a complete mesh.
5420        ///
5421        /// # Errors
5422        /// Returns an unsupported error when the assignment names a remote participant.
5423        pub fn establish_assignment_mesh(
5424            &self,
5425            assignment_fence: &CheckpointAssignmentFence,
5426        ) -> std::future::Ready<io::Result<()>> {
5427            let result = if assignment_fence
5428                .participants
5429                .iter()
5430                .all(|participant| participant.node_id == self.local_id)
5431            {
5432                Ok(())
5433            } else {
5434                Err(io::Error::new(
5435                    io::ErrorKind::Unsupported,
5436                    "cluster shuffle mesh is disabled",
5437                ))
5438            };
5439            std::future::ready(result)
5440        }
5441
5442        /// Fan out to every required peer, reporting any missing peer after all
5443        /// sends have been attempted.
5444        ///
5445        /// # Errors
5446        /// Returns the first peer error after attempting the full fan-out.
5447        pub async fn fan_out_barrier(
5448            &self,
5449            peers: &[ShufflePeerId],
5450            barrier: CheckpointBarrier,
5451            _assignment_fence: &CheckpointAssignmentFence,
5452        ) -> io::Result<()> {
5453            validate_checkpoint_barrier(barrier)?;
5454            let msg = ShuffleMessage::Barrier(barrier);
5455            let mut first_err = None;
5456            let results =
5457                futures::future::join_all(peers.iter().map(|&peer| self.send_to(peer, &msg))).await;
5458            for result in results {
5459                first_err = first_err.or(result.err());
5460            }
5461            match first_err {
5462                Some(error) => Err(error),
5463                None => Ok(()),
5464            }
5465        }
5466
5467        /// No cluster fabric exists in this build; preserve the guarded-send API for callers.
5468        ///
5469        /// # Errors
5470        /// Returns the same missing-peer error as [`Self::send_to`].
5471        pub async fn send_to_for_assignment(
5472            &self,
5473            peer: ShufflePeerId,
5474            _expected_assignment_version: u64,
5475            msg: &ShuffleMessage,
5476        ) -> io::Result<()> {
5477            self.send_to(peer, msg).await
5478        }
5479    }
5480
5481    /// Inbound shuffle handle: the bounded queue + holdover so the drain/stage API
5482    /// works locally without a network. Parked behind a `Mutex<Option<_>>` for the
5483    /// same `Sync` reason as the gRPC build.
5484    pub struct ShuffleReceiver {
5485        local_id: ShufflePeerId,
5486        local_addr: SocketAddr,
5487        // Keeps the local queue open so `recv` remains cancellation-safe even though this build
5488        // has no producer fabric.
5489        _tx: InboundTx,
5490        rx: Mutex<Option<InboundRx>>,
5491        rx_returned: Arc<tokio::sync::Notify>,
5492        holdover: Arc<Holdover>,
5493    }
5494
5495    impl std::fmt::Debug for ShuffleReceiver {
5496        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5497            f.debug_struct("ShuffleReceiver")
5498                .field("local_id", &self.local_id)
5499                .field("local_addr", &self.local_addr)
5500                .finish_non_exhaustive()
5501        }
5502    }
5503
5504    impl ShuffleReceiver {
5505        /// No peer fabric without the cluster feature; matches the cluster build's API.
5506        pub fn set_recovery_gen(&self, _gen: u64) {}
5507
5508        /// No peer fabric without the cluster feature; retain the clustered installation API.
5509        ///
5510        /// # Errors
5511        /// This networking-free implementation never errors.
5512        pub fn install_assignment_fence(
5513            &self,
5514            _fence: &CheckpointAssignmentFence,
5515            _owners: &[ShufflePeerId],
5516        ) -> io::Result<bool> {
5517            Ok(false)
5518        }
5519
5520        /// No cluster fabric exists, so there is no assignment authority to invalidate.
5521        pub fn invalidate_assignment_fence(&self) {}
5522
5523        /// No cluster fabric exists, so there is no assignment authority to suspend.
5524        pub fn suspend_assignment_fence(&self) {}
5525
5526        /// No cluster fabric exists, so no assignment scope is active.
5527        #[must_use]
5528        pub const fn assignment_version(&self) -> u64 {
5529            0
5530        }
5531
5532        /// No cluster fabric exists, so no assignment certificate is active.
5533        #[must_use]
5534        pub const fn active_assignment_digest(&self) -> Option<[u8; 32]> {
5535            None
5536        }
5537
5538        /// No cluster fabric exists, so no recovery scope is active.
5539        #[must_use]
5540        pub const fn recovery_gen(&self) -> u64 {
5541            0
5542        }
5543
5544        /// No peer fabric without the cluster feature; nothing can be lost in transit.
5545        #[must_use]
5546        pub fn delivery_loss_incidents(&self) -> Arc<std::sync::atomic::AtomicU64> {
5547            Arc::new(std::sync::atomic::AtomicU64::new(0))
5548        }
5549
5550        /// No peer fabric exists, so no delivery-loss incident has required recovery.
5551        #[must_use]
5552        pub fn recovered_delivery_loss_incidents(&self) -> Arc<std::sync::atomic::AtomicU64> {
5553            Arc::new(std::sync::atomic::AtomicU64::new(0))
5554        }
5555
5556        /// No peer fabric exists, so no delivery loss can require recovery.
5557        #[must_use]
5558        pub const fn has_unrecovered_delivery_loss(&self) -> bool {
5559            false
5560        }
5561
5562        /// # Errors
5563        /// Returns `io::Error` on bind failure.
5564        pub async fn bind(
5565            local_id: ShufflePeerId,
5566            addr: SocketAddr,
5567            _incarnation: uuid::Uuid,
5568        ) -> io::Result<Self> {
5569            // Resolve the address (incl. ephemeral port) by binding momentarily.
5570            let listener = tokio::net::TcpListener::bind(addr).await?;
5571            let local_addr = listener.local_addr()?;
5572            drop(listener);
5573            let (tx, rx) = mpsc::bounded_async::<ReceivedShuffle>(SHUFFLE_RECV_QUEUE);
5574            Ok(Self {
5575                local_id,
5576                local_addr,
5577                _tx: tx,
5578                rx: Mutex::new(Some(rx)),
5579                rx_returned: Arc::new(tokio::sync::Notify::new()),
5580                holdover: Arc::new(Holdover::default()),
5581            })
5582        }
5583
5584        /// Local socket address resolved at bind time.
5585        #[must_use]
5586        pub fn local_addr(&self) -> SocketAddr {
5587            self.local_addr
5588        }
5589
5590        /// Await the next admitted message. `None` once all senders drop.
5591        pub async fn recv(&self) -> Option<ReceivedShuffle> {
5592            loop {
5593                let taken = { self.rx.lock().take() };
5594                let Some(rx) = taken else {
5595                    self.rx_returned.notified().await;
5596                    continue;
5597                };
5598                let mut guard = RxReturnGuard {
5599                    slot: &self.rx,
5600                    notify: &self.rx_returned,
5601                    rx: Some(rx),
5602                };
5603                return guard.rx.as_mut()?.recv().await.ok();
5604            }
5605        }
5606
5607        /// Drain every currently available admitted message without blocking.
5608        #[must_use]
5609        pub fn drain_available(&self) -> Vec<ReceivedShuffle> {
5610            let mut out = Vec::new();
5611            let slot = self.rx.lock();
5612            if let Some(rx) = slot.as_ref() {
5613                while let Ok(item) = rx.try_recv() {
5614                    out.push(item);
5615                }
5616            }
5617            out
5618        }
5619
5620        fn drain_inbound_into(&self, staged: &mut FxHashMap<String, Vec<ReceivedBatch>>) {
5621            if self.holdover.has_staged_barriers() || self.holdover.has_staged_frontiers() {
5622                return;
5623            }
5624            let slot = self.rx.lock();
5625            if let Some(rx) = slot.as_ref() {
5626                while self.holdover.try_reserve_item() {
5627                    let Ok(received) = rx.try_recv() else {
5628                        self.holdover.release_items(1);
5629                        break;
5630                    };
5631                    let ReceivedShuffle {
5632                        peer,
5633                        message,
5634                        reservation,
5635                        sender_incarnation,
5636                        receiver_incarnation,
5637                        stream_id,
5638                        assignment_version,
5639                        assignment_digest: _,
5640                        recovery_gen,
5641                        topology,
5642                        checkpoint_sequence,
5643                    } = received;
5644                    match message {
5645                        ShuffleMessage::Data {
5646                            stage,
5647                            routed_vnodes,
5648                            batch,
5649                        } => {
5650                            staged.entry(stage).or_default().push(ReceivedBatch {
5651                                batch,
5652                                routed_vnodes,
5653                                reservation,
5654                                peer,
5655                                sender_incarnation,
5656                                receiver_incarnation,
5657                                stream_id,
5658                                assignment_version,
5659                                recovery_gen,
5660                                topology,
5661                                checkpoint_sequence,
5662                            });
5663                        }
5664                        ShuffleMessage::Frontier {
5665                            stage,
5666                            watermark,
5667                            idle,
5668                        } => {
5669                            let frontier = ReceivedShuffle {
5670                                peer,
5671                                message: ShuffleMessage::Frontier {
5672                                    stage: stage.clone(),
5673                                    watermark,
5674                                    idle,
5675                                },
5676                                reservation,
5677                                sender_incarnation,
5678                                receiver_incarnation,
5679                                stream_id,
5680                                assignment_version,
5681                                assignment_digest: None,
5682                                recovery_gen,
5683                                topology,
5684                                checkpoint_sequence,
5685                            };
5686                            let preceding = take_frontier_prefix(staged, &stage, &frontier);
5687                            self.holdover.stage_frontier(ReceivedFrontierCut {
5688                                preceding,
5689                                frontier,
5690                            });
5691                            break;
5692                        }
5693                        ShuffleMessage::Barrier(barrier) => {
5694                            let barrier = ReceivedShuffle {
5695                                peer,
5696                                message: ShuffleMessage::Barrier(barrier),
5697                                reservation,
5698                                sender_incarnation,
5699                                receiver_incarnation,
5700                                stream_id,
5701                                assignment_version,
5702                                assignment_digest: None,
5703                                recovery_gen,
5704                                topology,
5705                                checkpoint_sequence,
5706                            };
5707                            match self.holdover.stage_barrier(barrier) {
5708                                Ok(true) => break,
5709                                Ok(false) | Err(_) => self.holdover.release_items(1),
5710                            }
5711                        }
5712                    }
5713                }
5714            }
5715        }
5716
5717        /// Non-blocking drain of checkpointed batches for `stage`.
5718        #[must_use]
5719        pub fn drain_checkpointed_data_for(&self, stage: &str) -> Vec<ReceivedBatch> {
5720            let mut staged = self.holdover.staged.lock();
5721            self.drain_inbound_into(&mut staged);
5722            let batches = staged.remove(stage).unwrap_or_default();
5723            self.holdover.release_items(batches.len());
5724            batches
5725        }
5726
5727        /// Take the barriers stashed by [`Self::drain_checkpointed_data_for`].
5728        #[must_use]
5729        pub fn drain_staged_barriers(&self) -> Vec<ReceivedShuffle> {
5730            let barriers = self.holdover.take_staged_barriers();
5731            self.holdover.release_items(barriers.len());
5732            barriers
5733        }
5734
5735        /// Take the frontier that stopped the checkpointed data drainer.
5736        #[must_use]
5737        pub fn drain_staged_frontiers(&self) -> Vec<ReceivedFrontierCut> {
5738            let frontiers = self.holdover.take_staged_frontiers();
5739            let item_count = frontiers.iter().map(ReceivedFrontierCut::item_count).sum();
5740            self.holdover.release_items(item_count);
5741            frontiers
5742        }
5743
5744        /// Empty the per-stage holdover, returning every buffered `(stage, batch)`.
5745        #[must_use]
5746        pub fn drain_all_staged(&self) -> Vec<(String, ReceivedBatch)> {
5747            let mut staged = self.holdover.staged.lock();
5748            let frontiers = self.holdover.take_staged_frontiers();
5749            let item_count = staged.values().map(Vec::len).sum::<usize>()
5750                + frontiers
5751                    .iter()
5752                    .map(ReceivedFrontierCut::item_count)
5753                    .sum::<usize>();
5754            let mut drained: Vec<_> = staged
5755                .drain()
5756                .flat_map(|(stage, batches)| {
5757                    batches
5758                        .into_iter()
5759                        .map(move |staged| (stage.clone(), staged))
5760                })
5761                .collect();
5762            for cut in frontiers {
5763                let stage = cut.stage().to_owned();
5764                drained.extend(
5765                    cut.preceding
5766                        .into_iter()
5767                        .map(|batch| (stage.clone(), batch)),
5768                );
5769            }
5770            self.holdover.release_items(item_count);
5771            drained
5772        }
5773    }
5774
5775    /// Returns the receiver to the slot on drop so a cancelled `recv()` future
5776    /// doesn't strand it; wakes the next parked waiter.
5777    struct RxReturnGuard<'a> {
5778        slot: &'a Mutex<Option<InboundRx>>,
5779        notify: &'a tokio::sync::Notify,
5780        rx: Option<InboundRx>,
5781    }
5782
5783    impl Drop for RxReturnGuard<'_> {
5784        fn drop(&mut self) {
5785            if let Some(rx) = self.rx.take() {
5786                *self.slot.lock() = Some(rx);
5787                self.notify.notify_one();
5788            }
5789        }
5790    }
5791}
5792
5793#[cfg(not(feature = "cluster"))]
5794pub use shim::{ShuffleReceiver, ShuffleSender};
5795
5796#[cfg(all(test, not(feature = "cluster")))]
5797mod shim_tests;
5798
5799#[cfg(all(test, feature = "cluster"))]
5800mod tests;