laminar_core/shuffle/transport/grpc/
flow.rs1use std::time::{Duration, Instant};
6
7use tokio::sync::TryAcquireError;
8
9use super::*;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub struct ShufflePeerFlow {
15 pub peer: ShufflePeerId,
17 pub send_wait: Duration,
21 pub sent_bytes: u64,
24 pub sent_rows: u64,
26}
27
28#[derive(Default)]
29pub(super) struct PeerSendCounters {
30 waited_ns: AtomicU64,
31 sent_bytes: AtomicU64,
32 sent_rows: AtomicU64,
33}
34
35struct WaitClock<'a> {
37 counters: &'a PeerSendCounters,
38 since: Instant,
39}
40
41impl Drop for WaitClock<'_> {
42 fn drop(&mut self) {
43 let waited = u64::try_from(self.since.elapsed().as_nanos()).unwrap_or(u64::MAX);
44 self.counters.waited_ns.fetch_add(waited, Ordering::Relaxed);
45 }
46}
47
48impl PeerSendCounters {
49 fn start_wait(&self) -> WaitClock<'_> {
50 WaitClock {
51 counters: self,
52 since: Instant::now(),
53 }
54 }
55}
56
57#[derive(Default)]
58pub(super) struct SendFlow {
59 peers: Mutex<FxHashMap<ShufflePeerId, Arc<PeerSendCounters>>>,
60 stall_timeout: Option<Duration>,
61}
62
63#[derive(Debug)]
64struct SendStalled {
65 peer: ShufflePeerId,
66 waiting_for: &'static str,
67 timeout: Duration,
68}
69
70impl std::fmt::Display for SendStalled {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(
73 f,
74 "shuffle send to peer {} waited {:?} for {} without progress",
75 self.peer, self.timeout, self.waiting_for
76 )
77 }
78}
79
80impl std::error::Error for SendStalled {}
81
82#[must_use]
85pub fn is_shuffle_send_stalled(error: &io::Error) -> bool {
86 error
87 .get_ref()
88 .and_then(|source| source.downcast_ref::<SendStalled>())
89 .is_some()
90}
91
92#[derive(Clone, Copy)]
94struct StallLimit {
95 peer: ShufflePeerId,
96 waiting_for: &'static str,
97 timeout: Option<Duration>,
98}
99
100impl StallLimit {
101 async fn elapsed(self) -> io::Error {
103 let Some(timeout) = self.timeout else {
104 return std::future::pending().await;
105 };
106 tokio::time::sleep(timeout).await;
107 io::Error::new(
108 io::ErrorKind::TimedOut,
109 SendStalled {
110 peer: self.peer,
111 waiting_for: self.waiting_for,
112 timeout,
113 },
114 )
115 }
116}
117
118pub(super) struct OutboundReservation {
119 pub(super) peer: OwnedSemaphorePermit,
120 pub(super) node: OwnedSemaphorePermit,
121}
122
123impl OutboundReservation {
124 pub(super) fn shrink_to(&mut self, retained_bytes: usize) -> io::Result<()> {
125 let peer_bytes = self.peer.num_permits();
126 let node_bytes = self.node.num_permits();
127 if peer_bytes != node_bytes || retained_bytes > peer_bytes {
128 return Err(io::Error::other(
129 "shuffle outbound reservation accounting mismatch",
130 ));
131 }
132 let release = peer_bytes - retained_bytes;
133 if release != 0 {
134 let peer = self
135 .peer
136 .split(release)
137 .expect("validated outbound peer reservation split");
138 let node = self
139 .node
140 .split(release)
141 .expect("validated outbound node reservation split");
142 drop((peer, node));
143 }
144 Ok(())
145 }
146}
147
148async fn acquire(
150 budget: &Arc<Semaphore>,
151 permits: u32,
152 cancel: &CancellationToken,
153 counters: &PeerSendCounters,
154 stall: StallLimit,
155 closed: &'static str,
156) -> io::Result<OwnedSemaphorePermit> {
157 if cancel.is_cancelled() {
158 return Err(scope_cancelled_io());
159 }
160 match Arc::clone(budget).try_acquire_many_owned(permits) {
161 Ok(permit) => return Ok(permit),
162 Err(TryAcquireError::Closed) => {
163 return Err(io::Error::new(io::ErrorKind::BrokenPipe, closed));
164 }
165 Err(TryAcquireError::NoPermits) => {}
166 }
167 let _wait = counters.start_wait();
168 tokio::select! {
169 biased;
170 () = cancel.cancelled() => Err(scope_cancelled_io()),
171 permit = Arc::clone(budget).acquire_many_owned(permits) => {
172 permit.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, closed))
173 }
174 error = stall.elapsed() => Err(error),
175 }
176}
177
178impl ShuffleSender {
179 #[must_use]
186 pub fn with_send_stall_timeout(mut self, timeout: Option<Duration>) -> Self {
187 self.flow.stall_timeout = timeout;
188 self
189 }
190
191 fn stall_limit(
192 &self,
193 peer: ShufflePeerId,
194 msg: &ShuffleMessage,
195 waiting_for: &'static str,
196 ) -> StallLimit {
197 StallLimit {
198 peer,
199 waiting_for,
200 timeout: self
201 .flow
202 .stall_timeout
203 .filter(|_| !matches!(msg, ShuffleMessage::Barrier(_))),
204 }
205 }
206
207 #[must_use]
209 pub fn peer_flow(&self) -> Vec<ShufflePeerFlow> {
210 let mut flows = self
211 .flow
212 .peers
213 .lock()
214 .iter()
215 .map(|(&peer, counters)| ShufflePeerFlow {
216 peer,
217 send_wait: Duration::from_nanos(counters.waited_ns.load(Ordering::Relaxed)),
218 sent_bytes: counters.sent_bytes.load(Ordering::Relaxed),
219 sent_rows: counters.sent_rows.load(Ordering::Relaxed),
220 })
221 .collect::<Vec<_>>();
222 flows.sort_unstable_by_key(|flow| flow.peer);
223 flows
224 }
225
226 #[must_use]
230 pub fn outbound_budget_used_bytes(&self) -> usize {
231 OUTBOUND_NODE_BUDGET_BYTES.saturating_sub(self.node_budget.available_permits())
232 }
233
234 pub(super) fn counters_for(&self, peer: ShufflePeerId) -> Arc<PeerSendCounters> {
235 Arc::clone(self.flow.peers.lock().entry(peer).or_default())
236 }
237
238 pub(super) async fn reserve_outbound(
241 &self,
242 conn: &PeerConn,
243 peer: ShufflePeerId,
244 msg: &ShuffleMessage,
245 admission_bytes: usize,
246 cancel: &CancellationToken,
247 ) -> io::Result<OutboundReservation> {
248 let permits = u32::try_from(admission_bytes).map_err(|_| {
249 io::Error::new(
250 io::ErrorKind::InvalidInput,
251 "shuffle admission is too large",
252 )
253 })?;
254 let (peer_budget, node_budget) = if matches!(msg, ShuffleMessage::Barrier(_)) {
255 (
256 &conn.control_byte_budget,
257 &self.checkpointed_control_node_budget,
258 )
259 } else {
260 (&conn.byte_budget, &self.node_budget)
261 };
262 let counters = &conn.counters;
263 let peer_permit = acquire(
264 peer_budget,
265 permits,
266 cancel,
267 counters,
268 self.stall_limit(peer, msg, "its outbound byte budget"),
269 "shuffle byte budget closed",
270 )
271 .await?;
272 let node_permit = acquire(
273 node_budget,
274 permits,
275 cancel,
276 counters,
277 self.stall_limit(peer, msg, "the node's outbound byte budget"),
278 "shuffle node byte budget closed",
279 )
280 .await?;
281 Ok(OutboundReservation {
282 peer: peer_permit,
283 node: node_permit,
284 })
285 }
286
287 pub(super) async fn enqueue(
289 &self,
290 conn: &PeerConn,
291 peer: ShufflePeerId,
292 msg: &ShuffleMessage,
293 out: Outbound,
294 cancel: &CancellationToken,
295 ) -> io::Result<()> {
296 let sent_bytes = match &out.msg {
297 PreparedMessage::Data { arrow_ipc, .. } => arrow_ipc.len(),
298 PreparedMessage::Barrier(_) | PreparedMessage::Frontier { .. } => 0,
299 };
300 match conn.tx.try_send(out) {
301 Ok(()) => {}
302 Err(crossfire::TrySendError::Full(out)) => {
303 let _wait = conn.counters.start_wait();
304 tokio::select! {
305 biased;
306 () = self.process_lease.wait_until_lost() => {
307 return Err(process_lease_expired_io());
308 }
309 () = cancel.cancelled() => return Err(scope_cancelled_io()),
310 result = conn.tx.send(out) => result.map_err(|_| io::Error::new(
311 io::ErrorKind::BrokenPipe,
312 format!("shuffle stream to peer {peer} closed"),
313 ))?,
314 error = self.stall_limit(peer, msg, "a slot in its stream queue").elapsed() => {
315 return Err(error);
316 }
317 }
318 }
319 Err(crossfire::TrySendError::Disconnected(_)) => {
320 return Err(io::Error::new(
321 io::ErrorKind::BrokenPipe,
322 format!("shuffle stream to peer {peer} closed"),
323 ));
324 }
325 }
326 if let ShuffleMessage::Data { batch, .. } = msg {
327 let counters = &conn.counters;
328 counters
329 .sent_bytes
330 .fetch_add(sent_bytes as u64, Ordering::Relaxed);
331 counters
332 .sent_rows
333 .fetch_add(batch.num_rows() as u64, Ordering::Relaxed);
334 }
335 Ok(())
336 }
337}
338
339impl ShuffleReceiver {
340 #[must_use]
343 pub fn inbound_budget_used_bytes(&self) -> usize {
344 INBOUND_NODE_BUDGET_BYTES.saturating_sub(self.inbound_budget.node.available_permits())
345 }
346}