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// ---------------------------------------------------------------------------