1use super::message::ShuffleMessage;
8use super::ShuffleTopologyFence;
9#[cfg(feature = "cluster")]
10use crate::checkpoint::CheckpointAttempt;
11
12const SHUFFLE_RECV_QUEUE: usize = 256;
15const MAX_STAGE_NAME_BYTES: usize = 4096;
16
17pub 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#[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#[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#[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
130struct 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 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
379pub 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#[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 #[must_use]
415 pub const fn batch(&self) -> &arrow_array::RecordBatch {
416 &self.batch
417 }
418
419 #[must_use]
421 pub fn routed_vnodes(&self) -> &[u32] {
422 &self.routed_vnodes
423 }
424
425 #[must_use]
427 pub fn routed_vnodes_arc(&self) -> std::sync::Arc<[u32]> {
428 std::sync::Arc::clone(&self.routed_vnodes)
429 }
430
431 #[must_use]
433 pub const fn peer(&self) -> ShufflePeerId {
434 self.peer
435 }
436
437 #[must_use]
439 pub const fn sender_incarnation(&self) -> uuid::Uuid {
440 self.sender_incarnation
441 }
442
443 #[must_use]
445 pub const fn receiver_incarnation(&self) -> uuid::Uuid {
446 self.receiver_incarnation
447 }
448
449 #[must_use]
451 pub const fn stream_id(&self) -> uuid::Uuid {
452 self.stream_id
453 }
454
455 #[must_use]
457 pub const fn assignment_version(&self) -> u64 {
458 self.assignment_version
459 }
460
461 #[must_use]
463 pub const fn recovery_gen(&self) -> u64 {
464 self.recovery_gen
465 }
466
467 #[must_use]
469 pub const fn topology_fence(&self) -> Option<ShuffleTopologyFence> {
470 self.topology
471 }
472
473 #[must_use]
475 pub const fn checkpoint_sequence(&self) -> u64 {
476 self.checkpoint_sequence
477 }
478
479 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
505pub 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 #[must_use]
525 pub const fn peer(&self) -> ShufflePeerId {
526 self.peer
527 }
528
529 #[must_use]
531 pub const fn message(&self) -> &ShuffleMessage {
532 &self.message
533 }
534
535 #[must_use]
537 pub const fn sender_incarnation(&self) -> uuid::Uuid {
538 self.sender_incarnation
539 }
540
541 #[must_use]
543 pub const fn receiver_incarnation(&self) -> uuid::Uuid {
544 self.receiver_incarnation
545 }
546
547 #[must_use]
549 pub const fn stream_id(&self) -> uuid::Uuid {
550 self.stream_id
551 }
552
553 #[must_use]
555 pub const fn assignment_version(&self) -> u64 {
556 self.assignment_version
557 }
558
559 #[must_use]
561 pub const fn assignment_digest(&self) -> Option<[u8; 32]> {
562 self.assignment_digest
563 }
564
565 #[must_use]
567 pub const fn recovery_gen(&self) -> u64 {
568 self.recovery_gen
569 }
570
571 #[must_use]
573 pub const fn topology_fence(&self) -> Option<ShuffleTopologyFence> {
574 self.topology
575 }
576
577 #[must_use]
580 pub const fn checkpoint_sequence(&self) -> u64 {
581 self.checkpoint_sequence
582 }
583
584 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#[derive(Debug)]
608pub struct ReceivedFrontierCut {
609 preceding: Vec<ReceivedBatch>,
610 frontier: ReceivedShuffle,
611}
612
613impl ReceivedFrontierCut {
614 #[must_use]
616 pub fn preceding(&self) -> &[ReceivedBatch] {
617 &self.preceding
618 }
619
620 #[must_use]
622 pub const fn frontier(&self) -> &ReceivedShuffle {
623 &self.frontier
624 }
625
626 #[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 const MAX_SCHEMA_WIRE_BYTES: usize = 512 * 1024;
738 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 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 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 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 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 seq: u64,
1030 msg: PreparedMessage,
1031 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 .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 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 #[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 #[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 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 struct PeerConn {
2022 tx: MAsyncTx<mpsc::Array<Outbound>>,
2023 byte_budget: Arc<Semaphore>,
2024 control_byte_budget: Arc<Semaphore>,
2025 counters: Arc<PeerSendCounters>,
2026 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 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 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 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 assignment_suspended: AtomicBool,
2084 recovery_gen: Arc<AtomicU64>,
2086 topology_version: Arc<AtomicU64>,
2087 pipeline_identity: RwLock<Option<[u8; 32]>>,
2088 seqs: Mutex<FxHashMap<ShufflePeerId, u64>>,
2091 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 #[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 #[must_use]
2166 pub const fn local_id(&self) -> ShufflePeerId {
2167 self.local_id
2168 }
2169
2170 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 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 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 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 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 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 #[must_use]
2347 pub fn assignment_version(&self) -> u64 {
2348 self.assignment_version.load(Ordering::Acquire)
2349 }
2350
2351 #[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 pub fn set_recovery_gen(&self, gen: u64) {
2365 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 #[must_use]
2388 pub fn recovery_gen(&self) -> u64 {
2389 self.recovery_gen.load(Ordering::Acquire)
2390 }
2391
2392 #[must_use]
2394 pub const fn incarnation(&self) -> Uuid {
2395 self.sender_incarnation
2396 }
2397
2398 #[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 #[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 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 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 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 let gen = scope.recovery_gen;
2541 let seq = {
2542 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 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 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 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 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 self.pool
2858 .lock()
2859 .retain(|pool_key, connection| *pool_key != key || connection.is_alive());
2860
2861 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 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 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 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 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 pub struct ShuffleReceiver {
3146 local_id: ShufflePeerId,
3147 local_addr: SocketAddr,
3148 receiver_incarnation: Uuid,
3149 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 barrier_arrivals: Arc<AtomicU64>,
3166 barrier_reconciled: AtomicU64,
3167 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 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 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 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 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 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 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 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 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 #[must_use]
3446 pub fn assignment_version(&self) -> u64 {
3447 self.assignment_version.load(Ordering::Acquire)
3448 }
3449
3450 #[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 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 }
3483
3484 #[must_use]
3486 pub fn recovery_gen(&self) -> u64 {
3487 self.recovery_gen.load(Ordering::Acquire)
3488 }
3489
3490 #[must_use]
3492 pub const fn incarnation(&self) -> Uuid {
3493 self.receiver_incarnation
3494 }
3495
3496 #[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 #[must_use]
3614 pub fn delivery_loss_incidents(&self) -> Arc<AtomicU64> {
3615 Arc::clone(&self.delivery.delivery_loss_incidents)
3616 }
3617
3618 #[must_use]
3621 pub fn recovered_delivery_loss_incidents(&self) -> Arc<AtomicU64> {
3622 Arc::clone(&self.delivery.recovered_delivery_loss_incidents)
3623 }
3624
3625 #[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 pub fn complete_recovery(&self, gen: u64) -> bool {
3642 self.recovery_gen.load(Ordering::Acquire) == gen && self.delivery.complete_recovery(gen)
3643 }
3644
3645 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 #[must_use]
3663 pub fn local_addr(&self) -> SocketAddr {
3664 self.local_addr
3665 }
3666
3667 #[must_use]
3669 pub fn work_ready_notify(&self) -> Arc<tokio::sync::Notify> {
3670 Arc::clone(&self.work_ready)
3671 }
3672
3673 #[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 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 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 pub async fn recv(&self) -> Option<ReceivedShuffle> {
3820 loop {
3821 if !self.wait_while_assignment_suspended().await {
3822 return None;
3823 }
3824
3825 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 #[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 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 #[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 #[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 #[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 #[must_use]
4059 pub fn has_staged_checkpoint_barriers(&self) -> bool {
4060 self.holdover.has_staged_barriers()
4061 }
4062
4063 #[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 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 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 #[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 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 #[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 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 self.notify.notify_one();
4246 }
4247 }
4248 }
4249
4250 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 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 #[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 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 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 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 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 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 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 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 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 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 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; };
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 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 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 #[must_use]
5341 pub fn new(local_id: ShufflePeerId, _incarnation: uuid::Uuid) -> Self {
5342 Self { local_id }
5343 }
5344
5345 #[must_use]
5347 pub const fn local_id(&self) -> ShufflePeerId {
5348 self.local_id
5349 }
5350
5351 pub fn set_recovery_gen(&self, _gen: u64) {}
5353
5354 pub fn install_assignment_fence(
5359 &self,
5360 _fence: &CheckpointAssignmentFence,
5361 _owners: &[ShufflePeerId],
5362 ) -> io::Result<bool> {
5363 Ok(false)
5364 }
5365
5366 pub fn invalidate_assignment_fence(&self) {}
5368
5369 pub fn suspend_assignment_fence(&self) {}
5371
5372 #[must_use]
5374 pub const fn assignment_version(&self) -> u64 {
5375 0
5376 }
5377
5378 #[must_use]
5380 pub const fn active_assignment_digest(&self) -> Option<[u8; 32]> {
5381 None
5382 }
5383
5384 #[must_use]
5386 pub const fn recovery_gen(&self) -> u64 {
5387 0
5388 }
5389
5390 pub fn register_peer(&self, _peer: ShufflePeerId, _addr: SocketAddr) {}
5392
5393 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 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 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 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 pub struct ShuffleReceiver {
5485 local_id: ShufflePeerId,
5486 local_addr: SocketAddr,
5487 _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 pub fn set_recovery_gen(&self, _gen: u64) {}
5507
5508 pub fn install_assignment_fence(
5513 &self,
5514 _fence: &CheckpointAssignmentFence,
5515 _owners: &[ShufflePeerId],
5516 ) -> io::Result<bool> {
5517 Ok(false)
5518 }
5519
5520 pub fn invalidate_assignment_fence(&self) {}
5522
5523 pub fn suspend_assignment_fence(&self) {}
5525
5526 #[must_use]
5528 pub const fn assignment_version(&self) -> u64 {
5529 0
5530 }
5531
5532 #[must_use]
5534 pub const fn active_assignment_digest(&self) -> Option<[u8; 32]> {
5535 None
5536 }
5537
5538 #[must_use]
5540 pub const fn recovery_gen(&self) -> u64 {
5541 0
5542 }
5543
5544 #[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 #[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 #[must_use]
5558 pub const fn has_unrecovered_delivery_loss(&self) -> bool {
5559 false
5560 }
5561
5562 pub async fn bind(
5565 local_id: ShufflePeerId,
5566 addr: SocketAddr,
5567 _incarnation: uuid::Uuid,
5568 ) -> io::Result<Self> {
5569 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 #[must_use]
5586 pub fn local_addr(&self) -> SocketAddr {
5587 self.local_addr
5588 }
5589
5590 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 #[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 #[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 #[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 #[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 #[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 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;