Skip to main content

foundation_core/io/readers/
mod.rs

1//! TCP-resilient batch readers that use `read()` instead of `read_exact()`.
2//!
3//! # WHY
4//!
5//! `read_exact()` converts `WouldBlock` and `TimedOut` errors into `UnexpectedEof`,
6//! making it impossible for callers to distinguish "no data available yet" from
7//! "connection closed." On TCP streams this causes spurious failures on slow or
8//! congested connections.
9//!
10//! # WHAT
11//!
12//! Provides:
13//! - [`Data`] — enum distinguishing real bytes from retry signals
14//! - [`BatchReader`] — iterator over read batches with retry handling
15//! - [`FullBodyReader`] — iterator for known-size bodies, exposes `Data::Retry`
16//! - [`EofReader`] — iterator for reading until EOF, exposes `Data::Retry`
17//! - [`BatchStreamReader`] — adapter from `BatchReader` that yields `Data` (no loop)
18//! - [`LimitedBatchStreamReader`] — like `BatchStreamReader` but with byte cap
19//! - [`EOFStreamReader`] — reads until EOF via iterator, exposes `Data::Retry`
20//! - [`LimitedEOFStreamReader`] — reads until EOF with max size enforcement
21//! - [`DataBytesIterator`] — backward compat: absorbs retries, yields `Vec<u8>`
22//!
23//! # HOW
24//!
25//! Uses `read()` and propagates `WouldBlock`/`TimedOut` directly (as `Data::Retry`),
26//! following the pattern established in `wire::websocket::frame::decode`.
27
28use std::io::{self, Read};
29
30/// WHY: Callers need to distinguish between actual data and transient
31/// "no data yet" signals when reading from TCP streams.
32///
33/// WHAT: Result of a single read batch — either real bytes or a retry signal.
34///
35/// HOW: Wraps the two possible outcomes of a non-blocking `read()` call
36/// into a type-safe enum that the caller can pattern-match on.
37#[derive(Debug, Clone)]
38pub enum Data {
39    /// A batch of bytes successfully read from the source.
40    Bytes(Vec<u8>),
41    /// Yielded on `WouldBlock`, `TimedOut`, or `Ok(0)`-when-not-EOF.
42    /// Signals the caller should retry (poll again later).
43    Retry,
44}
45
46/// WHY: `read()`-based readers with a full inner `Read` need a way for
47/// outer limited readers to tell them exactly how many bytes remain,
48/// so they don't overshoot by reading a full buffer.
49///
50/// WHAT: Trait for hint-aware reading. `next_bytes(Some(n))` reads up to
51/// `n` bytes; `next_bytes(None)` uses the reader's default batch size.
52///
53/// HOW: Implementations pass the hint down to their inner reader or
54/// use it to limit the buffer allocation for the next `read()` call.
55pub trait HintReadterator {
56    type Item;
57    type Error;
58
59    fn next_bytes(&mut self, size_hint: Option<usize>) -> Option<Result<Self::Item, Self::Error>>;
60}
61
62/// WHY: `read_exact()` converts `WouldBlock`/`TimedOut` into `UnexpectedEof`,
63/// hiding transient failures on TCP streams. An iterator-based reader using
64/// `read()` preserves these error kinds so callers can decide when to retry.
65///
66/// WHAT: Iterator over read batches from a `Read` source with TCP-resilient
67/// retry handling, yielding `Result<Data, io::Error>`.
68///
69/// HOW: Each call to `next()` performs a single `read()` into a buffer of
70/// `batch_size` bytes. `WouldBlock`/`TimedOut` errors and (optionally)
71/// zero-length reads are converted to `Data::Retry`. A consecutive retry
72/// counter prevents infinite spinning.
73pub struct BatchReader<R: Read> {
74    reader: R,
75    batch_size: usize,
76    eof_on_zero_read: bool,
77    received_data: bool,
78    max_consecutive_retries: usize,
79    consecutive_retries: usize,
80    done: bool,
81}
82
83impl<R: Read> BatchReader<R> {
84    /// Create a new `BatchReader` with default configuration.
85    ///
86    /// Defaults: `batch_size=512`, `eof_on_zero_read=true`, `max_consecutive_retries=100`.
87    ///
88    /// # Panics
89    /// Never panics.
90    pub fn new(reader: R) -> Self {
91        Self {
92            reader,
93            batch_size: 512,
94            received_data: false,
95            eof_on_zero_read: true,
96            max_consecutive_retries: 100,
97            consecutive_retries: 0,
98            done: false,
99        }
100    }
101
102    /// Set the read buffer size per batch.
103    #[must_use]
104    pub fn batch_size(mut self, size: usize) -> Self {
105        self.batch_size = size;
106        self
107    }
108
109    /// If `true`, `read()` returning 0 means EOF (iterator ends).
110    /// If `false`, 0 is retryable (yields `Data::Retry`).
111    #[must_use]
112    pub fn eof_on_zero_read(mut self, eof: bool) -> Self {
113        self.eof_on_zero_read = eof;
114        self
115    }
116
117    /// Maximum retries without progress before erroring.
118    #[must_use]
119    pub fn max_consecutive_retries(mut self, max: usize) -> Self {
120        self.max_consecutive_retries = max;
121        self
122    }
123}
124
125impl<R: Read> Iterator for BatchReader<R> {
126    type Item = Result<Data, io::Error>;
127
128    fn next(&mut self) -> Option<Self::Item> {
129        let _span = tracing::span!(tracing::Level::TRACE, "next").entered();
130        self.next_bytes(None)
131    }
132}
133
134impl<R: Read> HintReadterator for BatchReader<R> {
135    type Item = Data;
136    type Error = io::Error;
137
138    fn next_bytes(&mut self, size_hint: Option<usize>) -> Option<Result<Self::Item, Self::Error>> {
139        let _span =
140            tracing::span!(tracing::Level::TRACE, "next_bytes", hint = ?size_hint).entered();
141        if self.done {
142            tracing::trace!("Reading is now considered done!");
143            return None;
144        }
145
146        // return zero once int indicates no more to read.
147        if size_hint.is_some() && size_hint == Some(0) {
148            return None;
149        }
150
151        let target_size = size_hint.unwrap_or(self.batch_size);
152        tracing::trace!("Using target size for buffer: {}", &target_size);
153        let mut buf = vec![0u8; target_size];
154        match self.reader.read(&mut buf) {
155            Ok(0) => {
156                tracing::trace!("Zero bytes read occured");
157                if self.eof_on_zero_read {
158                    tracing::trace!("Stream is now considered finished");
159                    self.done = true;
160                    None
161                } else {
162                    self.consecutive_retries += 1;
163                    if self.consecutive_retries > self.max_consecutive_retries {
164                        self.done = true;
165
166                        if self.received_data {
167                            return None;
168                        }
169
170                        Some(Err(io::Error::new(
171                            io::ErrorKind::TimedOut,
172                            "max consecutive retries exceeded without progress",
173                        )))
174                    } else {
175                        tracing::debug!("Sending retry");
176                        Some(Ok(Data::Retry))
177                    }
178                }
179            }
180            Ok(n) => {
181                tracing::trace!("Received data Bytes(len={})", n);
182                self.received_data = true;
183                self.consecutive_retries = 0;
184                buf.truncate(n);
185                // NOTE: never trace `buf` contents here — this is the raw body
186                // stream (e.g. multi-GB model downloads), so dumping it explodes
187                // the logs and leaks payload bytes. The length above is enough.
188                Some(Ok(Data::Bytes(buf)))
189            }
190            Err(e)
191                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
192            {
193                tracing::error!("Timeout/WouldBlock error received: {:?}", &e);
194                self.consecutive_retries += 1;
195                tracing::trace!(
196                    "Checking consecutive errors (max={}): current={}",
197                    &self.max_consecutive_retries,
198                    self.consecutive_retries
199                );
200                if self.consecutive_retries > self.max_consecutive_retries {
201                    self.done = true;
202
203                    if self.received_data {
204                        tracing::trace!("Finished reading, saw data, ending");
205                        return None;
206                    }
207
208                    tracing::trace!(
209                        "Problematic, max retries reached, returning exahaustion errors"
210                    );
211                    Some(Err(io::Error::new(
212                        e.kind(),
213                        "max consecutive retries exceeded without progress",
214                    )))
215                } else {
216                    Some(Ok(Data::Retry))
217                }
218            }
219            Err(e) => {
220                tracing::error!("Read error occured: {:?}", &e);
221                self.done = true;
222                Some(Err(e))
223            }
224        }
225    }
226}
227
228// ---------------------------------------------------------------------------
229// FullBodyReader — iterator for known-size bodies
230// ---------------------------------------------------------------------------
231
232/// WHY: Stream known-size bodies, exposing `Data::Retry` for caller control.
233///
234/// WHAT: Iterator yielding `Result<Data, io::Error>` until `target_size` bytes read.
235///
236/// HOW: Wraps `BatchReader`, counts bytes, returns `Data::Bytes` or passes through `Data::Retry`.
237pub struct FullBodyReader<R: Read> {
238    inner: BatchReader<R>,
239    bytes_yielded: usize,
240    target_size: usize,
241}
242
243impl<R: Read> FullBodyReader<R> {
244    /// Create a new `FullBodyReader` that reads exactly `target_size` bytes.
245    ///
246    /// # Panics
247    /// Never panics.
248    pub fn new(inner: BatchReader<R>, target_size: usize) -> Self {
249        Self {
250            inner,
251            bytes_yielded: 0,
252            target_size,
253        }
254    }
255}
256
257impl<R: Read> Iterator for FullBodyReader<R> {
258    type Item = Result<Data, io::Error>;
259
260    fn next(&mut self) -> Option<Self::Item> {
261        if self.bytes_yielded >= self.target_size {
262            return None;
263        }
264
265        match self.inner.next() {
266            Some(Ok(Data::Bytes(bytes))) => {
267                self.bytes_yielded += bytes.len();
268                Some(Ok(Data::Bytes(bytes)))
269            }
270            other => other, // Pass through Data::Retry and errors
271        }
272    }
273}
274
275// ---------------------------------------------------------------------------
276// EofReader — iterator for reading until EOF
277// ---------------------------------------------------------------------------
278
279/// WHY: Stream until EOF, exposing `Data::Retry` for caller control.
280///
281/// WHAT: Iterator yielding `Result<Data, io::Error>` until EOF.
282///
283/// HOW: Wraps `BatchReader` with `eof_on_zero_read=true`, passes through all `Data`.
284pub struct EofReader<R: Read> {
285    inner: BatchReader<R>,
286}
287
288impl<R: Read> EofReader<R> {
289    /// Create a new `EofReader` that reads until EOF.
290    ///
291    /// # Panics
292    /// Never panics.
293    pub fn new(inner: BatchReader<R>) -> Self {
294        Self { inner }
295    }
296}
297
298impl<R: Read> Iterator for EofReader<R> {
299    type Item = Result<Data, io::Error>;
300
301    fn next(&mut self) -> Option<Self::Item> {
302        self.inner.next()
303    }
304}
305
306// ---------------------------------------------------------------------------
307// BatchStreamReader — adapter from BatchReader, exposes Data (no loop)
308// ---------------------------------------------------------------------------
309
310/// WHY: Adapter from `BatchReader` that can be boxed for `SendSafeBody`.
311///
312/// WHAT: Iterator yielding `Result<Data, BoxedError>` — exposes `Data::Retry` to caller.
313///
314/// HOW: No internal loop — caller decides how to handle `Data::Retry`.
315pub struct BatchStreamReader<R: Read> {
316    inner: BatchReader<R>,
317}
318
319impl<R: Read> BatchStreamReader<R> {
320    /// Wrap a [`BatchReader`] to produce a stream-compatible iterator.
321    ///
322    /// # Panics
323    /// Never panics.
324    pub fn new(inner: BatchReader<R>) -> Self {
325        Self { inner }
326    }
327}
328
329impl<R: Read + Send> Iterator for BatchStreamReader<R> {
330    type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
331
332    fn next(&mut self) -> Option<Self::Item> {
333        match self.inner.next() {
334            Some(Ok(data)) => Some(Ok(data)),
335            Some(Err(e)) => Some(Err(Box::new(e))),
336            None => None,
337        }
338    }
339}
340
341// ---------------------------------------------------------------------------
342// LimitedBatchStreamReader — byte-capped BatchStreamReader
343// ---------------------------------------------------------------------------
344
345/// WHY: Stream limited-size bodies with byte cap, exposing `Data::Retry`.
346///
347/// WHAT: Iterator yielding `Result<Data, BoxedError>`, stops at cap.
348///
349/// HOW: Wraps `BatchReader`, counts bytes, returns `None` when cap reached.
350pub struct LimitedBatchStreamReader<R: Read> {
351    inner: BatchReader<R>,
352    bytes_yielded: usize,
353    byte_cap: usize,
354}
355
356impl<R: Read> LimitedBatchStreamReader<R> {
357    /// Create a new `LimitedBatchStreamReader` capped at `byte_cap` bytes.
358    ///
359    /// # Panics
360    /// Never panics.
361    pub fn new(inner: BatchReader<R>, byte_cap: usize) -> Self {
362        Self {
363            inner,
364            bytes_yielded: 0,
365            byte_cap,
366        }
367    }
368}
369
370impl<R: Read + Send> Iterator for LimitedBatchStreamReader<R> {
371    type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
372
373    fn next(&mut self) -> Option<Self::Item> {
374        tracing::trace!("Pulling next data bytes from stream");
375        if self.bytes_yielded >= self.byte_cap {
376            tracing::trace!("Readed max allowed bytes, stopping");
377            return None;
378        }
379
380        let remaining = self.byte_cap - self.bytes_yielded;
381
382        tracing::trace!("Remaining bytes to read: {}", remaining);
383        match self.inner.next_bytes(Some(remaining)) {
384            Some(Ok(Data::Bytes(bytes))) => {
385                tracing::trace!("Readed new bytes size from reader: {}", bytes.len());
386                self.bytes_yielded += bytes.len();
387                Some(Ok(Data::Bytes(bytes)))
388            }
389            Some(Ok(data)) => Some(Ok(data)), // Data::Retry
390            Some(Err(e)) => Some(Err(Box::new(e))),
391            None => None,
392        }
393    }
394}
395
396// ---------------------------------------------------------------------------
397// EOFStreamReader — reads until EOF via iterator
398// ---------------------------------------------------------------------------
399
400/// WHY: Stream until EOF via `BoxedSendableIterator`, exposing `Data::Retry`.
401///
402/// WHAT: Iterator yielding `Result<Data, BoxedError>` until EOF.
403pub struct EOFStreamReader<R: Read> {
404    inner: BatchReader<R>,
405}
406
407impl<R: Read> EOFStreamReader<R> {
408    /// Create a new `EOFStreamReader` that reads until EOF.
409    ///
410    /// # Panics
411    /// Never panics.
412    pub fn new(inner: BatchReader<R>) -> Self {
413        Self { inner }
414    }
415}
416
417impl<R: Read + Send> Iterator for EOFStreamReader<R> {
418    type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
419
420    fn next(&mut self) -> Option<Self::Item> {
421        self.inner
422            .next()
423            .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error + 'static>))
424    }
425}
426
427// ---------------------------------------------------------------------------
428// LimitedEOFStreamReader — reads until EOF with max size enforcement
429// ---------------------------------------------------------------------------
430
431/// WHY: Stream until EOF with max size enforcement, exposing `Data::Retry`.
432///
433/// WHAT: Iterator yielding `Result<Data, BoxedError>`, errors if cap exceeded.
434pub struct LimitedEOFStreamReader<R: Read> {
435    inner: BatchReader<R>,
436    bytes_yielded: usize,
437    byte_cap: usize,
438}
439
440impl<R: Read> LimitedEOFStreamReader<R> {
441    /// Create a new `LimitedEOFStreamReader` capped at `byte_cap` bytes.
442    ///
443    /// # Panics
444    /// Never panics.
445    pub fn new(inner: BatchReader<R>, byte_cap: usize) -> Self {
446        Self {
447            inner,
448            bytes_yielded: 0,
449            byte_cap,
450        }
451    }
452}
453
454impl<R: Read + Send> Iterator for LimitedEOFStreamReader<R> {
455    type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
456
457    fn next(&mut self) -> Option<Self::Item> {
458        if self.bytes_yielded > self.byte_cap {
459            tracing::error!(
460                "body size {} exceeds max {}",
461                self.bytes_yielded,
462                self.byte_cap
463            );
464            return Some(Err(Box::new(io::Error::new(
465                io::ErrorKind::InvalidInput,
466                format!(
467                    "body size {} exceeds max {}",
468                    self.bytes_yielded, self.byte_cap
469                ),
470            ))));
471        }
472
473        let remaining = self.byte_cap - self.bytes_yielded;
474        tracing::trace!(remaining = %remaining, "Remaining bytes to read: {}", &remaining);
475        match self.inner.next_bytes(Some(remaining)) {
476            Some(Ok(Data::Bytes(bytes))) => {
477                self.bytes_yielded += bytes.len();
478                if self.bytes_yielded > self.byte_cap {
479                    tracing::error!(
480                        "body size {} exceeds max {}",
481                        self.bytes_yielded,
482                        self.byte_cap
483                    );
484                    return None;
485                }
486                Some(Ok(Data::Bytes(bytes)))
487            }
488            Some(Ok(Data::Retry)) => Some(Ok(Data::Retry)),
489            Some(Err(e)) => Some(Err(Box::new(e) as Box<dyn std::error::Error + 'static>)),
490            None => None,
491        }
492    }
493}
494
495// ---------------------------------------------------------------------------
496// DataBytesIterator — backward compat: absorbs retries, yields Vec<u8>
497// ---------------------------------------------------------------------------
498
499use std::marker::PhantomData;
500
501/// WHY: Backward compatibility — restores old retry-absorbing behavior.
502///
503/// WHAT: Wraps `Iterator<Item=Result<Data, E>>` and loops on `Data::Retry`.
504///
505/// HOW: Transforms `Data::Bytes` into `Vec<u8>`, filters `Data::Retry`.
506pub struct DataBytesIterator<I, E> {
507    inner: I,
508    _marker: PhantomData<E>,
509}
510
511impl<I, E> DataBytesIterator<I, E> {
512    /// Wrap a `Data`-exposing iterator into a `Vec<u8>`-yielding iterator.
513    ///
514    /// # Panics
515    /// Never panics.
516    pub fn new(inner: I) -> Self {
517        Self {
518            inner,
519            _marker: PhantomData,
520        }
521    }
522}
523
524impl<I, E> Iterator for DataBytesIterator<I, E>
525where
526    I: Iterator<Item = Result<Data, E>>,
527{
528    type Item = Result<Vec<u8>, E>;
529
530    fn next(&mut self) -> Option<Self::Item> {
531        loop {
532            match self.inner.next() {
533                Some(Ok(Data::Retry)) => continue, // Absorb and retry
534                Some(Ok(Data::Bytes(bytes))) => return Some(Ok(bytes)),
535                Some(Err(e)) => return Some(Err(e)),
536                None => return None,
537            }
538        }
539    }
540}
541
542/// WHY: Convenience extension trait for converting `Data`-exposing iterators
543/// to `Vec<u8>`-yielding iterators via `.into_bytes()`.
544///
545/// WHAT: Extension trait implemented for all `Iterator<Item=Result<Data, E>>`.
546pub trait IntoDataBytes<E> {
547    fn into_bytes(self) -> DataBytesIterator<Self, E>
548    where
549        Self: Iterator<Item = Result<Data, E>> + Sized,
550    {
551        DataBytesIterator::new(self)
552    }
553}
554
555impl<I, E> IntoDataBytes<E> for I where I: Iterator<Item = Result<Data, E>> {}
556
557// ---------------------------------------------------------------------------
558// Tests
559// ---------------------------------------------------------------------------