Skip to main content

google_cloud_bigquery/read/
reader.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helpers to reconnect a [ReadRows] stream.
16
17use crate::builder::read::ReadRows;
18use crate::model::ReadRowsResponse;
19use crate::read::retry_policy::RetryableErrors;
20use crate::{Error, Result};
21use google_cloud_gax::backoff_policy::BackoffPolicy;
22use google_cloud_gax::exponential_backoff::{ExponentialBackoff, ExponentialBackoffBuilder};
23use google_cloud_gax::retry_policy::{RetryPolicy, RetryPolicyArg, RetryPolicyExt};
24use google_cloud_gax::retry_result::RetryResult;
25use google_cloud_gax::retry_state::RetryState;
26use google_cloud_gax::streaming::ResponseStream;
27use std::sync::Arc;
28use std::time::Duration;
29use tokio::time::Instant;
30
31const INITIAL_DELAY: Duration = Duration::from_millis(100);
32const MAXIMUM_DELAY: Duration = Duration::from_secs(60);
33const SCALING_FACTOR: f64 = 1.3;
34const MAX_ATTEMPTS: u32 = 10;
35
36/// Default retry policy for mid-stream `read_rows` reconnections.
37///
38/// - `max_times` (10): Limits total consecutive failed reconnection attempts when no data progress is made.
39fn default_retry_policy() -> Arc<dyn RetryPolicy> {
40    Arc::new(RetryableErrors.with_attempt_limit(MAX_ATTEMPTS))
41}
42
43/// Default backoff policy for mid-stream `read_rows` reconnections.
44///
45/// - `min_delay` (100ms): Starts with a short initial backoff to quickly recover from brief network blips.
46/// - `max_delay` (60s): Caps the backoff delay so exponential growth (100ms -> 130ms -> 169ms ...) does not produce excessively long single delays.
47/// - `factor` (1.3): Multiplier scaling factor for exponential backoff (matching Python BigQuery Storage client standard).
48fn default_backoff_policy() -> ExponentialBackoff {
49    ExponentialBackoffBuilder::new()
50        .with_initial_delay(INITIAL_DELAY)
51        .with_maximum_delay(MAXIMUM_DELAY)
52        .with_scaling(SCALING_FACTOR)
53        .build()
54        .expect("hardcoded value guaranteed to be valid")
55}
56
57/// State machine for reconnection logic. If reading fails at some point,
58/// attempt to reconnect with the current row offset.
59///
60/// All mutable runtime state for a [`Reader`] (such as the current row offset,
61/// active stream, and retry progress) must be stored in [`ReaderState`] rather
62/// than as separate fields on [`Reader`], keeping state transitions explicit
63/// and self-contained.
64///
65/// ```text
66///                Start here
67///                    │
68///              ┌─────▼──────┐      Retryable       ┌─────────┐
69///        ┌─────┼ Connecting ┼─────────────────────►│ Backoff │
70///        │     └─────┬──────┘◄─────────────────────┼         │
71///        │           │          Deadline elapsed   └────▲────┘
72///   Exhausted        │                                  │
73///    Retries         │                             Retryable
74///       or           │                               Error
75/// Unrecoverable      │                  (retains retry_state if Some, or
76///      Error   ┌─────▼──────┐            starts fresh RetryState if None)
77///        │     │  Reading   ┼───────────────────────────┘
78///        │     └─────┬──────┘ (clears retry_state
79///        │           │         on first message)
80///        │           ▼
81///        │      Unrecoverable
82///        │      Error or EOF
83///        │           │            ┌────────────┐
84///        │           └───────────►│            │
85///        └───────────────────────►│ Terminated │
86///                                 │            │
87///                                 └────────────┘
88/// ```
89#[derive(Debug)]
90pub(crate) enum ReaderState {
91    /// Connect or reconnect to the BigQuery read stream.
92    Connecting {
93        offset: i64,
94        retry_state: RetryState,
95    },
96    /// Waiting until `deadline` before reconnecting to the BigQuery read stream.
97    Backoff {
98        offset: i64,
99        retry_state: RetryState,
100        deadline: Instant,
101    },
102    /// Reading messages from an open BigQuery read stream.
103    ///
104    /// `retry_state` is `Some` while waiting for the first message after connecting
105    /// (so failures before any data progress retain the retry count from `Connecting`),
106    /// and is cleared to `None` once the first message is successfully received.
107    Reading {
108        offset: i64,
109        retry_state: Option<RetryState>,
110        stream: ResponseStream<ReadRowsResponse>,
111    },
112    /// Stream completed cleanly, fatal error occurred, or consumer dropped the receiver.
113    Terminated(Option<Error>),
114}
115
116/// A stream reader for [`ReadRows`] that automatically reconnects on transient errors,
117/// resuming from the current row offset.
118#[derive(Debug)]
119pub struct Reader {
120    retry_policy: Arc<dyn RetryPolicy>,
121    backoff_policy: ExponentialBackoff,
122    request: ReadRows,
123    /// Encapsulates all mutable state for the [`Reader`]. Any new mutable
124    /// runtime state should be added to [`ReaderState`] rather than [`Reader`].
125    state: ReaderState,
126}
127
128impl Reader {
129    pub(crate) fn new(request: ReadRows) -> Self {
130        let offset = request.0.request.offset;
131        Self {
132            retry_policy: default_retry_policy(),
133            backoff_policy: default_backoff_policy(),
134            request,
135            state: ReaderState::Connecting {
136                offset,
137                retry_state: RetryState::new(true),
138            },
139        }
140    }
141
142    /// Configure the retry policy for reconnecting the stream.
143    ///
144    /// # Example
145    /// ```
146    /// # use google_cloud_bigquery::client::Read;
147    /// # async fn sample(client: Read) -> anyhow::Result<()> {
148    /// use google_cloud_bigquery::read::retry_policy::RetryableErrors;
149    /// use google_cloud_gax::retry_policy::RetryPolicyExt;
150    /// let mut reader = client
151    ///     .read_rows()
152    ///     .set_read_stream("projects/my-project/locations/us/sessions/s1/streams/st1")
153    ///     .into_reader()
154    ///     .with_retry_policy(RetryableErrors.with_attempt_limit(5));
155    /// # Ok(())
156    /// # }
157    /// ```
158    pub fn with_retry_policy(mut self, v: impl Into<RetryPolicyArg>) -> Self {
159        self.retry_policy = v.into().into();
160        self
161    }
162
163    fn handle_error(&self, offset: i64, mut retry_state: RetryState, err: Error) -> ReaderState {
164        retry_state.attempt_count += 1;
165        let prev_err = match self.retry_policy.on_error(&retry_state, err) {
166            RetryResult::Permanent(e) | RetryResult::Exhausted(e) => {
167                return ReaderState::Terminated(Some(e));
168            }
169            RetryResult::Continue(e) => e,
170        };
171
172        let delay = self.backoff_policy.on_failure(&retry_state);
173        if self
174            .retry_policy
175            .remaining_time(&retry_state)
176            .is_some_and(|remaining| remaining <= delay)
177        {
178            return ReaderState::Terminated(Some(Error::exhausted(prev_err)));
179        }
180        ReaderState::Backoff {
181            offset,
182            retry_state,
183            deadline: Instant::now() + delay,
184        }
185    }
186
187    /// Advances the state machine by a single transition, returning a
188    /// [`ReadRowsResponse`] if one was read during this step.
189    async fn step(&mut self) -> Option<ReadRowsResponse> {
190        match &mut self.state {
191            ReaderState::Connecting {
192                offset,
193                retry_state,
194            } => {
195                let offset = *offset;
196                let retry_state = retry_state.clone();
197                let req = self.request.clone().set_offset(offset);
198                match req.send().await {
199                    Ok(stream) => {
200                        self.state = ReaderState::Reading {
201                            offset,
202                            retry_state: Some(retry_state),
203                            stream,
204                        };
205                    }
206                    Err(err) => {
207                        self.state = self.handle_error(offset, retry_state, err);
208                    }
209                }
210                None
211            }
212            ReaderState::Backoff {
213                offset,
214                retry_state,
215                deadline,
216            } => {
217                tokio::time::sleep_until(*deadline).await;
218                self.state = ReaderState::Connecting {
219                    offset: *offset,
220                    retry_state: retry_state.clone(),
221                };
222                None
223            }
224            ReaderState::Reading {
225                offset,
226                retry_state,
227                stream,
228            } => match stream.next().await {
229                Some(Ok(response)) => {
230                    *offset += response.row_count;
231                    // Data progress was made: clear `retry_state` so any future
232                    // mid-stream failure starts a fresh `RetryState::new(true)`.
233                    *retry_state = None;
234                    Some(response)
235                }
236                Some(Err(err)) => {
237                    let offset = *offset;
238                    let retry_state = retry_state.take().unwrap_or_else(|| RetryState::new(true));
239                    self.state = self.handle_error(offset, retry_state, err);
240                    None
241                }
242                None => {
243                    self.state = ReaderState::Terminated(None);
244                    None
245                }
246            },
247            ReaderState::Terminated(_) => None,
248        }
249    }
250
251    /// Receives the next [`ReadRowsResponse`] from the stream, reconnecting if a
252    /// retryable error occurs, or returns `None` when the stream completes.
253    pub async fn next(&mut self) -> Option<Result<ReadRowsResponse>> {
254        loop {
255            if let Some(response) = self.step().await {
256                return Some(Ok(response));
257            }
258            if let ReaderState::Terminated(err) = &mut self.state {
259                return err.take().map(Err);
260            }
261        }
262    }
263}
264
265impl ReadRows {
266    /// Returns a [`Reader`], which automatically reconnects and resumes reading
267    /// from the latest row offset if the stream is interrupted by a retryable error.
268    ///
269    /// # Example
270    /// ```
271    /// # use google_cloud_bigquery::builder::read::ReadRows;
272    /// # async fn sample(builder: ReadRows) -> google_cloud_bigquery::Result<()> {
273    /// let mut rows = builder.into_reader();
274    /// while let Some(response) = rows.next().await.transpose()? {
275    ///     println!("Read {} rows", response.row_count);
276    /// }
277    /// # Ok(())
278    /// # }
279    /// ```
280    pub fn into_reader(self) -> Reader {
281        Reader::new(self)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::client::Read;
289    use crate::model::ReadRowsRequest;
290    use google_cloud_gax::error::rpc::{Code, Status};
291    use google_cloud_gax::options::RequestOptions;
292    use std::error::Error as _;
293    use tokio::sync::mpsc;
294
295    mockall::mock! {
296        #[derive(Debug)]
297        ReadStub {}
298        impl crate::stub::Read for ReadStub {
299            async fn read_rows(
300                &self,
301                req: ReadRowsRequest,
302                options: RequestOptions,
303            ) -> Result<ResponseStream<ReadRowsResponse>>;
304        }
305    }
306
307    mockall::mock! {
308        #[derive(Debug)]
309        pub RetryPolicy {}
310        impl RetryPolicy for RetryPolicy {
311            fn on_error(&self, state: &RetryState, error: Error) -> RetryResult;
312            fn remaining_time(&self, state: &RetryState) -> Option<Duration>;
313        }
314    }
315
316    /// A `MockRetryPolicy` without an overall deadline.
317    fn mock_retry_policy() -> MockRetryPolicy {
318        let mut retry = MockRetryPolicy::new();
319        retry.expect_remaining_time().returning(|_| None);
320        retry
321    }
322
323    fn transient_error() -> Error {
324        Error::service(
325            Status::default()
326                .set_code(Code::Unavailable)
327                .set_message("transient failure"),
328        )
329    }
330
331    fn permanent_error() -> Error {
332        Error::service(
333            Status::default()
334                .set_code(Code::PermissionDenied)
335                .set_message("permission denied"),
336        )
337    }
338
339    #[test]
340    fn traits() {
341        static_assertions::assert_impl_all!(Reader: Send, std::fmt::Debug);
342    }
343
344    #[tokio::test]
345    async fn step_connecting_to_reading() -> anyhow::Result<()> {
346        let mut mock = MockReadStub::new();
347        mock.expect_read_rows().once().returning(|req, _| {
348            assert_eq!(req.read_stream, "streams/1");
349            assert_eq!(req.offset, 42);
350            let (_tx, rx) = mpsc::channel(1);
351            Ok(ResponseStream::from(rx))
352        });
353
354        let mut retry = mock_retry_policy();
355        retry.expect_on_error().never();
356
357        let client = Read::from_stub(mock);
358        let req = client.read_rows().set_read_stream("streams/1");
359        let mut reader = Reader::new(req).with_retry_policy(retry);
360
361        let initial_retry = RetryState::new(true).set_attempt_count(2_u32);
362        reader.state = ReaderState::Connecting {
363            offset: 42,
364            retry_state: initial_retry,
365        };
366        let resp = reader.step().await;
367        assert!(resp.is_none());
368        let ReaderState::Reading {
369            offset,
370            retry_state: Some(retry_state),
371            ..
372        } = &reader.state
373        else {
374            anyhow::bail!(
375                "expected Reading state with Some(retry_state), got: {:?}",
376                reader.state
377            );
378        };
379        assert_eq!(*offset, 42);
380        assert_eq!(retry_state.attempt_count, 2);
381        Ok(())
382    }
383
384    #[tokio::test(start_paused = true)]
385    async fn step_connecting_retryable_error() -> anyhow::Result<()> {
386        let mut mock = MockReadStub::new();
387        mock.expect_read_rows()
388            .once()
389            .returning(|_, _| Err(transient_error()));
390
391        let mut retry = mock_retry_policy();
392        retry
393            .expect_on_error()
394            .withf(|state, e| {
395                state.attempt_count == 2 && e.status().is_some_and(|s| s.code == Code::Unavailable)
396            })
397            .once()
398            .returning(|_, e| RetryResult::Continue(e));
399
400        let client = Read::from_stub(mock);
401        let req = client.read_rows().set_read_stream("streams/1");
402        let mut reader = Reader::new(req).with_retry_policy(retry);
403
404        let initial_retry = RetryState::new(true).set_attempt_count(1_u32);
405        reader.state = ReaderState::Connecting {
406            offset: 10,
407            retry_state: initial_retry,
408        };
409        let now = Instant::now();
410        let resp = reader.step().await;
411        assert!(resp.is_none());
412        let ReaderState::Backoff {
413            offset,
414            retry_state,
415            deadline,
416        } = &reader.state
417        else {
418            anyhow::bail!("expected Backoff state, got: {:?}", reader.state);
419        };
420        assert_eq!(*offset, 10);
421        assert_eq!(retry_state.attempt_count, 2);
422        assert!(*deadline >= now);
423        assert!(*deadline <= now + INITIAL_DELAY.mul_f64(SCALING_FACTOR));
424        Ok(())
425    }
426
427    #[tokio::test]
428    async fn step_connecting_permanent_error() -> anyhow::Result<()> {
429        let mut mock = MockReadStub::new();
430        mock.expect_read_rows()
431            .once()
432            .returning(|_, _| Err(permanent_error()));
433
434        let mut retry = mock_retry_policy();
435        retry
436            .expect_on_error()
437            .withf(|state, e| {
438                state.attempt_count == 1
439                    && e.status().is_some_and(|s| s.code == Code::PermissionDenied)
440            })
441            .once()
442            .returning(|_, e| RetryResult::Permanent(e));
443
444        let client = Read::from_stub(mock);
445        let req = client.read_rows().set_read_stream("streams/1");
446        let mut reader = Reader::new(req).with_retry_policy(retry);
447
448        reader.state = ReaderState::Connecting {
449            offset: 0,
450            retry_state: RetryState::new(true),
451        };
452        let resp = reader.step().await;
453        assert!(resp.is_none());
454        let ReaderState::Terminated(Some(err)) = &reader.state else {
455            anyhow::bail!("expected Terminated(Some(_)), got: {:?}", reader.state);
456        };
457        assert_eq!(err.status().map(|s| s.code), Some(Code::PermissionDenied));
458        Ok(())
459    }
460
461    #[tokio::test]
462    async fn step_connecting_deadline_exhausted() -> anyhow::Result<()> {
463        let mut mock = MockReadStub::new();
464        mock.expect_read_rows()
465            .once()
466            .returning(|_, _| Err(transient_error()));
467
468        let mut retry = MockRetryPolicy::new();
469        retry
470            .expect_remaining_time()
471            .returning(|_| Some(Duration::ZERO));
472        retry
473            .expect_on_error()
474            .once()
475            .returning(|_, e| RetryResult::Continue(e));
476
477        let client = Read::from_stub(mock);
478        let req = client.read_rows().set_read_stream("streams/1");
479        let mut reader = Reader::new(req).with_retry_policy(retry);
480
481        reader.state = ReaderState::Connecting {
482            offset: 0,
483            retry_state: RetryState::new(true),
484        };
485        let resp = reader.step().await;
486        assert!(resp.is_none());
487        let ReaderState::Terminated(Some(err)) = &reader.state else {
488            anyhow::bail!("expected Terminated(Some(_)), got: {:?}", reader.state);
489        };
490        assert!(err.is_exhausted(), "{err:?}");
491        let last_error = err.source().expect("the error should have a source");
492        assert!(
493            last_error.to_string().contains("transient failure"),
494            "{last_error:?}"
495        );
496        Ok(())
497    }
498
499    #[tokio::test(start_paused = true)]
500    async fn step_backoff_to_connecting() -> anyhow::Result<()> {
501        let mut mock = MockReadStub::new();
502        mock.expect_read_rows().never();
503        let mut retry = mock_retry_policy();
504        retry.expect_on_error().never();
505
506        let client = Read::from_stub(mock);
507        let req = client.read_rows().set_read_stream("streams/1");
508        let mut reader = Reader::new(req).with_retry_policy(retry);
509
510        let deadline = Instant::now() + Duration::from_millis(500);
511        reader.state = ReaderState::Backoff {
512            offset: 42,
513            retry_state: RetryState::new(true).set_attempt_count(2_u32),
514            deadline,
515        };
516        let resp = reader.step().await;
517        assert!(resp.is_none());
518        assert_eq!(Instant::now(), deadline);
519        let ReaderState::Connecting {
520            offset,
521            retry_state,
522        } = &reader.state
523        else {
524            anyhow::bail!("expected Connecting state, got: {:?}", reader.state);
525        };
526        assert_eq!(*offset, 42);
527        assert_eq!(retry_state.attempt_count, 2);
528        Ok(())
529    }
530
531    #[tokio::test]
532    async fn step_first_message_clears_retry_state() -> anyhow::Result<()> {
533        let mut mock = MockReadStub::new();
534        mock.expect_read_rows().never();
535        let mut retry = mock_retry_policy();
536        retry.expect_on_error().never();
537
538        let client = Read::from_stub(mock);
539        let req = client.read_rows().set_read_stream("streams/1");
540        let mut reader = Reader::new(req).with_retry_policy(retry);
541
542        let (tx, rx) = mpsc::channel(1);
543        tx.send(Ok(ReadRowsResponse::new().set_row_count(5)))
544            .await?;
545        let stream = ResponseStream::from(rx);
546
547        reader.state = ReaderState::Reading {
548            offset: 10,
549            retry_state: Some(RetryState::new(true).set_attempt_count(3_u32)),
550            stream,
551        };
552        let resp = reader.step().await.expect("expected Some(response)");
553        let ReaderState::Reading {
554            offset,
555            retry_state,
556            ..
557        } = &reader.state
558        else {
559            anyhow::bail!("expected Reading state, got: {:?}", reader.state);
560        };
561        assert_eq!(resp.row_count, 5);
562        assert_eq!(*offset, 15);
563        assert!(
564            retry_state.is_none(),
565            "expected retry_state to be cleared to None after first message, got: {retry_state:?}"
566        );
567        Ok(())
568    }
569
570    #[tokio::test(start_paused = true)]
571    async fn step_first_message_retryable_error_retains_retry_state() -> anyhow::Result<()> {
572        let mut mock = MockReadStub::new();
573        mock.expect_read_rows().never();
574
575        let mut retry = mock_retry_policy();
576        retry
577            .expect_on_error()
578            .withf(|state, e| {
579                state.attempt_count == 3 && e.status().is_some_and(|s| s.code == Code::Unavailable)
580            })
581            .once()
582            .returning(|_, e| RetryResult::Continue(e));
583
584        let client = Read::from_stub(mock);
585        let req = client.read_rows().set_read_stream("streams/1");
586        let mut reader = Reader::new(req).with_retry_policy(retry);
587
588        let (tx, rx) = mpsc::channel(1);
589        tx.send(Err(transient_error())).await?;
590        let stream = ResponseStream::from(rx);
591
592        // Waiting for the first message starts with attempt_count = 2 from prior Connecting attempts.
593        let prior_retry = RetryState::new(true).set_attempt_count(2_u32);
594        reader.state = ReaderState::Reading {
595            offset: 7,
596            retry_state: Some(prior_retry),
597            stream,
598        };
599        let now = Instant::now();
600        let resp = reader.step().await;
601        assert!(resp.is_none());
602        let ReaderState::Backoff {
603            offset,
604            retry_state,
605            deadline,
606        } = &reader.state
607        else {
608            anyhow::bail!("expected Backoff state, got: {:?}", reader.state);
609        };
610        assert_eq!(*offset, 7);
611        assert_eq!(retry_state.attempt_count, 3);
612        assert!(*deadline >= now);
613        assert!(*deadline <= now + INITIAL_DELAY.mul_f64(SCALING_FACTOR.powi(2)));
614        Ok(())
615    }
616
617    #[tokio::test]
618    async fn step_reading_to_reading() -> anyhow::Result<()> {
619        let mut mock = MockReadStub::new();
620        mock.expect_read_rows().never();
621        let mut retry = mock_retry_policy();
622        retry.expect_on_error().never();
623
624        let client = Read::from_stub(mock);
625        let req = client.read_rows().set_read_stream("streams/1");
626        let mut reader = Reader::new(req).with_retry_policy(retry);
627
628        let (tx, rx) = mpsc::channel(1);
629        tx.send(Ok(ReadRowsResponse::new().set_row_count(3)))
630            .await?;
631        let stream = ResponseStream::from(rx);
632
633        reader.state = ReaderState::Reading {
634            offset: 5,
635            retry_state: None,
636            stream,
637        };
638        let resp = reader.step().await.expect("expected Some(response)");
639        let ReaderState::Reading {
640            offset,
641            retry_state,
642            ..
643        } = &reader.state
644        else {
645            anyhow::bail!("expected Reading state, got: {:?}", reader.state);
646        };
647        assert_eq!(resp.row_count, 3);
648        assert_eq!(*offset, 8);
649        assert!(retry_state.is_none());
650        Ok(())
651    }
652
653    #[tokio::test(start_paused = true)]
654    async fn step_reading_retryable_error_resets_retry_state() -> anyhow::Result<()> {
655        let mut mock = MockReadStub::new();
656        mock.expect_read_rows().never();
657
658        let mut retry = mock_retry_policy();
659        retry
660            .expect_on_error()
661            .withf(|state, e| {
662                // Once retry_state is None, the first failure from Reading has attempt_count == 1.
663                state.attempt_count == 1 && e.status().is_some_and(|s| s.code == Code::Unavailable)
664            })
665            .once()
666            .returning(|_, e| RetryResult::Continue(e));
667
668        let client = Read::from_stub(mock);
669        let req = client.read_rows().set_read_stream("streams/1");
670        let mut reader = Reader::new(req).with_retry_policy(retry);
671
672        let (tx, rx) = mpsc::channel(1);
673        tx.send(Err(transient_error())).await?;
674        let stream = ResponseStream::from(rx);
675
676        reader.state = ReaderState::Reading {
677            offset: 12,
678            retry_state: None,
679            stream,
680        };
681        let now = Instant::now();
682        let resp = reader.step().await;
683        assert!(resp.is_none());
684        let ReaderState::Backoff {
685            offset,
686            retry_state,
687            deadline,
688        } = &reader.state
689        else {
690            anyhow::bail!("expected Backoff state, got: {:?}", reader.state);
691        };
692        assert_eq!(*offset, 12);
693        assert_eq!(retry_state.attempt_count, 1);
694        assert!(*deadline >= now);
695        assert!(*deadline <= now + INITIAL_DELAY);
696        Ok(())
697    }
698
699    #[tokio::test]
700    async fn step_reading_permanent_error_to_terminated() -> anyhow::Result<()> {
701        let mut mock = MockReadStub::new();
702        mock.expect_read_rows().never();
703
704        let mut retry = mock_retry_policy();
705        retry
706            .expect_on_error()
707            .withf(|state, e| {
708                state.attempt_count == 1
709                    && e.status().is_some_and(|s| s.code == Code::PermissionDenied)
710            })
711            .once()
712            .returning(|_, e| RetryResult::Permanent(e));
713
714        let client = Read::from_stub(mock);
715        let req = client.read_rows().set_read_stream("streams/1");
716        let mut reader = Reader::new(req).with_retry_policy(retry);
717
718        let (tx, rx) = mpsc::channel(1);
719        tx.send(Err(permanent_error())).await?;
720        let stream = ResponseStream::from(rx);
721
722        reader.state = ReaderState::Reading {
723            offset: 0,
724            retry_state: None,
725            stream,
726        };
727        let resp = reader.step().await;
728        assert!(resp.is_none());
729        let ReaderState::Terminated(Some(err)) = &reader.state else {
730            anyhow::bail!("expected Terminated(Some(_)), got: {:?}", reader.state);
731        };
732        assert_eq!(err.status().map(|s| s.code), Some(Code::PermissionDenied));
733        Ok(())
734    }
735
736    #[tokio::test]
737    async fn step_reading_eof_to_terminated() -> anyhow::Result<()> {
738        let mut mock = MockReadStub::new();
739        mock.expect_read_rows().never();
740
741        let client = Read::from_stub(mock);
742        let req = client.read_rows().set_read_stream("streams/1");
743        let mut reader = Reader::new(req);
744
745        let (tx, rx) = mpsc::channel(1);
746        drop(tx);
747        let stream = ResponseStream::from(rx);
748
749        reader.state = ReaderState::Reading {
750            offset: 0,
751            retry_state: None,
752            stream,
753        };
754        let resp = reader.step().await;
755        assert!(resp.is_none());
756        assert!(
757            matches!(reader.state, ReaderState::Terminated(None)),
758            "expected Terminated(None), got: {:?}",
759            reader.state
760        );
761        Ok(())
762    }
763
764    #[tokio::test]
765    async fn step_terminated_stays_terminated() -> anyhow::Result<()> {
766        let mut mock = MockReadStub::new();
767        mock.expect_read_rows().never();
768
769        let client = Read::from_stub(mock);
770        let req = client.read_rows().set_read_stream("streams/1");
771        let mut reader = Reader::new(req);
772
773        reader.state = ReaderState::Terminated(None);
774        assert!(reader.step().await.is_none());
775        assert!(
776            matches!(reader.state, ReaderState::Terminated(None)),
777            "expected Terminated(None), got: {:?}",
778            reader.state
779        );
780
781        reader.state = ReaderState::Terminated(Some(permanent_error()));
782        assert!(reader.step().await.is_none());
783        let ReaderState::Terminated(Some(err)) = &reader.state else {
784            anyhow::bail!("expected Terminated(Some(_)), got: {:?}", reader.state);
785        };
786        assert_eq!(err.status().map(|s| s.code), Some(Code::PermissionDenied));
787        Ok(())
788    }
789
790    #[tokio::test]
791    async fn read_rows_success() -> anyhow::Result<()> {
792        let mut mock = MockReadStub::new();
793        mock.expect_read_rows().once().returning(|req, _| {
794            assert_eq!(req.read_stream, "streams/1");
795            assert_eq!(req.offset, 0);
796            let (tx, rx) = mpsc::channel(4);
797            tokio::spawn(async move {
798                let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(5))).await;
799                let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(3))).await;
800            });
801            Ok(ResponseStream::from(rx))
802        });
803
804        let client = Read::from_stub(mock);
805        let req = client.read_rows().set_read_stream("streams/1");
806        let mut reader = Reader::new(req);
807
808        let r1 = reader.next().await.transpose()?.expect("row batch 1");
809        assert_eq!(r1.row_count, 5);
810        let r2 = reader.next().await.transpose()?.expect("row batch 2");
811        assert_eq!(r2.row_count, 3);
812        assert!(reader.next().await.is_none());
813        assert!(reader.next().await.is_none());
814        Ok(())
815    }
816
817    #[tokio::test(start_paused = true)]
818    async fn reconnect_mid_stream_resumes_at_offset() -> anyhow::Result<()> {
819        let mut seq = mockall::Sequence::new();
820        let mut mock = MockReadStub::new();
821
822        // First stream yields 5 rows, then 3 rows, then fails with Unavailable.
823        mock.expect_read_rows()
824            .once()
825            .in_sequence(&mut seq)
826            .returning(|req, _| {
827                assert_eq!(req.offset, 0);
828                let (tx, rx) = mpsc::channel(4);
829                tokio::spawn(async move {
830                    let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(5))).await;
831                    let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(3))).await;
832                    let _ = tx.send(Err(transient_error())).await;
833                });
834                Ok(ResponseStream::from(rx))
835            });
836
837        // Second stream should reconnect at offset 8 (5 + 3).
838        mock.expect_read_rows()
839            .once()
840            .in_sequence(&mut seq)
841            .returning(|req, _| {
842                assert_eq!(req.offset, 8);
843                let (tx, rx) = mpsc::channel(4);
844                tokio::spawn(async move {
845                    let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(2))).await;
846                });
847                Ok(ResponseStream::from(rx))
848            });
849
850        let client = Read::from_stub(mock);
851        let req = client.read_rows().set_read_stream("streams/1");
852        let mut reader = Reader::new(req);
853
854        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 5);
855        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 3);
856        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 2);
857        assert!(reader.next().await.is_none());
858        Ok(())
859    }
860
861    #[tokio::test(start_paused = true)]
862    async fn with_offset_starts_and_reconnects_from_initial_offset() -> anyhow::Result<()> {
863        let mut seq = mockall::Sequence::new();
864        let mut mock = MockReadStub::new();
865
866        mock.expect_read_rows()
867            .once()
868            .in_sequence(&mut seq)
869            .returning(|req, _| {
870                assert_eq!(req.offset, 1_000);
871                let (tx, rx) = mpsc::channel(4);
872                tokio::spawn(async move {
873                    let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(25))).await;
874                    let _ = tx
875                        .send(Err(Error::service(
876                            Status::default()
877                                .set_code(Code::ResourceExhausted)
878                                .set_message("rate limited"),
879                        )))
880                        .await;
881                });
882                Ok(ResponseStream::from(rx))
883            });
884
885        mock.expect_read_rows()
886            .once()
887            .in_sequence(&mut seq)
888            .returning(|req, _| {
889                assert_eq!(req.offset, 1_025);
890                let (tx, rx) = mpsc::channel(4);
891                tokio::spawn(async move {
892                    let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(10))).await;
893                });
894                Ok(ResponseStream::from(rx))
895            });
896
897        let client = Read::from_stub(mock);
898        let mut reader = client
899            .read_rows()
900            .set_read_stream("streams/1")
901            .set_offset(1_000)
902            .into_reader();
903
904        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 25);
905        if let ReaderState::Reading { offset, .. } = &reader.state {
906            assert_eq!(*offset, 1025);
907        } else {
908            panic!("expected reader state to be Reading");
909        }
910        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 10);
911        if let ReaderState::Reading { offset, .. } = &reader.state {
912            assert_eq!(*offset, 1035);
913        } else {
914            panic!("expected reader state to be Reading");
915        }
916        assert!(reader.next().await.is_none());
917        Ok(())
918    }
919
920    #[tokio::test]
921    async fn next_is_cancel_safe_while_waiting_for_stream() -> anyhow::Result<()> {
922        let mut mock = MockReadStub::new();
923        let (tx, rx) = mpsc::channel(4);
924
925        mock.expect_read_rows()
926            .once()
927            .return_once(move |_, _| Ok(ResponseStream::from(rx)));
928
929        let client = Read::from_stub(mock);
930        let mut reader = client
931            .read_rows()
932            .set_read_stream("streams/1")
933            .into_reader();
934
935        tx.send(Ok(ReadRowsResponse::new().set_row_count(4)))
936            .await?;
937        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 4);
938
939        // Cancel `reader.next()` via timeout while the stream has no message ready yet.
940        let timed_out = tokio::time::timeout(Duration::from_millis(10), reader.next()).await;
941        assert!(timed_out.is_err(), "expected timeout while stream is idle");
942
943        // Now send the next batch on the same stream; the reader must not have dropped
944        // the stream or transitioned to Terminated(None).
945        tx.send(Ok(ReadRowsResponse::new().set_row_count(6)))
946            .await?;
947        drop(tx);
948
949        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 6);
950        assert!(reader.next().await.is_none());
951        Ok(())
952    }
953
954    #[tokio::test(start_paused = true)]
955    async fn next_is_cancel_safe_during_backoff() -> anyhow::Result<()> {
956        let mut mock = MockReadStub::new();
957
958        // Stream should only be opened once the full backoff deadline elapses.
959        mock.expect_read_rows().once().returning(|req, _| {
960            assert_eq!(req.offset, 4);
961            let (tx, rx) = mpsc::channel(4);
962            tokio::spawn(async move {
963                let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(6))).await;
964            });
965            Ok(ResponseStream::from(rx))
966        });
967
968        let client = Read::from_stub(mock);
969        let mut reader = client
970            .read_rows()
971            .set_read_stream("streams/1")
972            .into_reader();
973
974        let start = Instant::now();
975        let backoff_delay = Duration::from_secs(10);
976        reader.state = ReaderState::Backoff {
977            offset: 4,
978            retry_state: RetryState::new(true).set_attempt_count(1_u32),
979            deadline: start + backoff_delay,
980        };
981
982        // Cancel `reader.next()` after 3s of the 10s backoff.
983        let timed_out = tokio::time::timeout(Duration::from_secs(3), reader.next()).await;
984        assert!(timed_out.is_err(), "expected timeout during backoff");
985        assert!(
986            matches!(reader.state, ReaderState::Backoff { .. }),
987            "expected Reader to remain in Backoff state after cancellation, got: {:?}",
988            reader.state
989        );
990
991        // Cancel again after another 3s (6s elapsed total, 4s remaining); must not reconnect early.
992        let timed_out = tokio::time::timeout(Duration::from_secs(3), reader.next()).await;
993        assert!(
994            timed_out.is_err(),
995            "expected second timeout during remaining backoff"
996        );
997        assert!(
998            matches!(reader.state, ReaderState::Backoff { .. }),
999            "expected Reader to remain in Backoff state after second cancellation, got: {:?}",
1000            reader.state
1001        );
1002
1003        // Resume `reader.next()`; it should finish the remaining 4s of backoff and reconnect.
1004        let r2 = reader.next().await.transpose()?.expect("row batch");
1005        assert_eq!(r2.row_count, 6);
1006        assert_eq!(Instant::now().duration_since(start), backoff_delay);
1007        assert!(reader.next().await.is_none());
1008        Ok(())
1009    }
1010
1011    #[tokio::test]
1012    async fn permanent_error_terminates_immediately() -> anyhow::Result<()> {
1013        let mut mock = MockReadStub::new();
1014        mock.expect_read_rows().once().returning(|_, _| {
1015            let (tx, rx) = mpsc::channel(2);
1016            tokio::spawn(async move {
1017                let _ = tx.send(Ok(ReadRowsResponse::new().set_row_count(5))).await;
1018                let _ = tx.send(Err(permanent_error())).await;
1019            });
1020            Ok(ResponseStream::from(rx))
1021        });
1022
1023        let client = Read::from_stub(mock);
1024        let req = client.read_rows().set_read_stream("streams/1");
1025        let mut reader = Reader::new(req);
1026
1027        assert_eq!(reader.next().await.transpose()?.unwrap().row_count, 5);
1028        let err = reader
1029            .next()
1030            .await
1031            .expect("should return error")
1032            .expect_err("should be permanent error");
1033        assert_eq!(err.status().map(|s| s.code), Some(Code::PermissionDenied));
1034        assert!(reader.next().await.is_none());
1035        Ok(())
1036    }
1037}