databricks_zerobus_ingest_sdk/
multiplexed_stream.rs1use futures::future::join_all;
22use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
23use std::sync::Arc;
24use tracing::{error, info, warn};
25
26use crate::{
27 AckCallback, EncodedBatch, EncodedRecord, OffsetId, ZerobusError, ZerobusResult, ZerobusStream,
28};
29
30const STREAM_BITS: u32 = 6;
33const OFFSET_MASK: i64 = (1i64 << (64 - STREAM_BITS)) - 1;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct MessageId(i64);
42
43impl std::fmt::Display for MessageId {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(
46 f,
47 "MessageId(stream={}, offset={})",
48 self.stream_index(),
49 self.sub_offset()
50 )
51 }
52}
53
54impl MessageId {
55 fn new(stream_index: usize, sub_offset: OffsetId) -> Self {
56 debug_assert!(stream_index < (1 << STREAM_BITS));
57 debug_assert!((0..=OFFSET_MASK).contains(&sub_offset));
58 Self(((stream_index as i64) << (64 - STREAM_BITS)) | (sub_offset & OFFSET_MASK))
59 }
60
61 pub fn stream_index(&self) -> usize {
63 ((self.0 as u64) >> (64 - STREAM_BITS)) as usize
64 }
65
66 pub fn sub_offset(&self) -> OffsetId {
68 self.0 & OFFSET_MASK
69 }
70
71 pub fn raw(&self) -> i64 {
73 self.0
74 }
75
76 pub fn from_raw(raw: i64) -> Self {
82 Self(raw)
83 }
84}
85
86struct MultiplexedAckCallbackAdapter {
87 stream_index: usize,
88 callback: Arc<dyn AckCallback<MessageId>>,
89}
90
91impl AckCallback for MultiplexedAckCallbackAdapter {
92 fn on_ack(&self, offset_id: OffsetId) {
93 self.callback
94 .on_ack(MessageId::new(self.stream_index, offset_id));
95 }
96
97 fn on_error(&self, offset_id: OffsetId, error_message: &str) {
98 self.callback
99 .on_error(MessageId::new(self.stream_index, offset_id), error_message);
100 }
101}
102
103#[allow(dead_code)]
108pub(crate) fn multiplexed_ack_callback(
109 stream_index: usize,
110 callback: Arc<dyn AckCallback<MessageId>>,
111) -> Arc<dyn AckCallback> {
112 assert!(
113 stream_index < (1 << STREAM_BITS),
114 "MultiplexedStream supports at most {} sub-streams",
115 1 << STREAM_BITS
116 );
117 Arc::new(MultiplexedAckCallbackAdapter {
118 stream_index,
119 callback,
120 })
121}
122
123pub struct MultiplexedStream {
128 streams: Vec<ZerobusStream>,
129 round_robin_counter: AtomicUsize,
130 is_closed: AtomicBool,
131}
132
133impl MultiplexedStream {
134 pub fn new(streams: Vec<ZerobusStream>) -> Self {
140 assert!(
141 !streams.is_empty(),
142 "MultiplexedStream requires at least one sub-stream"
143 );
144 assert!(
145 streams.len() <= (1 << STREAM_BITS),
146 "MultiplexedStream supports at most {} sub-streams",
147 1 << STREAM_BITS
148 );
149 Self {
150 streams,
151 round_robin_counter: AtomicUsize::new(0),
152 is_closed: AtomicBool::new(false),
153 }
154 }
155
156 #[allow(clippy::result_large_err)]
157 fn check_closed(&self) -> ZerobusResult<()> {
158 if self.is_closed_fast() {
159 return Err(ZerobusError::InvalidStateError(
160 "MultiplexedStream is closed".to_string(),
161 ));
162 }
163 Ok(())
164 }
165
166 fn is_closed_fast(&self) -> bool {
167 self.is_closed.load(Ordering::Relaxed)
168 }
169
170 async fn shutdown_on_failure(&self, trigger_index: usize, cause: &ZerobusError) {
171 if self.is_closed.swap(true, Ordering::Relaxed) {
172 return;
173 }
174
175 error!(
176 trigger_stream_index = trigger_index,
177 cause = %cause,
178 num_streams = self.streams.len(),
179 "MultiplexedStream poisoned due to sub-stream failure"
180 );
181
182 let flush_results = join_all(self.streams.iter().map(|s| s.flush())).await;
183 for (i, result) in flush_results.into_iter().enumerate() {
184 if let Err(e) = result {
185 warn!(stream_index = i, error = %e, "Failed to flush sub-stream during shutdown");
186 }
187 }
188
189 for s in &self.streams {
193 s.signal_shutdown();
194 }
195 }
196
197 fn pick_substream(&self) -> usize {
200 self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % self.streams.len()
201 }
202
203 async fn wait_for_capacity(&self, stream: &ZerobusStream, idx: usize) -> ZerobusResult<()> {
204 let mut backoff_ms = 1u64;
205 let mut total_wait_ms = 0u64;
206 let mut logged_backpressure = false;
207
208 loop {
209 self.check_closed()?;
210
211 if stream.is_closed() {
212 let err = ZerobusError::InvalidStateError(format!(
213 "Sub-stream {} closed unexpectedly",
214 idx
215 ));
216 self.shutdown_on_failure(idx, &err).await;
217 return Err(err);
218 }
219
220 if stream.has_capacity() {
221 return Ok(());
222 }
223
224 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
225 total_wait_ms += backoff_ms;
226 backoff_ms = (backoff_ms * 2).min(50);
227
228 if !logged_backpressure && total_wait_ms >= 1000 {
229 warn!(
230 stream_index = idx,
231 total_wait_ms, "Backpressure: sub-stream at capacity, waiting for drain"
232 );
233 logged_backpressure = true;
234 }
235 }
236 }
237
238 async fn handle_ingest_error(
245 &self,
246 e: ZerobusError,
247 stream: &ZerobusStream,
248 idx: usize,
249 ) -> ZerobusError {
250 if stream.is_closed() {
251 self.shutdown_on_failure(idx, &e).await;
252 } else {
253 warn!(stream_index = idx, error = %e, "Ingest errored but sub-stream still alive");
254 }
255 e
256 }
257
258 pub async fn ingest_record(
265 &self,
266 payload: impl Into<EncodedRecord>,
267 ) -> ZerobusResult<MessageId> {
268 self.check_closed()?;
269 let record = payload.into();
270 let idx = self.pick_substream();
271 let stream = &self.streams[idx];
272 self.wait_for_capacity(stream, idx).await?;
273 self.check_closed()?;
274
275 match stream.ingest_record_offset(record).await {
276 Ok(off) => Ok(MessageId::new(idx, off)),
277 Err(e) => Err(self.handle_ingest_error(e, stream, idx).await),
278 }
279 }
280
281 pub async fn ingest_records<I, T>(&self, payload: I) -> ZerobusResult<Option<MessageId>>
287 where
288 I: IntoIterator<Item = T>,
289 T: Into<EncodedRecord>,
290 {
291 self.check_closed()?;
292 let records: Vec<EncodedRecord> = payload.into_iter().map(Into::into).collect();
293 if records.is_empty() {
294 return Ok(None);
295 }
296 let idx = self.pick_substream();
297 let stream = &self.streams[idx];
298 self.wait_for_capacity(stream, idx).await?;
299 self.check_closed()?;
300
301 match stream.ingest_records_offset(records).await {
302 Ok(sub_offset) => Ok(sub_offset.map(|off| MessageId::new(idx, off))),
303 Err(e) => Err(self.handle_ingest_error(e, stream, idx).await),
304 }
305 }
306
307 pub async fn flush(&self) -> ZerobusResult<()> {
314 self.check_closed()?;
315 let results = join_all(self.streams.iter().map(|s| s.flush())).await;
316 let mut first_error: Option<ZerobusError> = None;
317 let mut first_closed: Option<(usize, ZerobusError)> = None;
318 for (i, result) in results.into_iter().enumerate() {
319 if let Err(e) = result {
320 if self.streams[i].is_closed() && first_closed.is_none() {
321 first_closed = Some((i, e.clone()));
322 }
323 if first_error.is_none() {
324 first_error = Some(e);
325 } else {
326 warn!(
327 stream_index = i,
328 error = %e,
329 "Additional sub-stream flush error (first error will be returned)"
330 );
331 }
332 }
333 }
334 match first_error {
335 Some(e) => {
336 if let Some((closed_idx, closed_err)) = first_closed {
337 self.shutdown_on_failure(closed_idx, &closed_err).await;
338 } else {
339 warn!(error = %e, "flush errored but sub-streams still alive");
340 }
341 Err(e)
342 }
343 None => Ok(()),
344 }
345 }
346
347 pub async fn wait_for_message_id(&self, message_id: MessageId) -> ZerobusResult<()> {
351 let idx = message_id.stream_index();
352 if idx >= self.streams.len() {
353 return Err(ZerobusError::InvalidArgument(format!(
354 "Invalid stream index {} in message id",
355 idx
356 )));
357 }
358 match self.streams[idx]
359 .wait_for_offset(message_id.sub_offset())
360 .await
361 {
362 Ok(()) => Ok(()),
363 Err(e) => {
364 if self.streams[idx].is_closed() {
365 self.shutdown_on_failure(idx, &e).await;
366 } else {
367 warn!(
368 stream_index = idx,
369 error = %e,
370 "wait_for_offset errored but sub-stream still alive"
371 );
372 }
373 Err(e)
374 }
375 }
376 }
377
378 pub async fn close(&mut self) -> ZerobusResult<()> {
384 info!("Closing MultiplexedStream");
385 self.is_closed.store(true, Ordering::Relaxed);
386
387 let mut first_error: Option<ZerobusError> = None;
388
389 let flush_results = join_all(self.streams.iter().map(|s| s.flush())).await;
393 for (i, result) in flush_results.into_iter().enumerate() {
394 if let Err(e) = result {
395 if first_error.is_none() {
396 first_error = Some(e);
397 } else {
398 warn!(
399 stream_index = i,
400 error = %e,
401 "Additional sub-stream flush error during close"
402 );
403 }
404 }
405 }
406
407 for (i, stream) in self.streams.iter_mut().enumerate() {
408 if let Err(e) = stream.close().await {
409 if first_error.is_none() {
410 first_error = Some(e);
411 } else {
412 warn!(
413 stream_index = i,
414 error = %e,
415 "Additional sub-stream close error"
416 );
417 }
418 }
419 }
420
421 match first_error {
422 Some(e) => Err(e),
423 None => Ok(()),
424 }
425 }
426
427 pub fn is_closed(&self) -> bool {
430 self.is_closed_fast() || self.streams.iter().any(ZerobusStream::is_closed)
431 }
432
433 pub async fn get_unacked_records(
439 &mut self,
440 ) -> ZerobusResult<impl Iterator<Item = EncodedRecord>> {
441 let _ = self.close().await;
442 let mut all_records = Vec::new();
443 for stream in &self.streams {
444 all_records.extend(stream.get_unacked_records().await?);
445 }
446 Ok(all_records.into_iter())
447 }
448
449 pub async fn get_unacked_batches(&mut self) -> ZerobusResult<Vec<EncodedBatch>> {
455 let _ = self.close().await;
456 let mut all_batches = Vec::new();
457 for stream in &self.streams {
458 all_batches.extend(stream.get_unacked_batches().await?);
459 }
460 Ok(all_batches)
461 }
462}
463
464impl Drop for MultiplexedStream {
465 fn drop(&mut self) {
466 self.is_closed.store(true, Ordering::Relaxed);
467 for stream in &self.streams {
472 stream.signal_shutdown();
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use std::sync::Mutex;
481
482 #[derive(Default)]
483 struct RecordingMultiplexedCallback {
484 acks: Mutex<Vec<MessageId>>,
485 errors: Mutex<Vec<(MessageId, String)>>,
486 }
487
488 impl AckCallback<MessageId> for RecordingMultiplexedCallback {
489 fn on_ack(&self, message_id: MessageId) {
490 self.acks.lock().unwrap().push(message_id);
491 }
492
493 fn on_error(&self, message_id: MessageId, error_message: &str) {
494 self.errors
495 .lock()
496 .unwrap()
497 .push((message_id, error_message.to_string()));
498 }
499 }
500
501 #[test]
502 #[should_panic(expected = "MultiplexedStream requires at least one sub-stream")]
503 fn test_constructor_panics_on_empty_streams() {
504 MultiplexedStream::new(vec![]);
505 }
506
507 #[test]
508 fn test_message_id_roundtrip() {
509 for stream_idx in 0..64 {
510 for sub_offset in [0i64, 1, 100, 1_000_000, i64::MAX >> STREAM_BITS] {
511 let id = MessageId::new(stream_idx, sub_offset);
512 assert_eq!(id.stream_index(), stream_idx);
513 assert_eq!(id.sub_offset(), sub_offset);
514 }
515 }
516 }
517
518 #[test]
519 fn test_message_id_zero() {
520 let id = MessageId::new(0, 0);
521 assert_eq!(id.raw(), 0);
522 assert_eq!(id.stream_index(), 0);
523 assert_eq!(id.sub_offset(), 0);
524 }
525
526 #[test]
527 fn test_message_id_different_streams_same_offset() {
528 let a = MessageId::new(0, 42);
529 let b = MessageId::new(1, 42);
530 assert_ne!(a, b);
531 assert_eq!(a.sub_offset(), b.sub_offset());
532 assert_ne!(a.stream_index(), b.stream_index());
533 }
534
535 #[test]
536 fn test_multiplexed_ack_callback_routes_substream_ids() {
537 let callback = Arc::new(RecordingMultiplexedCallback::default());
538 let stream_0 = multiplexed_ack_callback(0, callback.clone());
539 let stream_1 = multiplexed_ack_callback(1, callback.clone());
540
541 stream_0.on_ack(42);
542 stream_1.on_ack(42);
543 stream_1.on_error(43, "test error");
544
545 assert_eq!(
546 callback.acks.lock().unwrap().as_slice(),
547 &[MessageId::new(0, 42), MessageId::new(1, 42)]
548 );
549 assert_eq!(
550 callback.errors.lock().unwrap().as_slice(),
551 &[(MessageId::new(1, 43), "test error".to_string())]
552 );
553 }
554
555 #[test]
556 #[should_panic(expected = "MultiplexedStream supports at most 64 sub-streams")]
557 fn test_multiplexed_ack_callback_rejects_invalid_stream_index() {
558 let callback = Arc::new(RecordingMultiplexedCallback::default());
559 multiplexed_ack_callback(64, callback);
560 }
561}