1use std::{
2 future::Future,
3 pin::Pin,
4 task::{Context, Poll},
5 time::Duration,
6};
7
8use async_stream::{stream, try_stream};
9use futures_util::{
10 StreamExt,
11 future::{FutureExt, Shared},
12};
13use s2_api::v1::stream::{ReadEnd, ReadStart};
14use tokio::{
15 sync::oneshot,
16 time::{Instant, timeout},
17};
18use tracing::debug;
19
20use crate::{
21 api::{ApiError, BasinClient, retry_builder},
22 error::{ReadError, RequestError},
23 reconnect::{AdvisedReconnects, ReconnectAdvice},
24 retry::RetryBackoff,
25 session::StreamHeaders,
26 types::{
27 AccessTokenMode, MeteredBytes, ReadBatch, ReadInput, ReadSessionConfig,
28 ReadSessionRetryPolicy, StreamName, StreamPosition,
29 },
30};
31
32#[derive(Debug, thiserror::Error)]
33enum ReadSessionFailure {
34 #[error(transparent)]
35 Api(#[from] ApiError),
36 #[error("heartbeat timeout")]
37 HeartbeatTimeout,
38}
39
40impl ReadSessionFailure {
41 pub fn is_retryable(&self) -> bool {
42 match self {
43 Self::Api(err) => err.is_retryable(),
44 Self::HeartbeatTimeout => true,
45 }
46 }
47
48 fn is_authentication_error(&self) -> bool {
49 matches!(self, Self::Api(error) if error.is_authentication_error())
50 }
51
52 fn is_server_draining(&self) -> bool {
53 matches!(self, Self::Api(error) if error.is_server_draining())
54 }
55}
56
57#[derive(Debug, Clone, thiserror::Error)]
59#[non_exhaustive]
60pub enum ReadSessionError {
61 #[error(transparent)]
63 Read(#[from] ReadError),
64 #[error("heartbeat timeout")]
66 HeartbeatTimeout,
67}
68
69impl ReadSessionError {
70 pub fn is_retryable(&self) -> bool {
72 match self {
73 Self::Read(error) => error.is_retryable(),
74 Self::HeartbeatTimeout => true,
75 }
76 }
77
78 pub fn request_error(&self) -> Option<&RequestError> {
80 match self {
81 Self::Read(error) => error.request_error(),
82 Self::HeartbeatTimeout => None,
83 }
84 }
85}
86
87impl From<ReadSessionFailure> for ReadSessionError {
88 fn from(error: ReadSessionFailure) -> Self {
89 match error {
90 ReadSessionFailure::Api(error) => Self::Read(error.into()),
91 ReadSessionFailure::HeartbeatTimeout => Self::HeartbeatTimeout,
92 }
93 }
94}
95
96const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(20);
99
100type InternalStreaming<R> =
101 Pin<Box<dyn Send + futures_core::Stream<Item = Result<R, ReadSessionFailure>>>>;
102
103enum ReadItem {
105 Batch(ReadBatch),
106 ReconnectAdvised,
111}
112
113#[derive(Debug, Clone, thiserror::Error)]
114#[non_exhaustive]
115pub enum CaughtUpError {
117 #[error("read session ended before catching up")]
118 SessionClosed,
120 #[error(transparent)]
121 Read(#[from] ReadSessionError),
123}
124
125impl CaughtUpError {
126 pub fn is_retryable(&self) -> bool {
128 match self {
129 Self::SessionClosed => false,
130 Self::Read(error) => error.is_retryable(),
131 }
132 }
133
134 pub fn request_error(&self) -> Option<&RequestError> {
136 match self {
137 Self::SessionClosed => None,
138 Self::Read(error) => error.request_error(),
139 }
140 }
141}
142
143type CaughtUpResult = Result<StreamPosition, CaughtUpError>;
144
145#[derive(Clone)]
146enum CaughtUpFuture {
147 Pending(Shared<oneshot::Receiver<CaughtUpResult>>),
148 Ready(CaughtUpResult),
149}
150
151impl Future for CaughtUpFuture {
152 type Output = CaughtUpResult;
153
154 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
155 match &mut *self {
156 Self::Pending(future) => match Pin::new(future).poll(cx) {
157 Poll::Ready(Ok(result)) => Poll::Ready(result),
158 Poll::Ready(Err(_)) => Poll::Ready(Err(CaughtUpError::SessionClosed)),
159 Poll::Pending => Poll::Pending,
160 },
161 Self::Ready(result) => Poll::Ready(result.clone()),
162 }
163 }
164}
165
166struct CaughtUpState {
167 tail: Option<StreamPosition>,
169 terminal: bool,
171 tx: Option<oneshot::Sender<CaughtUpResult>>,
173 future: CaughtUpFuture,
175}
176
177impl CaughtUpState {
178 fn new() -> Self {
179 let (tx, future) = pending_catch_up();
180 Self {
181 tail: None,
182 terminal: false,
183 tx: Some(tx),
184 future,
185 }
186 }
187
188 fn is_caught_up(&self) -> bool {
189 self.tail.is_some()
190 }
191
192 fn future(&self) -> CaughtUpFuture {
193 self.future.clone()
194 }
195
196 fn set_behind(&mut self) {
197 if self.terminal || self.tail.take().is_none() {
198 return;
199 }
200 let (tx, future) = pending_catch_up();
201 self.tx = Some(tx);
202 self.future = future;
203 }
204
205 fn set_caught_up(&mut self, tail: StreamPosition) {
206 if self.terminal || self.tail == Some(tail) {
207 return;
208 }
209 self.tail = Some(tail);
210 self.complete(Ok(tail));
211 }
212
213 fn end(&mut self, error: Option<ReadSessionError>) {
214 if self.terminal {
215 return;
216 }
217 self.terminal = true;
218 if let Some(error) = error {
219 self.tail = None;
220 self.complete(Err(CaughtUpError::Read(error)));
221 } else if self.tail.is_none() {
222 self.complete(Err(CaughtUpError::SessionClosed));
223 }
224 }
225
226 fn complete(&mut self, result: CaughtUpResult) {
227 if let Some(tx) = self.tx.take() {
228 let _ = tx.send(result);
229 } else {
230 self.future = CaughtUpFuture::Ready(result);
231 }
232 }
233}
234
235fn pending_catch_up() -> (oneshot::Sender<CaughtUpResult>, CaughtUpFuture) {
236 let (tx, rx) = oneshot::channel();
237 (tx, CaughtUpFuture::Pending(rx.shared()))
238}
239
240struct ReadUpdate {
241 batch: Option<ReadBatch>,
242 caught_up_tail: Option<StreamPosition>,
243 resume_seq_num: Option<u64>,
244}
245
246impl ReadUpdate {
247 fn behind() -> Self {
248 Self {
249 batch: None,
250 caught_up_tail: None,
251 resume_seq_num: None,
252 }
253 }
254
255 fn from_batch(mut batch: ReadBatch, ignore_command_records: bool) -> Self {
256 let resume_seq_num = resume_seq_num_after_batch(&batch);
257 let caught_up_tail = batch.tail.filter(|tail| {
258 batch.records.is_empty()
259 || batch
260 .records
261 .last()
262 .is_some_and(|record| record.seq_num.checked_add(1) == Some(tail.seq_num))
263 });
264
265 if ignore_command_records {
266 batch.records.retain(|record| !record.is_command_record());
267 }
268
269 Self {
270 batch: (!batch.records.is_empty()).then_some(batch),
271 caught_up_tail,
272 resume_seq_num,
273 }
274 }
275}
276
277pub struct ReadSession {
279 updates: InternalStreaming<ReadUpdate>,
280 state: CaughtUpState,
281 resume_seq_num: Option<u64>,
282}
283
284impl ReadSession {
285 fn new(updates: InternalStreaming<ReadUpdate>, resume_seq_num: Option<u64>) -> Self {
286 Self {
287 updates,
288 state: CaughtUpState::new(),
289 resume_seq_num,
290 }
291 }
292
293 pub fn resume_seq_num(&self) -> Option<u64> {
301 self.resume_seq_num
302 }
303
304 pub fn is_caught_up(&self) -> bool {
310 self.state.is_caught_up()
311 }
312
313 pub fn caught_up(
321 &self,
322 ) -> impl Future<Output = Result<StreamPosition, CaughtUpError>>
323 + Clone
324 + Send
325 + Sync
326 + Unpin
327 + 'static {
328 self.state.future()
329 }
330}
331
332impl futures_core::Stream for ReadSession {
333 type Item = Result<ReadBatch, ReadSessionError>;
334
335 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
336 loop {
337 match self.updates.as_mut().poll_next(cx) {
338 Poll::Pending => return Poll::Pending,
339 Poll::Ready(Some(Ok(update))) => {
340 if let Some(resume_seq_num) = update.resume_seq_num {
341 self.resume_seq_num = Some(resume_seq_num);
342 }
343 if let Some(tail) = update.caught_up_tail {
344 self.state.set_caught_up(tail);
345 } else {
346 self.state.set_behind();
347 }
348 if let Some(batch) = update.batch {
349 return Poll::Ready(Some(Ok(batch)));
350 }
351 }
352 Poll::Ready(Some(Err(error))) => {
353 let error = ReadSessionError::from(error);
354 self.state.end(Some(error.clone()));
355 return Poll::Ready(Some(Err(error)));
356 }
357 Poll::Ready(None) => {
358 self.state.end(None);
359 return Poll::Ready(None);
360 }
361 }
362 }
363 }
364}
365
366impl Drop for ReadSession {
367 fn drop(&mut self) {
368 self.state.end(None);
369 }
370}
371
372pub async fn read_session(
373 client: BasinClient,
374 name: StreamName,
375 headers: StreamHeaders,
376 input: ReadInput,
377 config: ReadSessionConfig,
378) -> Result<ReadSession, ReadSessionError> {
379 let ReadInput {
380 start,
381 stop,
382 ignore_command_records,
383 stream_config: _,
384 } = input;
385 let mut start: ReadStart = start.into();
386 let mut end: ReadEnd = stop.into();
387 let retry_policy = config.retry_policy;
388 let mut retry_backoff = retry_builder(&client.config.retry).build();
389 let access_token_mode = client.config.access_token.mode();
390 let baseline_wait = end.wait;
391 let mut last_tail_at: Option<Instant> = None;
392 let mut advised_reconnects = AdvisedReconnects::default();
393 let initial_resume_seq_num = if start.clamp == Some(true) {
394 None
395 } else {
396 start.seq_num
397 };
398
399 let batches = loop {
400 end.wait = remaining_wait(baseline_wait, last_tail_at);
401 match session_inner(
402 client.clone(),
403 name.clone(),
404 headers.clone(),
405 start.clone(),
406 end.clone(),
407 ReconnectAdvice::default(),
408 advised_reconnects,
409 )
410 .await
411 {
412 Ok(batches) => {
413 retry_backoff.reset();
414 break batches;
415 }
416 Err(err) => {
417 if take_server_draining_reconnect(&err, &mut advised_reconnects) {
418 debug!(
419 advised_reconnects = advised_reconnects.count(),
420 "reconnecting initial read session while server drains"
421 );
422 continue;
423 }
424 if let Some(backoff) =
425 retry_delay(&err, &mut retry_backoff, retry_policy, access_token_mode)
426 {
427 tokio::time::sleep(backoff).await;
428 continue;
429 }
430 return Err(err.into());
431 }
432 }
433 };
434
435 let updates = Box::pin(stream! {
436 let mut batches: Option<InternalStreaming<ReadItem>> = Some(batches);
437
438 loop {
439 if batches.is_none() {
440 end.wait = remaining_wait(baseline_wait, last_tail_at);
441 match session_inner(
442 client.clone(),
443 name.clone(),
444 headers.clone(),
445 start.clone(),
446 end.clone(),
447 ReconnectAdvice::default(),
448 advised_reconnects,
449 ).await {
450 Ok(b) => batches = Some(b),
451 Err(err) => {
452 if take_server_draining_reconnect(&err, &mut advised_reconnects) {
453 debug!(
454 resume_seq_num = ?start.seq_num,
455 advised_reconnects = advised_reconnects.count(),
456 "reconnecting read session while server drains"
457 );
458 continue;
459 }
460 if let Some(backoff) =
461 retry_delay(
462 &err,
463 &mut retry_backoff,
464 retry_policy,
465 access_token_mode,
466 )
467 {
468 tokio::time::sleep(backoff).await;
469 continue;
470 }
471 yield Err(err);
472 break;
473 }
474 }
475 }
476
477 match batches
478 .as_mut()
479 .expect("batches should not be None")
480 .next()
481 .await
482 {
483 Some(Ok(ReadItem::ReconnectAdvised)) => {
484 batches = None;
485 if read_limits_exhausted(&end) {
490 break;
491 }
492 advised_reconnects.record();
493 debug!(
494 resume_seq_num = ?start.seq_num,
495 advised_reconnects = advised_reconnects.count(),
496 "reconnecting read session on server advice"
497 );
498 yield Ok(ReadUpdate::behind());
499 continue;
500 }
501 Some(Ok(ReadItem::Batch(batch))) => {
502 if retry_backoff.used() > 0 {
503 retry_backoff.reset();
504 }
505
506 if batch.tail.is_some() {
507 last_tail_at = Some(Instant::now());
508 }
509
510 update_resume_start(&mut start, &batch);
511 if let Some(count) = end.count.as_mut() {
512 *count = count.saturating_sub(batch.records.len())
513 }
514 if let Some(bytes) = end.bytes.as_mut() {
515 *bytes = bytes.saturating_sub(
516 batch.records.iter().map(|r| r.metered_bytes()).sum()
517 )
518 }
519
520 yield Ok(ReadUpdate::from_batch(batch, ignore_command_records));
521 }
522 Some(Err(err)) => {
523 batches = None;
524 if err.is_server_draining() && read_limits_exhausted(&end) {
525 break;
526 }
527 if take_server_draining_reconnect(&err, &mut advised_reconnects) {
528 debug!(
529 resume_seq_num = ?start.seq_num,
530 advised_reconnects = advised_reconnects.count(),
531 "reconnecting read session while server drains"
532 );
533 yield Ok(ReadUpdate::behind());
534 continue;
535 }
536 if let Some(backoff) =
537 retry_delay(
538 &err,
539 &mut retry_backoff,
540 retry_policy,
541 access_token_mode,
542 )
543 {
544 yield Ok(ReadUpdate::behind());
545 tokio::time::sleep(backoff).await;
546 continue;
547 }
548 yield Err(err);
549 break;
550 }
551 None => break,
552 }
553 }
554 });
555 Ok(ReadSession::new(updates, initial_resume_seq_num))
556}
557
558fn resume_seq_num_after_batch(batch: &ReadBatch) -> Option<u64> {
559 batch
560 .records
561 .last()
562 .map(|record| record.seq_num + 1)
563 .or_else(|| batch.tail.as_ref().map(|tail| tail.seq_num))
564}
565
566fn update_resume_start(start: &mut ReadStart, batch: &ReadBatch) {
571 if let Some(seq_num) = resume_seq_num_after_batch(batch) {
572 *start = ReadStart {
573 seq_num: Some(seq_num),
574 timestamp: None,
575 tail_offset: None,
576 clamp: start.clamp,
577 };
578 }
579}
580
581async fn session_inner(
582 client: BasinClient,
583 name: StreamName,
584 headers: StreamHeaders,
585 start: ReadStart,
586 end: ReadEnd,
587 reconnect: ReconnectAdvice,
588 advised_reconnects: AdvisedReconnects,
589) -> Result<InternalStreaming<ReadItem>, ReadSessionFailure> {
590 let mut batches = client
591 .read_session(
592 &name,
593 start,
594 end,
595 headers.encryption.as_ref(),
596 headers.stream_config.as_ref(),
597 reconnect.clone(),
598 )
599 .await?;
600
601 let mut declined_advice = false;
602 Ok(Box::pin(try_stream! {
603 loop {
604 match timeout(HEARTBEAT_TIMEOUT, batches.next()).await {
605 Ok(Some(batch)) => {
606 yield ReadItem::Batch(ReadBatch::from_api(batch?));
607 if reconnect.is_advised() && !declined_advice {
608 if advised_reconnects.should_reconnect() {
609 yield ReadItem::ReconnectAdvised;
610 break;
611 }
612 declined_advice = true;
613 }
614 }
615 Ok(None) => break,
616 Err(_) => Err(ReadSessionFailure::HeartbeatTimeout)?,
617 }
618 }
619 }))
620}
621
622fn read_limits_exhausted(end: &ReadEnd) -> bool {
624 end.count == Some(0) || end.bytes == Some(0)
625}
626
627fn remaining_wait(baseline_wait: Option<u32>, last_tail_at: Option<Instant>) -> Option<u32> {
634 baseline_wait.map(|w| match last_tail_at {
635 Some(since) => w.saturating_sub(since.elapsed().as_secs() as u32),
636 None => w,
637 })
638}
639
640fn retry_delay(
641 err: &ReadSessionFailure,
642 backoffs: &mut RetryBackoff,
643 retry_policy: ReadSessionRetryPolicy,
644 access_token_mode: AccessTokenMode,
645) -> Option<Duration> {
646 let is_retryable =
647 err.is_retryable() || (access_token_mode.is_refreshable() && err.is_authentication_error());
648 if !is_retryable {
649 debug!(
650 %err,
651 is_retryable = false,
652 retries_exhausted = backoffs.is_exhausted(),
653 "not retrying read session"
654 );
655 return None;
656 }
657
658 let backoff = match retry_policy {
659 ReadSessionRetryPolicy::Budgeted => backoffs.next(),
660 ReadSessionRetryPolicy::Indefinite => Some(backoffs.next_or_max()),
661 };
662 if let Some(backoff) = backoff {
663 debug!(
664 %err,
665 ?backoff,
666 ?retry_policy,
667 num_retries_remaining = backoffs.remaining(),
668 "retrying read session"
669 );
670 Some(backoff)
671 } else {
672 debug!(
673 %err,
674 is_retryable,
675 retries_exhausted = backoffs.is_exhausted(),
676 "not retrying read session"
677 );
678 None
679 }
680}
681
682fn take_server_draining_reconnect(
683 err: &ReadSessionFailure,
684 advised_reconnects: &mut AdvisedReconnects,
685) -> bool {
686 if err.is_server_draining() {
687 advised_reconnects.record();
688 true
689 } else {
690 false
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use bytes::Bytes;
697 use futures_util::{StreamExt, poll, stream};
698 use tokio::sync::mpsc;
699 use tokio_stream::wrappers::UnboundedReceiverStream;
700
701 use super::*;
702 use crate::types::{Header, SequencedRecord};
703
704 fn position(seq_num: u64) -> StreamPosition {
705 StreamPosition {
706 seq_num,
707 timestamp: seq_num,
708 }
709 }
710
711 fn record(seq_num: u64, command: bool) -> SequencedRecord {
712 SequencedRecord {
713 seq_num,
714 timestamp: seq_num,
715 body: Bytes::new(),
716 headers: if command {
717 vec![Header::new("", "fence")]
718 } else {
719 Vec::new()
720 },
721 }
722 }
723
724 fn batch(records: Vec<SequencedRecord>, tail: Option<StreamPosition>) -> ReadBatch {
725 ReadBatch { records, tail }
726 }
727
728 #[test]
729 fn empty_tail_anchors_relative_resume_start() {
730 let mut start = ReadStart {
731 seq_num: None,
732 timestamp: None,
733 tail_offset: Some(0),
734 clamp: Some(true),
735 };
736
737 update_resume_start(&mut start, &batch(Vec::new(), Some(position(42))));
738
739 assert_eq!(start.seq_num, Some(42));
740 assert_eq!(start.timestamp, None);
741 assert_eq!(start.tail_offset, None);
742 assert_eq!(start.clamp, Some(true));
743 }
744
745 fn test_session(
746 updates: impl futures_core::Stream<Item = Result<ReadUpdate, ReadSessionFailure>>
747 + Send
748 + 'static,
749 ) -> ReadSession {
750 ReadSession::new(Box::pin(updates), None)
751 }
752
753 #[tokio::test]
754 async fn empty_tail_exposes_absolute_resume_seq_num() {
755 let (tx, rx) = mpsc::unbounded_channel();
756 let mut session = test_session(UnboundedReceiverStream::new(rx));
757
758 assert_eq!(session.resume_seq_num(), None);
759 tx.send(Ok(ReadUpdate::from_batch(
760 batch(Vec::new(), Some(position(42))),
761 false,
762 )))
763 .unwrap();
764
765 let mut next = Box::pin(session.next());
766 assert!(poll!(next.as_mut()).is_pending());
767 drop(next);
768
769 assert_eq!(session.resume_seq_num(), Some(42));
770 }
771
772 #[tokio::test]
773 async fn caught_up_follows_delivery_and_pins_tail() {
774 let tail = position(2);
775 let mut session = test_session(stream::iter([
776 Ok(ReadUpdate::from_batch(
777 batch(vec![record(0, false), record(1, false)], Some(tail)),
778 false,
779 )),
780 Ok(ReadUpdate::from_batch(
781 batch(vec![record(2, false)], Some(position(5))),
782 false,
783 )),
784 ]));
785 let caught_up = session.caught_up();
786 let mut pending = Box::pin(caught_up.clone());
787
788 assert!(poll!(pending.as_mut()).is_pending());
789 assert!(!session.is_caught_up());
790
791 let first = session.next().await.unwrap().unwrap();
792 assert_eq!(first.records.len(), 2);
793 assert!(session.is_caught_up());
794 assert_eq!(session.resume_seq_num(), Some(2));
795 let caught_up_while_caught = session.caught_up();
796
797 session.next().await.unwrap().unwrap();
798 assert!(!session.is_caught_up());
799 assert_eq!(session.resume_seq_num(), Some(3));
800 assert_eq!(caught_up.await.unwrap(), tail);
801 assert_eq!(caught_up_while_caught.await.unwrap(), tail);
802 }
803
804 #[tokio::test]
805 async fn heartbeat_waits_for_visible_batch() {
806 let tail = position(2);
807 let (tx, rx) = mpsc::unbounded_channel();
808 let mut session = test_session(UnboundedReceiverStream::new(rx));
809 let caught_up = session.caught_up();
810
811 tx.send(Ok(ReadUpdate::from_batch(
812 batch(vec![record(0, false), record(1, false)], None),
813 false,
814 )))
815 .unwrap();
816 tx.send(Ok(ReadUpdate::from_batch(
817 batch(Vec::new(), Some(tail)),
818 false,
819 )))
820 .unwrap();
821
822 assert_eq!(session.next().await.unwrap().unwrap().records.len(), 2);
823 assert!(!session.is_caught_up());
824
825 let mut next = Box::pin(session.next());
826 assert!(poll!(next.as_mut()).is_pending());
827 drop(next);
828 assert!(session.is_caught_up());
829 assert_eq!(caught_up.await.unwrap(), tail);
830 }
831
832 #[tokio::test]
833 async fn unchanged_heartbeat_reuses_caught_up_future() {
834 let tail = position(1);
835 let (tx, rx) = mpsc::unbounded_channel();
836 let mut session = test_session(UnboundedReceiverStream::new(rx));
837
838 tx.send(Ok(ReadUpdate::from_batch(
839 batch(vec![record(0, false)], Some(tail)),
840 false,
841 )))
842 .unwrap();
843 session.next().await.unwrap().unwrap();
844 let caught_up = session.state.future();
845
846 tx.send(Ok(ReadUpdate::from_batch(
847 batch(Vec::new(), Some(tail)),
848 false,
849 )))
850 .unwrap();
851 let mut next = Box::pin(session.next());
852 assert!(poll!(next.as_mut()).is_pending());
853 drop(next);
854
855 let CaughtUpFuture::Pending(caught_up) = caught_up else {
856 panic!("initial caught-up future should use the pending epoch");
857 };
858 let CaughtUpFuture::Pending(current) = session.state.future() else {
859 panic!("unchanged heartbeat should preserve the pending epoch");
860 };
861 assert!(caught_up.ptr_eq(¤t));
862 }
863
864 #[tokio::test]
865 async fn filtered_command_counts_toward_caught_up() {
866 let tail = position(2);
867 let mut session = test_session(stream::iter([
868 Ok(ReadUpdate::from_batch(
869 batch(vec![record(0, false)], None),
870 true,
871 )),
872 Ok(ReadUpdate::from_batch(
873 batch(vec![record(1, true)], Some(tail)),
874 true,
875 )),
876 ]));
877 let caught_up = session.caught_up();
878
879 let delivered = session.next().await.unwrap().unwrap();
880 assert_eq!(delivered.records.len(), 1);
881 assert_eq!(delivered.records[0].seq_num, 0);
882 assert!(!session.is_caught_up());
883
884 assert!(session.next().await.is_none());
885 assert!(session.is_caught_up());
886 assert_eq!(session.resume_seq_num(), Some(2));
887 assert_eq!(caught_up.await.unwrap(), tail);
888 }
889
890 #[tokio::test]
891 async fn caught_up_wait_survives_retry() {
892 let first_tail = position(1);
893 let tail = position(3);
894 let (tx, rx) = mpsc::unbounded_channel();
895 let mut session = test_session(UnboundedReceiverStream::new(rx));
896
897 tx.send(Ok(ReadUpdate::from_batch(
898 batch(Vec::new(), Some(first_tail)),
899 false,
900 )))
901 .unwrap();
902 let mut next = Box::pin(session.next());
903 assert!(poll!(next.as_mut()).is_pending());
904 drop(next);
905 assert!(session.is_caught_up());
906
907 tx.send(Ok(ReadUpdate::behind())).unwrap();
908 let mut next = Box::pin(session.next());
909 assert!(poll!(next.as_mut()).is_pending());
910 drop(next);
911 assert!(!session.is_caught_up());
912 let caught_up = session.caught_up();
913
914 tx.send(Ok(ReadUpdate::behind())).unwrap();
915 tx.send(Ok(ReadUpdate::from_batch(
916 batch(Vec::new(), Some(tail)),
917 false,
918 )))
919 .unwrap();
920 drop(tx);
921 assert!(session.next().await.is_none());
922 assert_eq!(caught_up.await.unwrap(), tail);
923 }
924
925 #[tokio::test]
926 async fn clean_end_rejects_wait() {
927 let mut session = test_session(stream::empty());
928 let caught_up = session.caught_up();
929
930 assert!(session.next().await.is_none());
931 assert!(matches!(caught_up.await, Err(CaughtUpError::SessionClosed)));
932 }
933
934 #[tokio::test]
935 async fn read_error_rejects_wait() {
936 let mut session = test_session(stream::iter([Err(ReadSessionFailure::HeartbeatTimeout)]));
937 let caught_up = session.caught_up();
938
939 let error = session.next().await.unwrap().unwrap_err();
940 assert_eq!(error.to_string(), "heartbeat timeout");
941 assert!(matches!(
942 caught_up.await,
943 Err(CaughtUpError::Read(ReadSessionError::HeartbeatTimeout))
944 ));
945 }
946
947 #[tokio::test]
948 async fn read_error_after_caught_up_preserves_resolved_future() {
949 let tail = position(1);
950 let mut session = test_session(stream::iter([
951 Ok(ReadUpdate::from_batch(
952 batch(vec![record(0, false)], Some(tail)),
953 false,
954 )),
955 Err(ReadSessionFailure::HeartbeatTimeout),
956 ]));
957
958 session.next().await.unwrap().unwrap();
959 assert!(session.is_caught_up());
960 let caught_up = session.caught_up();
961
962 session.next().await.unwrap().unwrap_err();
963 assert!(!session.is_caught_up());
964 assert_eq!(caught_up.await.unwrap(), tail);
965 assert!(matches!(
966 session.caught_up().await,
967 Err(CaughtUpError::Read(ReadSessionError::HeartbeatTimeout))
968 ));
969 }
970
971 #[tokio::test]
972 async fn dropping_session_rejects_wait() {
973 let caught_up = {
974 let session = test_session(stream::pending());
975 session.caught_up()
976 };
977
978 assert!(matches!(caught_up.await, Err(CaughtUpError::SessionClosed)));
979 }
980}