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