hurray_io/stream/reader.rs
1use bytes::{Bytes, BytesMut};
2use hurray_core::{CompositeValidator, LayoutDescriptor, SyncMode, TensorDescriptor};
3use tokio::io::{AsyncRead, AsyncReadExt};
4
5use crate::stream::frame;
6use crate::{Error, Result};
7
8/// Default maximum descriptor size: 16 MiB.
9///
10/// Limits memory allocation when reading untrusted streams.
11pub const DEFAULT_MAX_DESCRIPTOR_BYTES: u64 = 16 * 1024 * 1024;
12
13/// Default maximum composite nesting depth.
14///
15/// A composite member may itself be a composite (ADR-027 § Binding). This bounds the
16/// recursion so a maliciously deep composite on an untrusted stream cannot exhaust the
17/// stack. Matches the format's rank cap of 64 in spirit.
18pub const DEFAULT_MAX_COMPOSITE_DEPTH: usize = 64;
19
20/// Options for [`StreamReader`].
21pub struct StreamReaderOptions {
22 /// Maximum byte length of a single descriptor. Default: 16 MiB.
23 pub max_descriptor_bytes: u64,
24 /// Maximum byte length of a single buffer. Default: [`u64::MAX`] (unbounded).
25 pub max_buffer_bytes: u64,
26 /// Reject buffers whose `sync_mode` is not [`SyncMode::ProducerSynced`].
27 ///
28 /// Enable when the stream crosses machine boundaries.
29 pub enforce_cross_machine_sync: bool,
30 /// Maximum composite nesting depth for [`next_item`][StreamReader::next_item].
31 /// Default: [`DEFAULT_MAX_COMPOSITE_DEPTH`].
32 pub max_composite_depth: usize,
33}
34
35impl Default for StreamReaderOptions {
36 fn default() -> Self {
37 Self {
38 max_descriptor_bytes: DEFAULT_MAX_DESCRIPTOR_BYTES,
39 max_buffer_bytes: u64::MAX,
40 enforce_cross_machine_sync: false,
41 max_composite_depth: DEFAULT_MAX_COMPOSITE_DEPTH,
42 }
43 }
44}
45
46/// A decoded tensor: its descriptor plus zero-copy buffer views.
47///
48/// Each element in `buffers` corresponds to the [`hurray_core::BufferHandle`]
49/// at the same index in `descriptor.buffers`.
50#[derive(Debug)]
51pub struct StreamTensor {
52 /// The decoded tensor descriptor.
53 pub descriptor: TensorDescriptor,
54 /// Raw buffer bytes, one [`Bytes`] per buffer handle.
55 pub buffers: Vec<Bytes>,
56}
57
58/// One decoded item from the stream: either a plain tensor or a composite.
59///
60/// Returned by [`next_item`][StreamReader::next_item]. A composite's members are
61/// themselves [`StreamItem`]s, so nested composites (ADR-027 § Binding) are represented
62/// as a tree.
63#[derive(Debug)]
64pub enum StreamItem {
65 /// A single (non-composite) tensor.
66 Tensor(StreamTensor),
67 /// A composite: a data-less head plus its ordered members.
68 Composite(StreamComposite),
69}
70
71impl StreamItem {
72 /// The item's governing descriptor: the tensor's descriptor, or the composite head.
73 pub fn descriptor(&self) -> &TensorDescriptor {
74 match self {
75 StreamItem::Tensor(t) => &t.descriptor,
76 StreamItem::Composite(c) => &c.head,
77 }
78 }
79}
80
81/// A decoded composite tensor: its data-less head plus its ordered members.
82///
83/// Membership and ordering are exactly as they appeared on the wire (head precedes its
84/// members; ADR-027 § Binding). The head, `member_count`, and per-rule constraints
85/// (partition coverage, overlay ordering) have already been validated via
86/// [`CompositeValidator`].
87#[derive(Debug)]
88pub struct StreamComposite {
89 /// The composite head descriptor (owns no data buffers).
90 pub head: TensorDescriptor,
91 /// The members, in wire order. Each may itself be a composite (nesting).
92 pub members: Vec<StreamItem>,
93}
94
95/// Reads tensors from an async source in the Hurray streaming wire format.
96///
97/// Call [`next_tensor`][StreamReader::next_tensor] in a loop until it returns
98/// `Ok(None)` (clean EOF).
99///
100/// # Examples
101///
102/// ```no_run
103/// # #[tokio::main]
104/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105/// use hurray_io::stream::StreamReader;
106///
107/// let wire: &[u8] = &[]; // replace with actual data
108/// let mut reader = StreamReader::new(wire);
109/// while let Some(tensor) = reader.next_tensor().await? {
110/// println!(
111/// "tensor with {} buffer(s)",
112/// tensor.buffers.len()
113/// );
114/// }
115/// # Ok(())
116/// # }
117/// ```
118pub struct StreamReader<R> {
119 inner: R,
120 options: StreamReaderOptions,
121}
122
123impl<R: AsyncRead + Unpin> StreamReader<R> {
124 /// Creates a reader with default options.
125 pub fn new(inner: R) -> Self {
126 Self {
127 inner,
128 options: StreamReaderOptions::default(),
129 }
130 }
131
132 /// Creates a reader that enforces `ProducerSynced` on every buffer.
133 ///
134 /// Use when the stream was produced by a remote machine and GPU/semaphore
135 /// sync primitives are not meaningful locally.
136 pub fn cross_machine(inner: R) -> Self {
137 Self {
138 inner,
139 options: StreamReaderOptions {
140 enforce_cross_machine_sync: true,
141 ..Default::default()
142 },
143 }
144 }
145
146 /// Creates a reader with custom options.
147 pub fn with_options(inner: R, options: StreamReaderOptions) -> Self {
148 Self { inner, options }
149 }
150
151 /// Reads the next tensor from the stream.
152 ///
153 /// Returns `Ok(None)` on a clean EOF (no bytes remaining before a descriptor starts).
154 ///
155 /// # Errors
156 ///
157 /// - [`Error::UnexpectedEof`] — stream ended mid-descriptor or mid-buffer
158 /// - [`Error::InvalidHeader`] — malformed descriptor prefix
159 /// - [`Error::FrameTooLarge`] — descriptor or buffer exceeds configured limit
160 /// - [`Error::InvalidCrossMachineSyncMode`] — cross-machine mode and a non-`ProducerSynced` buffer
161 /// - [`Error::Core`] — descriptor decode failed
162 /// - [`Error::Io`] — underlying read error
163 pub async fn next_tensor(&mut self) -> Result<Option<StreamTensor>> {
164 let desc = match frame::read_descriptor(&mut self.inner, self.options.max_descriptor_bytes)
165 .await?
166 {
167 Some(d) => d,
168 None => return Ok(None),
169 };
170
171 let buffers = self.read_buffers(&desc).await?;
172
173 Ok(Some(StreamTensor {
174 descriptor: desc,
175 buffers,
176 }))
177 }
178
179 /// Reads the next item, assembling composites (head + members) into a
180 /// [`StreamItem::Composite`] and validating them.
181 ///
182 /// When the next descriptor on the wire is a composite head (`layout_tag = 0x0B`), this
183 /// reads its declared `member_count` members — each of which may itself be a composite
184 /// (recursion) — validates the group with [`CompositeValidator`] (member count, plus
185 /// partition coverage / overlay ordering), and returns them together. Otherwise it
186 /// behaves like [`next_tensor`][StreamReader::next_tensor], returning a
187 /// [`StreamItem::Tensor`]. Returns `Ok(None)` on a clean EOF.
188 ///
189 /// # Examples
190 ///
191 /// ```no_run
192 /// # #[tokio::main]
193 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
194 /// use hurray_io::stream::{StreamItem, StreamReader};
195 ///
196 /// let wire: &[u8] = &[]; // replace with actual data
197 /// let mut reader = StreamReader::new(wire);
198 /// while let Some(item) = reader.next_item().await? {
199 /// match item {
200 /// StreamItem::Tensor(t) => println!("tensor, {} buffer(s)", t.buffers.len()),
201 /// StreamItem::Composite(c) => println!("composite, {} member(s)", c.members.len()),
202 /// }
203 /// }
204 /// # Ok(())
205 /// # }
206 /// ```
207 ///
208 /// # Errors
209 ///
210 /// In addition to the errors of [`next_tensor`][StreamReader::next_tensor]:
211 ///
212 /// - [`Error::TornComposite`] — the stream ended before the head's `member_count`
213 /// members were read
214 /// - [`Error::CompositeNestingTooDeep`] — nesting exceeded `max_composite_depth`
215 /// - [`Error::Core`] — composite validation failed (e.g. partition does not cover the
216 /// index space, overlay ordering, member-count mismatch)
217 pub async fn next_item(&mut self) -> Result<Option<StreamItem>> {
218 self.read_item(0).await
219 }
220
221 /// Reads and freezes every buffer declared by `desc`, enforcing the buffer-size limit
222 /// and (in cross-machine mode) the sync-mode requirement.
223 async fn read_buffers(&mut self, desc: &TensorDescriptor) -> Result<Vec<Bytes>> {
224 let mut buffers = Vec::with_capacity(desc.buffers.len());
225
226 for (i, handle) in desc.buffers.iter().enumerate() {
227 if self.options.enforce_cross_machine_sync
228 && handle.sync_mode() != SyncMode::ProducerSynced
229 {
230 return Err(Error::InvalidCrossMachineSyncMode {
231 index: i,
232 actual: handle.sync_mode().to_byte(),
233 });
234 }
235
236 let byte_size = handle.byte_size();
237
238 if byte_size > self.options.max_buffer_bytes {
239 return Err(Error::FrameTooLarge {
240 kind: "buffer",
241 value: byte_size,
242 limit: self.options.max_buffer_bytes,
243 });
244 }
245
246 // One allocation per buffer; resize then freeze gives a Bytes with no extra copy.
247 let mut buf = BytesMut::with_capacity(byte_size as usize);
248 buf.resize(byte_size as usize, 0);
249 self.inner
250 .read_exact(&mut buf)
251 .await
252 .map_err(frame::map_unexpected_eof)?;
253 buffers.push(buf.freeze());
254 }
255
256 Ok(buffers)
257 }
258
259 /// Reads one item at composite-nesting `depth`, recursing into members.
260 async fn read_item(&mut self, depth: usize) -> Result<Option<StreamItem>> {
261 let desc = match frame::read_descriptor(&mut self.inner, self.options.max_descriptor_bytes)
262 .await?
263 {
264 Some(d) => d,
265 None => return Ok(None),
266 };
267
268 let member_count = match &desc.layout {
269 LayoutDescriptor::Composite(c) => c.member_count,
270 // Not a composite head: read its buffers and return a plain tensor.
271 _ => {
272 let buffers = self.read_buffers(&desc).await?;
273 return Ok(Some(StreamItem::Tensor(StreamTensor {
274 descriptor: desc,
275 buffers,
276 })));
277 }
278 };
279
280 if depth >= self.options.max_composite_depth {
281 return Err(Error::CompositeNestingTooDeep {
282 limit: self.options.max_composite_depth,
283 });
284 }
285
286 // Validate the group as its members arrive, reusing the core validator: member
287 // count, and per-rule constraints (partition coverage, overlay base-first ordering).
288 let mut validator = CompositeValidator::new(&desc)?;
289 let mut members = Vec::with_capacity(member_count as usize);
290 for i in 0..member_count {
291 // Box the recursive call: an async fn cannot name its own future inline.
292 let item = match Box::pin(self.read_item(depth + 1)).await? {
293 Some(item) => item,
294 None => {
295 return Err(Error::TornComposite {
296 declared: member_count,
297 actual: i,
298 })
299 }
300 };
301 validator.push_member(item.descriptor())?;
302 members.push(item);
303 }
304 validator.finish()?;
305
306 Ok(Some(StreamItem::Composite(StreamComposite {
307 head: desc,
308 members,
309 })))
310 }
311
312 /// Returns the underlying reader.
313 pub fn into_inner(self) -> R {
314 self.inner
315 }
316}