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