Skip to main content

nectar_primitives/file/
windowed.rs

1//! Bounded seek-and-play reader over the async joiner.
2//!
3//! [`WindowedReader`] seeks to a byte offset then streams forward in file order
4//! with peak memory bounded by `window` leaf bodies rather than the file size.
5//! The window slides with the read cursor: at most `window` leaf fetches plus
6//! buffered leaves are held at once, and the lowest-offset (head) leaf is always
7//! fetched first, so emission makes progress and the bound holds regardless of
8//! resolve order. Reseeking drops the window and repositions against the
9//! retained frontier, so no intermediate chunk is re-fetched.
10
11use std::collections::{BTreeMap, VecDeque};
12use std::io::SeekFrom;
13use std::pin::Pin;
14use std::sync::Arc;
15
16use bytes::Bytes;
17use futures::stream::{self, FuturesUnordered, Stream, StreamExt};
18
19use crate::bmt::DEFAULT_BODY_SIZE;
20use crate::chunk::ChunkAddress;
21
22use super::error::{FileError, Result};
23use super::frontier::{SubtreeNode, overlapping_children};
24use super::joiner::{GenericJoiner, MAX_INTERMEDIATE_IN_FLIGHT};
25use super::mode::JoinMode;
26use super::tree::{ChunkRange, TreeParams};
27use crate::store::{ChunkGet, MaybeSend};
28
29/// Number of times a failed leaf fetch is re-enqueued before the walk surfaces
30/// the error.
31const DEFAULT_LEAF_RETRIES: u32 = 4;
32
33#[cfg(test)]
34thread_local! {
35    /// Test hook: peak observed `leaf_in_flight + buffered.len()` across a walk
36    /// on this thread, so a test can assert the true window bound on leaf bodies
37    /// held at once. Thread-local because `block_on` runs each test's walk on its
38    /// own thread, so the parallel test runner never interleaves the counter.
39    static PEAK_LEAF_OCCUPANCY: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
40}
41
42/// Record a sampled occupancy, keeping the running peak for this thread.
43#[cfg(test)]
44fn record_occupancy(occupancy: usize) {
45    PEAK_LEAF_OCCUPANCY.with(|p| p.set(p.get().max(occupancy)));
46}
47
48/// Seek-and-play reader: forward in-order delivery over a window that slides
49/// with the read cursor, bounded to `window` in-flight plus buffered leaves.
50///
51/// Retains the joiner's reusable state (getter, pre-expanded frontier, tree
52/// params) so each [`stream`](Self::stream) rebuilds a fresh window-bounded walk
53/// from `[position, size)` without re-expanding the frontier. [`seek`](Self::seek)
54/// only repositions; it never re-fetches intermediates.
55pub struct WindowedReader<G, M: JoinMode, const BODY_SIZE: usize = DEFAULT_BODY_SIZE>
56where
57    G: ChunkGet<BODY_SIZE>,
58{
59    getter: Arc<G>,
60    subtrees: Vec<SubtreeNode<M>>,
61    tree: TreeParams<BODY_SIZE>,
62    span: u64,
63    concurrency: usize,
64    position: u64,
65    window: usize,
66    #[allow(
67        dead_code,
68        reason = "retained reusable joiner state for re-stream-on-seek"
69    )]
70    root: ChunkAddress,
71}
72
73impl<G, M, const BODY_SIZE: usize> std::fmt::Debug for WindowedReader<G, M, BODY_SIZE>
74where
75    G: ChunkGet<BODY_SIZE>,
76    M: JoinMode,
77{
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("WindowedReader")
80            .field("span", &self.span)
81            .field("position", &self.position)
82            .field("concurrency", &self.concurrency)
83            .finish_non_exhaustive()
84    }
85}
86
87impl<G, M, const BODY_SIZE: usize> GenericJoiner<G, M, BODY_SIZE>
88where
89    G: ChunkGet<BODY_SIZE> + 'static,
90    M: JoinMode + MaybeSend + Sync,
91{
92    /// Seek-and-play reader over a window that slides with the read cursor. Peak
93    /// memory is `window` leaf bodies: in-flight plus buffered leaves never
94    /// exceed `window`. `window` is clamped to at least 1; a value of at least
95    /// `2 * concurrency` keeps the pool full even while the head leaf is the
96    /// straggler.
97    pub fn into_windowed_reader(self, window: usize) -> WindowedReader<G, M, BODY_SIZE> {
98        WindowedReader {
99            getter: Arc::clone(self.getter()),
100            subtrees: self.subtrees().to_vec(),
101            tree: self.tree(),
102            span: self.size(),
103            concurrency: self.concurrency(),
104            position: self.position(),
105            root: *self.root(),
106            window: window.max(1),
107        }
108    }
109}
110
111impl<G, M, const BODY_SIZE: usize> WindowedReader<G, M, BODY_SIZE>
112where
113    G: ChunkGet<BODY_SIZE> + 'static,
114    M: JoinMode + MaybeSend + Sync,
115{
116    /// In-order leaf bodies from the current position to EOF. Each item is one
117    /// contiguous run; reassembly is pure concatenation. Peak memory is `window`
118    /// leaf bodies: the window slides with the emit cursor, so only leaves within
119    /// `window` spans ahead of it are ever fetched or held.
120    pub fn stream(&mut self) -> impl Stream<Item = Result<Bytes>> + '_ {
121        windowed_walk::<G, M, BODY_SIZE>(
122            Arc::clone(&self.getter),
123            self.subtrees.clone(),
124            self.tree,
125            self.span,
126            self.concurrency,
127            self.position,
128            self.window,
129        )
130    }
131
132    /// Reposition: drop the window and restart the walk from `pos`. Cheap; reuses
133    /// the frontier, no intermediate re-fetch.
134    pub fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
135        self.position = super::resolve_seek_position(pos, self.position, self.span)?;
136        Ok(self.position)
137    }
138
139    /// Current read position.
140    #[inline]
141    pub const fn position(&self) -> u64 {
142        self.position
143    }
144
145    /// Total file size.
146    #[inline]
147    pub const fn size(&self) -> u64 {
148        self.span
149    }
150}
151
152/// Cfg-gated boxed resolved-future alias: `+ Send` on native, unbounded on wasm.
153#[cfg(not(target_arch = "wasm32"))]
154type BoxResolvedFuture<M> = Pin<Box<dyn std::future::Future<Output = Resolved<M>> + Send>>;
155#[cfg(target_arch = "wasm32")]
156type BoxResolvedFuture<M> = Pin<Box<dyn std::future::Future<Output = Resolved<M>>>>;
157
158/// One unit of pending work: a tree node plus its remaining retry budget. The
159/// budget only decrements on a failed leaf fetch.
160struct Pending<M: JoinMode> {
161    node: SubtreeNode<M>,
162    retries: u32,
163}
164
165/// What a worker future resolves to once its chunk lands.
166enum Resolved<M: JoinMode> {
167    /// A leaf: its absolute byte offset and decoded body.
168    Leaf(u64, Bytes),
169    /// An intermediate: the parent byte offset (to retire from the undescended
170    /// set) plus its overlapping children to re-queue.
171    Children(u64, Vec<SubtreeNode<M>>),
172    /// A leaf fetch failed with retries left: re-queue this node.
173    Retry(Pending<M>),
174    /// A fetch failed terminally (retries exhausted or intermediate error).
175    Failed(FileError),
176}
177
178/// Is this node a leaf?
179#[inline]
180fn is_leaf<M: JoinMode, const BS: usize>(node: &SubtreeNode<M>) -> bool {
181    node.span <= BS as u64
182}
183
184/// Fetch one node: a leaf yields its body, an intermediate yields its children.
185/// A leaf error consumes one retry, then re-queues or fails.
186async fn fetch_one<G, M, const BS: usize>(
187    getter: &G,
188    chunk_range: &ChunkRange,
189    pending: Pending<M>,
190) -> Resolved<M>
191where
192    G: ChunkGet<BS>,
193    M: JoinMode + MaybeSend + Sync,
194{
195    let node = &pending.node;
196    let body = match super::mode::read_chunk_body::<M, G, BS>(
197        getter,
198        &node.addr,
199        &node.context,
200        node.span,
201    )
202    .await
203    {
204        Ok(body) => body,
205        Err(e) => {
206            if is_leaf::<M, BS>(node) && pending.retries > 0 {
207                return Resolved::Retry(Pending {
208                    node: pending.node,
209                    retries: pending.retries - 1,
210                });
211            }
212            return Resolved::Failed(e);
213        }
214    };
215
216    if is_leaf::<M, BS>(node) {
217        return Resolved::Leaf(node.byte_offset, body);
218    }
219    let parent_offset = node.byte_offset;
220    match overlapping_children::<M, BS>(&body, node, chunk_range) {
221        Ok(children) => Resolved::Children(parent_offset, children),
222        Err(e) => Resolved::Failed(e),
223    }
224}
225
226/// Insert into an offset-ordered pending queue, keeping it ascending by byte
227/// offset so the front is always the lowest-offset node.
228fn insert_by_offset<M: JoinMode>(queue: &mut VecDeque<Pending<M>>, pending: Pending<M>) {
229    let pos = queue.partition_point(|p| p.node.byte_offset < pending.node.byte_offset);
230    queue.insert(pos, pending);
231}
232
233/// Window-bounded in-order walk from `start` to EOF.
234///
235/// A single walk integrating fetch and reorder. Intermediates and leaves are
236/// descended lowest-offset first and run on separate budgets out of `width`: at
237/// most `MAX_INTERMEDIATE_IN_FLIGHT` intermediate fetches resolve at once
238/// (enough to keep the next region's leaves discovered ahead of the cursor
239/// without fetching the whole frontier first), and a leaf is admitted only while
240/// in-flight plus buffered leaves are below `window`. Resolved leaves land in a
241/// `BTreeMap` reorder buffer keyed by absolute offset; the head is emitted when
242/// its offset equals `next_emit_offset`, which then advances by `body.len()`
243/// (never a fixed stride, so shorter encrypted leaves stay aligned) and frees a
244/// slot, sliding the window forward.
245///
246/// In-order and deadlock freedom rest on one rule: a queued leaf is admitted
247/// only when its offset is below every undescended intermediate's offset (a leaf
248/// at or above one could be preceded by a lower leaf that intermediate has yet
249/// to yield). Safe leaves admitted lowest-first are therefore a contiguous
250/// emittable prefix, so the window can never fill with un-emittable leaves while
251/// the head is still hidden behind an intermediate, and descending the lowest
252/// intermediate always exposes the head's region next. The count gate keeps
253/// in-flight plus buffered leaves at `window`, so peak memory is `window` leaf
254/// bodies regardless of resolve order, leaf span, tree depth, or file size.
255fn windowed_walk<G, M, const BODY_SIZE: usize>(
256    getter: Arc<G>,
257    subtrees: Vec<SubtreeNode<M>>,
258    tree: TreeParams<BODY_SIZE>,
259    span: u64,
260    concurrency: usize,
261    start: u64,
262    window: usize,
263) -> impl Stream<Item = Result<Bytes>>
264where
265    G: ChunkGet<BODY_SIZE> + 'static,
266    M: JoinMode + MaybeSend + Sync,
267{
268    let width = concurrency.max(1);
269    let window = window.max(1);
270
271    let range_start = start.min(span);
272    let chunk_range = tree.chunks_for_range(range_start, span - range_start);
273
274    struct State<G, M: JoinMode> {
275        getter: Arc<G>,
276        chunk_range: ChunkRange,
277        range_start: u64,
278        width: usize,
279        window: usize,
280        next_emit_offset: u64,
281        /// Leaves not yet admitted to the pool, kept in offset order so the
282        /// lowest-offset (head) leaf is always admitted first.
283        leaf_queue: VecDeque<Pending<M>>,
284        /// Intermediate nodes pending descent, kept in offset order so the
285        /// lowest-offset subtree is always descended first.
286        node_queue: VecDeque<Pending<M>>,
287        /// Byte offset -> count of undescended intermediates (queued or in
288        /// flight) starting there. The lowest key is the boundary below which a
289        /// leaf is safe to admit.
290        pending_intermediate_offsets: BTreeMap<u64, usize>,
291        /// Count of in-flight intermediate fetches (capped).
292        intermediate_in_flight: usize,
293        /// Count of in-flight leaf fetches (excludes intermediates).
294        leaf_in_flight: usize,
295        in_flight: FuturesUnordered<BoxResolvedFuture<M>>,
296        buffered: BTreeMap<u64, Bytes>,
297    }
298
299    // Seed the queues with the subtrees overlapping `[range_start, span)`,
300    // separating leaves from intermediates and recording each intermediate's
301    // offset as undescended.
302    let mut leaf_queue = VecDeque::new();
303    let mut node_queue = VecDeque::new();
304    let mut pending_intermediate_offsets: BTreeMap<u64, usize> = BTreeMap::new();
305    let range_start_byte = chunk_range.start * BODY_SIZE as u64;
306    let range_end_byte = chunk_range.end * BODY_SIZE as u64;
307    for st in subtrees {
308        if st.byte_offset >= range_end_byte || st.byte_offset + st.span <= range_start_byte {
309            continue;
310        }
311        let pending = Pending {
312            node: st,
313            retries: DEFAULT_LEAF_RETRIES,
314        };
315        if is_leaf::<M, BODY_SIZE>(&pending.node) {
316            insert_by_offset(&mut leaf_queue, pending);
317        } else {
318            *pending_intermediate_offsets
319                .entry(pending.node.byte_offset)
320                .or_insert(0) += 1;
321            insert_by_offset(&mut node_queue, pending);
322        }
323    }
324
325    let state = State::<G, M> {
326        getter,
327        chunk_range,
328        range_start,
329        width,
330        window,
331        next_emit_offset: range_start,
332        leaf_queue,
333        node_queue,
334        pending_intermediate_offsets,
335        intermediate_in_flight: 0,
336        leaf_in_flight: 0,
337        in_flight: FuturesUnordered::new(),
338        buffered: BTreeMap::new(),
339    };
340
341    stream::unfold(state, move |mut state| async move {
342        loop {
343            // Emit the head leaf as soon as it is buffered, advancing the cursor
344            // and sliding the window so the refill below admits newly-in-range
345            // leaves on the next turn.
346            let head_ready = state
347                .buffered
348                .first_key_value()
349                .is_some_and(|(&k, _)| k == state.next_emit_offset);
350            if head_ready {
351                let (_, body) = state.buffered.pop_first().expect("head just observed");
352                state.next_emit_offset += body.len() as u64;
353                return Some((Ok(body), state));
354            }
355
356            // Refill the pool. An intermediate is descended up to the cap,
357            // reserving a slot for a ready safe leaf at tiny widths; a leaf is
358            // admitted only when it is below every undescended intermediate (so
359            // no lower leaf can still appear) and a window slot is free. With no
360            // safe leaf ready, intermediates drive the descent to expose the
361            // head's region.
362            loop {
363                if state.in_flight.len() >= state.width {
364                    break;
365                }
366
367                let min_pending_intermediate =
368                    state.pending_intermediate_offsets.keys().next().copied();
369                let leaf_admissible = state.leaf_queue.front().is_some_and(|p| {
370                    min_pending_intermediate.is_none_or(|m| p.node.byte_offset < m)
371                }) && state.leaf_in_flight + state.buffered.len()
372                    < state.window;
373
374                let can_admit_intermediate = state.intermediate_in_flight
375                    < MAX_INTERMEDIATE_IN_FLIGHT
376                    && !state.node_queue.is_empty();
377                let admit_intermediate = can_admit_intermediate
378                    && (!leaf_admissible || state.intermediate_in_flight + 1 < state.width);
379
380                let pending = if admit_intermediate {
381                    state.intermediate_in_flight += 1;
382                    state.node_queue.pop_front().expect("node queue non-empty")
383                } else if leaf_admissible {
384                    state.leaf_in_flight += 1;
385                    state.leaf_queue.pop_front().expect("front admissible")
386                } else {
387                    break;
388                };
389
390                let getter = Arc::clone(&state.getter);
391                let range = state.chunk_range;
392                state.in_flight.push(Box::pin(async move {
393                    fetch_one::<G, M, BODY_SIZE>(&*getter, &range, pending).await
394                }) as BoxResolvedFuture<M>);
395            }
396
397            // In-flight leaf fetches plus buffered leaves never exceed `window`.
398            let occupancy = state.leaf_in_flight + state.buffered.len();
399            debug_assert!(
400                occupancy <= state.window,
401                "windowed walk exceeded the leaf-body bound"
402            );
403            #[cfg(test)]
404            record_occupancy(occupancy);
405
406            // Pool empty and nothing left to admit: the file is drained.
407            let resolved = match state.in_flight.next().await {
408                Some(r) => r,
409                None => return None,
410            };
411            match resolved {
412                Resolved::Leaf(leaf_start, body) => {
413                    state.leaf_in_flight -= 1;
414                    // Clip the first partial leaf at the read position; offsets
415                    // stay absolute, so a boundary leaf buffers only its in-range
416                    // tail keyed at the read position.
417                    let leaf_end = leaf_start + body.len() as u64;
418                    if leaf_end <= state.range_start {
419                        continue;
420                    }
421                    let clip_lo = state.range_start.saturating_sub(leaf_start) as usize;
422                    let offset = leaf_start.max(state.range_start);
423                    state.buffered.insert(offset, body.slice(clip_lo..));
424                }
425                Resolved::Children(parent_offset, children) => {
426                    state.intermediate_in_flight -= 1;
427                    // Retire the descended parent from the undescended set.
428                    if let Some(count) = state.pending_intermediate_offsets.get_mut(&parent_offset)
429                    {
430                        *count -= 1;
431                        if *count == 0 {
432                            state.pending_intermediate_offsets.remove(&parent_offset);
433                        }
434                    }
435                    for child in children {
436                        let pending = Pending {
437                            node: child,
438                            retries: DEFAULT_LEAF_RETRIES,
439                        };
440                        if is_leaf::<M, BODY_SIZE>(&pending.node) {
441                            insert_by_offset(&mut state.leaf_queue, pending);
442                        } else {
443                            *state
444                                .pending_intermediate_offsets
445                                .entry(pending.node.byte_offset)
446                                .or_insert(0) += 1;
447                            insert_by_offset(&mut state.node_queue, pending);
448                        }
449                    }
450                }
451                Resolved::Retry(pending) => {
452                    // Re-enqueue in offset order so the gate still sees the lowest
453                    // leaf at the front and the slot it held is freed for refill.
454                    state.leaf_in_flight -= 1;
455                    insert_by_offset(&mut state.leaf_queue, pending);
456                }
457                Resolved::Failed(e) => return Some((Err(e), state)),
458            }
459        }
460    })
461}
462
463/// Native `AsyncRead` + `AsyncSeek` shim, draining [`WindowedReader::stream`]
464/// into a residual buffer.
465#[cfg(feature = "tokio")]
466pub struct WindowedJoinerReader<G, M: JoinMode, const BODY_SIZE: usize = DEFAULT_BODY_SIZE>
467where
468    G: ChunkGet<BODY_SIZE>,
469{
470    reader: WindowedReader<G, M, BODY_SIZE>,
471    residual: Bytes,
472    #[allow(clippy::type_complexity)]
473    stream: Option<Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>>,
474}
475
476#[cfg(feature = "tokio")]
477impl<G, M, const BODY_SIZE: usize> std::fmt::Debug for WindowedJoinerReader<G, M, BODY_SIZE>
478where
479    G: ChunkGet<BODY_SIZE>,
480    M: JoinMode,
481{
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        f.debug_struct("WindowedJoinerReader")
484            .field("reader", &self.reader)
485            .field("residual_len", &self.residual.len())
486            .field("has_stream", &self.stream.is_some())
487            .finish()
488    }
489}
490
491#[cfg(feature = "tokio")]
492impl<G, M, const BODY_SIZE: usize> WindowedReader<G, M, BODY_SIZE>
493where
494    G: ChunkGet<BODY_SIZE> + 'static,
495    M: JoinMode + Send + Sync + 'static,
496{
497    /// Wrap as a tokio `AsyncRead` + `AsyncSeek` reader. Native-only.
498    pub fn into_reader(self) -> WindowedJoinerReader<G, M, BODY_SIZE> {
499        WindowedJoinerReader {
500            reader: self,
501            residual: Bytes::new(),
502            stream: None,
503        }
504    }
505}
506
507#[cfg(feature = "tokio")]
508impl<G: ChunkGet<BODY_SIZE>, M: JoinMode, const BODY_SIZE: usize> Unpin
509    for WindowedJoinerReader<G, M, BODY_SIZE>
510{
511}
512
513#[cfg(feature = "tokio")]
514impl<G, M, const BODY_SIZE: usize> tokio::io::AsyncRead for WindowedJoinerReader<G, M, BODY_SIZE>
515where
516    G: ChunkGet<BODY_SIZE> + 'static,
517    M: JoinMode + Send + Sync + 'static,
518{
519    fn poll_read(
520        self: Pin<&mut Self>,
521        cx: &mut std::task::Context<'_>,
522        buf: &mut tokio::io::ReadBuf<'_>,
523    ) -> std::task::Poll<std::io::Result<()>> {
524        use bytes::Buf;
525        use std::task::Poll;
526
527        let this = self.get_mut();
528
529        if !this.residual.is_empty() {
530            let to_copy = this.residual.len().min(buf.remaining());
531            buf.put_slice(&this.residual[..to_copy]);
532            this.residual.advance(to_copy);
533            return Poll::Ready(Ok(()));
534        }
535
536        if this.stream.is_none() {
537            // Rebuild an owned `'static` stream from the retained joiner parts,
538            // so the boxed stream does not borrow `this`. Cloning the parts is the
539            // same cost as `stream()`; no frontier re-expansion.
540            let r = &this.reader;
541            this.stream = Some(Box::pin(windowed_walk::<G, M, BODY_SIZE>(
542                Arc::clone(&r.getter),
543                r.subtrees.clone(),
544                r.tree,
545                r.span,
546                r.concurrency,
547                r.position,
548                r.window,
549            )));
550        }
551
552        let stream = this.stream.as_mut().expect("stream just set");
553        match stream.as_mut().poll_next(cx) {
554            Poll::Ready(Some(Ok(body))) => {
555                let to_copy = body.len().min(buf.remaining());
556                buf.put_slice(&body[..to_copy]);
557                this.reader.position += body.len() as u64;
558                if to_copy < body.len() {
559                    this.residual = body.slice(to_copy..);
560                }
561                Poll::Ready(Ok(()))
562            }
563            Poll::Ready(Some(Err(e))) => Poll::Ready(Err(std::io::Error::other(e))),
564            Poll::Ready(None) => Poll::Ready(Ok(())),
565            Poll::Pending => Poll::Pending,
566        }
567    }
568}
569
570#[cfg(feature = "tokio")]
571impl<G, M, const BODY_SIZE: usize> tokio::io::AsyncSeek for WindowedJoinerReader<G, M, BODY_SIZE>
572where
573    G: ChunkGet<BODY_SIZE> + 'static,
574    M: JoinMode + Send + Sync + 'static,
575{
576    fn start_seek(self: Pin<&mut Self>, pos: SeekFrom) -> std::io::Result<()> {
577        let this = self.get_mut();
578        this.stream = None;
579        this.residual = Bytes::new();
580        this.reader.seek(pos)?;
581        Ok(())
582    }
583
584    fn poll_complete(
585        self: Pin<&mut Self>,
586        _cx: &mut std::task::Context<'_>,
587    ) -> std::task::Poll<std::io::Result<u64>> {
588        std::task::Poll::Ready(Ok(self.get_mut().reader.position))
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use crate::bmt::DEFAULT_BODY_SIZE;
596    use crate::chunk::AnyChunk;
597    use crate::file::Joiner;
598    use crate::file::split;
599    use futures::executor::block_on;
600    use std::collections::HashMap;
601    use std::sync::atomic::{AtomicUsize, Ordering};
602
603    fn split_and_store(data: &[u8]) -> (ChunkAddress, HashMap<ChunkAddress, AnyChunk>) {
604        let (root, store) = split::<DEFAULT_BODY_SIZE>(data).unwrap();
605        (root, store.into_chunks())
606    }
607
608    async fn drain(
609        reader: &mut WindowedReader<HashMap<ChunkAddress, AnyChunk>, super::super::mode::PlainMode>,
610    ) -> Vec<u8> {
611        let stream = reader.stream();
612        futures::pin_mut!(stream);
613        let mut out = Vec::new();
614        while let Some(item) = stream.next().await {
615            out.extend_from_slice(&item.unwrap());
616        }
617        out
618    }
619
620    #[test]
621    fn stream_from_zero_equals_whole_file() {
622        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 321)
623            .map(|i| (i % 256) as u8)
624            .collect();
625        let (root, store) = split_and_store(&data);
626        block_on(async {
627            let joiner = Joiner::new(store, root).await.unwrap();
628            let mut reader = joiner.into_windowed_reader(16);
629            let got = drain(&mut reader).await;
630            assert_eq!(got, data);
631        });
632    }
633
634    #[test]
635    fn seek_then_stream_yields_suffix() {
636        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 321)
637            .map(|i| (i % 256) as u8)
638            .collect();
639        let bs = DEFAULT_BODY_SIZE as u64;
640        let cases = [bs + 10, bs, bs * 3, bs / 2, data.len() as u64 - 1];
641        let (root, store) = split_and_store(&data);
642        block_on(async {
643            for k in cases {
644                let joiner = Joiner::new(store.clone(), root).await.unwrap();
645                let mut reader = joiner.into_windowed_reader(16);
646                reader.seek(SeekFrom::Start(k)).unwrap();
647                let got = drain(&mut reader).await;
648                assert_eq!(got, &data[k as usize..], "suffix from {k}");
649            }
650        });
651    }
652
653    #[test]
654    fn backward_seek_then_read_is_correct() {
655        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 4 + 17)
656            .map(|i| (i % 256) as u8)
657            .collect();
658        let bs = DEFAULT_BODY_SIZE as u64;
659        let (root, store) = split_and_store(&data);
660        block_on(async {
661            let joiner = Joiner::new(store, root).await.unwrap();
662            let mut reader = joiner.into_windowed_reader(16);
663            reader.seek(SeekFrom::Start(bs * 3)).unwrap();
664            let _ = drain(&mut reader).await;
665            reader.seek(SeekFrom::Start(bs)).unwrap();
666            let got = drain(&mut reader).await;
667            assert_eq!(got, &data[bs as usize..]);
668        });
669    }
670
671    #[test]
672    fn width_one_correct() {
673        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 7)
674            .map(|i| (i % 256) as u8)
675            .collect();
676        let (root, store) = split_and_store(&data);
677        block_on(async {
678            let joiner = Joiner::new(store, root).await.unwrap().with_concurrency(1);
679            let mut reader = joiner.into_windowed_reader(1);
680            let got = drain(&mut reader).await;
681            assert_eq!(got, data);
682        });
683    }
684
685    /// A self-waking yield: returns `Pending` once and immediately schedules a
686    /// re-poll, so it makes progress under `futures::executor::block_on`.
687    async fn yield_now() {
688        struct YieldNow(bool);
689        impl std::future::Future for YieldNow {
690            type Output = ();
691            fn poll(
692                mut self: Pin<&mut Self>,
693                cx: &mut std::task::Context<'_>,
694            ) -> std::task::Poll<()> {
695                if self.0 {
696                    std::task::Poll::Ready(())
697                } else {
698                    self.0 = true;
699                    cx.waker().wake_by_ref();
700                    std::task::Poll::Pending
701                }
702            }
703        }
704        YieldNow(false).await
705    }
706
707    /// A getter that records peak concurrent fetches so a test can assert the
708    /// reorder buffer never admits more than `window` leaves at once.
709    #[derive(Clone)]
710    struct BoundProbe {
711        chunks: Arc<HashMap<ChunkAddress, AnyChunk>>,
712        in_flight: Arc<AtomicUsize>,
713        max_in_flight: Arc<AtomicUsize>,
714    }
715
716    impl ChunkGet<DEFAULT_BODY_SIZE> for BoundProbe {
717        type Error = crate::store::ChunkStoreError;
718
719        async fn get(&self, address: &ChunkAddress) -> std::result::Result<AnyChunk, Self::Error> {
720            let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
721            self.max_in_flight.fetch_max(now, Ordering::SeqCst);
722            // Hold the fetch open across several self-waking yields so
723            // concurrently admitted leaves overlap here before any resolves.
724            for _ in 0..4 {
725                yield_now().await;
726            }
727            self.in_flight.fetch_sub(1, Ordering::SeqCst);
728            self.chunks
729                .get(address)
730                .cloned()
731                .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
732        }
733    }
734
735    #[test]
736    fn reorder_buffer_never_exceeds_window() {
737        // Many leaves, small window: leaf fetches overlap, so without the bound
738        // the in-flight peak would climb to the leaf count. The window caps it.
739        let leaves = 40usize;
740        let window = 6usize;
741        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * leaves)
742            .map(|i| (i % 256) as u8)
743            .collect();
744        let (root, store) = split_and_store(&data);
745
746        let probe = BoundProbe {
747            chunks: Arc::new(store),
748            in_flight: Arc::new(AtomicUsize::new(0)),
749            max_in_flight: Arc::new(AtomicUsize::new(0)),
750        };
751        let max_seen = Arc::clone(&probe.max_in_flight);
752
753        block_on(async {
754            let joiner = Joiner::new(probe, root)
755                .await
756                .unwrap()
757                .with_concurrency(leaves);
758            let mut reader = joiner.into_windowed_reader(window);
759            let stream = reader.stream();
760            futures::pin_mut!(stream);
761            let mut total = 0usize;
762            while let Some(item) = stream.next().await {
763                total += item.unwrap().len();
764            }
765            assert_eq!(total, data.len());
766        });
767
768        // The reorder buffer admits at most `window` leaves, so concurrent leaf
769        // fetches (plus the intermediate node fetches that seed children) never
770        // climb far above the window. Allow a small slack for in-flight
771        // intermediates, but assert it is nowhere near the leaf count.
772        let peak = max_seen.load(Ordering::SeqCst);
773        assert!(
774            peak <= window + 2,
775            "in-flight peak {peak} exceeds window {window} (+slack)"
776        );
777    }
778
779    /// A getter that delays the leaf at `slow_offset` until `gate` other leaves
780    /// have resolved. This is the adversarial resolve order the original reorder
781    /// buffer mishandled: with the head leaf landing last, the old buffer kept
782    /// fetching and buffering later leaves, so resident leaf bodies (in-flight
783    /// plus buffered) climbed to roughly twice the window. The sliding window
784    /// refuses to fetch past a free slot, holding the total at the window.
785    #[derive(Clone)]
786    struct HeadSlow {
787        chunks: Arc<HashMap<ChunkAddress, AnyChunk>>,
788        /// File offset of the head leaf to delay.
789        slow_offset: u64,
790        /// Number of leaves released so far (used to gate the slow leaf).
791        released: Arc<AtomicUsize>,
792        /// How many other leaves must release before the slow leaf may.
793        gate: usize,
794        /// Address-to-offset map so the getter can recognise the head leaf.
795        leaf_offsets: Arc<HashMap<ChunkAddress, u64>>,
796    }
797
798    impl ChunkGet<DEFAULT_BODY_SIZE> for HeadSlow {
799        type Error = crate::store::ChunkStoreError;
800
801        async fn get(&self, address: &ChunkAddress) -> std::result::Result<AnyChunk, Self::Error> {
802            let is_slow = self.leaf_offsets.get(address) == Some(&self.slow_offset);
803            if is_slow {
804                // Park until the gate of other leaves has released, then a few
805                // more yields so a buffer would overflow if it were unbounded.
806                while self.released.load(Ordering::SeqCst) < self.gate {
807                    yield_now().await;
808                }
809                for _ in 0..4 {
810                    yield_now().await;
811                }
812            } else {
813                for _ in 0..4 {
814                    yield_now().await;
815                }
816                if self.leaf_offsets.contains_key(address) {
817                    self.released.fetch_add(1, Ordering::SeqCst);
818                }
819            }
820
821            self.chunks
822                .get(address)
823                .cloned()
824                .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
825        }
826    }
827
828    /// Distinct byte per file position, so every leaf body (and thus its
829    /// content address) is unique. A `(i % 256)` fill would make all full leaves
830    /// identical, collapsing the address-to-offset map and defeating the
831    /// per-leaf delay below.
832    fn unique_byte(i: u64) -> u8 {
833        (i.wrapping_mul(2_654_435_761) >> 11) as u8
834    }
835
836    /// Map each leaf chunk address to its file offset. Leaf bodies are
837    /// content-addressed, so rebuilding each leaf from `unique_byte` reproduces
838    /// the stored address.
839    fn leaf_offsets_of(total: u64) -> HashMap<ChunkAddress, u64> {
840        use crate::chunk::Chunk;
841        let mut map = HashMap::new();
842        let leaves = total.div_ceil(DEFAULT_BODY_SIZE as u64);
843        for i in 0..leaves {
844            let off = i * DEFAULT_BODY_SIZE as u64;
845            let end = (off + DEFAULT_BODY_SIZE as u64).min(total);
846            let body: Vec<u8> = (off..end).map(unique_byte).collect();
847            let chunk = crate::chunk::ContentChunk::<DEFAULT_BODY_SIZE>::new(body).unwrap();
848            map.insert(*chunk.address(), off);
849        }
850        map
851    }
852
853    #[test]
854    fn head_slowest_in_order_and_bounded() {
855        // The head leaf at `position` resolves last among the leaves in its
856        // window. Under the old buffer this leaks: with the head missing, the
857        // walk kept `window` leaf fetches in flight while also buffering the
858        // resolved stragglers, so resident leaf bodies reached roughly twice the
859        // window. The sliding window keeps in-flight plus buffered at `window` and
860        // delivery in order. A naive run at `gate = window` instead would deadlock
861        // the correct walk (the head can never release), so the gate is the most a
862        // non-deadlocking adversary can demand, and the old buffer still overruns
863        // it.
864        let leaves = 40usize;
865        let window = 6usize;
866        let total = (DEFAULT_BODY_SIZE * leaves) as u64;
867        let data: Vec<u8> = (0..total).map(unique_byte).collect();
868        let (root, store) = split_and_store(&data);
869
870        PEAK_LEAF_OCCUPANCY.with(|p| p.set(0));
871        let leaf_offsets = leaf_offsets_of(total);
872        let getter = HeadSlow {
873            chunks: Arc::new(store),
874            slow_offset: 0,
875            released: Arc::new(AtomicUsize::new(0)),
876            gate: window - 1,
877            leaf_offsets: Arc::new(leaf_offsets),
878        };
879
880        block_on(async {
881            let joiner = Joiner::new(getter, root)
882                .await
883                .unwrap()
884                .with_concurrency(leaves);
885            let mut reader = joiner.into_windowed_reader(window);
886            let stream = reader.stream();
887            futures::pin_mut!(stream);
888            let mut got = Vec::new();
889            while let Some(item) = stream.next().await {
890                got.extend_from_slice(&item.unwrap());
891            }
892            assert_eq!(got, data, "head-slowest still yields whole file in order");
893        });
894
895        let peak = PEAK_LEAF_OCCUPANCY.with(std::cell::Cell::get);
896        assert!(
897            peak <= window,
898            "leaf-body occupancy peak {peak} exceeds window {window}"
899        );
900    }
901
902    // --- Front-load / deep-frontier guards (small body size = deep tree) ---
903
904    /// Branching for a 256-byte body is `256 / REF_SIZE` = 8, so a few hundred
905    /// leaves build a wide intermediate frontier with little data.
906    const TINY_BODY: usize = 256;
907
908    fn tiny_leaf_addresses(data: &[u8]) -> HashMap<ChunkAddress, ()> {
909        use crate::chunk::Chunk;
910        let mut set = HashMap::new();
911        for block in data.chunks(TINY_BODY) {
912            let chunk = crate::chunk::ContentChunk::<TINY_BODY>::new(block.to_vec()).unwrap();
913            set.insert(*chunk.address(), ());
914        }
915        set
916    }
917
918    fn tiny_deep_data(leaves: usize) -> Vec<u8> {
919        (0..TINY_BODY * leaves).map(|i| (i % 251) as u8).collect()
920    }
921
922    /// Records the leaf/intermediate kind of every fetch in start order and the
923    /// peak concurrent intermediate fetches.
924    #[derive(Clone)]
925    struct TinyOrderProbe {
926        chunks: Arc<HashMap<ChunkAddress, AnyChunk<TINY_BODY>>>,
927        leaves: Arc<HashMap<ChunkAddress, ()>>,
928        kinds: Arc<std::sync::Mutex<Vec<bool>>>,
929        intermediate_in_flight: Arc<AtomicUsize>,
930        peak_intermediate: Arc<AtomicUsize>,
931    }
932
933    impl TinyOrderProbe {
934        fn new(store: HashMap<ChunkAddress, AnyChunk<TINY_BODY>>, data: &[u8]) -> Self {
935            Self {
936                chunks: Arc::new(store),
937                leaves: Arc::new(tiny_leaf_addresses(data)),
938                kinds: Arc::new(std::sync::Mutex::new(Vec::new())),
939                intermediate_in_flight: Arc::new(AtomicUsize::new(0)),
940                peak_intermediate: Arc::new(AtomicUsize::new(0)),
941            }
942        }
943
944        fn reset(&self) {
945            self.kinds.lock().unwrap().clear();
946            self.peak_intermediate.store(0, Ordering::SeqCst);
947        }
948
949        fn intermediates_before_first_leaf(&self) -> usize {
950            self.kinds
951                .lock()
952                .unwrap()
953                .iter()
954                .take_while(|is_leaf| !**is_leaf)
955                .count()
956        }
957
958        fn intermediate_fetches(&self) -> usize {
959            self.kinds.lock().unwrap().iter().filter(|l| !**l).count()
960        }
961    }
962
963    impl ChunkGet<TINY_BODY> for TinyOrderProbe {
964        type Error = crate::store::ChunkStoreError;
965
966        async fn get(
967            &self,
968            address: &ChunkAddress,
969        ) -> std::result::Result<AnyChunk<TINY_BODY>, Self::Error> {
970            let is_leaf = self.leaves.contains_key(address);
971            self.kinds.lock().unwrap().push(is_leaf);
972            if !is_leaf {
973                let now = self.intermediate_in_flight.fetch_add(1, Ordering::SeqCst) + 1;
974                self.peak_intermediate.fetch_max(now, Ordering::SeqCst);
975            }
976            for _ in 0..4 {
977                yield_now().await;
978            }
979            if !is_leaf {
980                self.intermediate_in_flight.fetch_sub(1, Ordering::SeqCst);
981            }
982            self.chunks
983                .get(address)
984                .cloned()
985                .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
986        }
987    }
988
989    /// The in-order regression guard: on a wide frontier the first data leaf is
990    /// fetched after only a short descent, not after the whole frontier drains.
991    #[test]
992    fn windowed_first_leaf_before_frontier() {
993        let data = tiny_deep_data(900);
994        let (root, store) = split::<TINY_BODY>(&data).unwrap();
995        let probe = TinyOrderProbe::new(store.into_chunks(), &data);
996
997        block_on(async {
998            let joiner = Joiner::<_, TINY_BODY>::new(probe.clone(), root)
999                .await
1000                .unwrap();
1001            // Measure only the streaming walk, not the upfront expansion.
1002            probe.reset();
1003            let mut reader = joiner.into_windowed_reader(16);
1004            let stream = reader.stream();
1005            futures::pin_mut!(stream);
1006            let mut got = Vec::new();
1007            while let Some(item) = stream.next().await {
1008                got.extend_from_slice(&item.unwrap());
1009            }
1010            assert_eq!(got, data, "deep-tree in-order reassembly is byte-exact");
1011        });
1012
1013        let frontier = probe.intermediate_fetches();
1014        assert!(
1015            frontier >= 40,
1016            "test needs a frontier far larger than the cap, saw {frontier}"
1017        );
1018        let before = probe.intermediates_before_first_leaf();
1019        assert!(
1020            before <= 4 * MAX_INTERMEDIATE_IN_FLIGHT,
1021            "first leaf fetched after {before} intermediates (frontier {frontier}); \
1022             expected a short descent, not the whole frontier"
1023        );
1024        let peak = probe.peak_intermediate.load(Ordering::SeqCst);
1025        assert!(
1026            peak <= MAX_INTERMEDIATE_IN_FLIGHT,
1027            "intermediate in-flight peak {peak} exceeds cap {MAX_INTERMEDIATE_IN_FLIGHT}"
1028        );
1029    }
1030
1031    /// A multi-level frontier (intermediates whose children are themselves
1032    /// intermediates) must still stream in order under a tight window without
1033    /// deadlocking: the undescended-offset gate keeps buffered leaves a
1034    /// contiguous emittable prefix, so the head is never starved.
1035    #[test]
1036    fn windowed_deep_frontier_in_order_and_bounded() {
1037        let leaves = 2000usize;
1038        let window = 5usize;
1039        let data = tiny_deep_data(leaves);
1040        let (root, store) = split::<TINY_BODY>(&data).unwrap();
1041        let probe = TinyOrderProbe::new(store.into_chunks(), &data);
1042
1043        PEAK_LEAF_OCCUPANCY.with(|p| p.set(0));
1044        block_on(async {
1045            let joiner = Joiner::<_, TINY_BODY>::new(probe.clone(), root)
1046                .await
1047                .unwrap()
1048                .with_concurrency(leaves);
1049            probe.reset();
1050            let mut reader = joiner.into_windowed_reader(window);
1051            let stream = reader.stream();
1052            futures::pin_mut!(stream);
1053            let mut got = Vec::new();
1054            while let Some(item) = stream.next().await {
1055                got.extend_from_slice(&item.unwrap());
1056            }
1057            assert_eq!(got, data, "deep frontier still yields whole file in order");
1058        });
1059
1060        let occupancy = PEAK_LEAF_OCCUPANCY.with(std::cell::Cell::get);
1061        assert!(
1062            occupancy <= window,
1063            "leaf-body occupancy peak {occupancy} exceeds window {window}"
1064        );
1065        let peak = probe.peak_intermediate.load(Ordering::SeqCst);
1066        assert!(
1067            peak <= MAX_INTERMEDIATE_IN_FLIGHT,
1068            "intermediate in-flight peak {peak} exceeds cap {MAX_INTERMEDIATE_IN_FLIGHT}"
1069        );
1070    }
1071
1072    #[cfg(feature = "encryption")]
1073    #[test]
1074    fn head_slowest_encrypted_in_order_and_bounded() {
1075        // Encrypted leaf bodies are shorter than BODY_SIZE, so the slide must
1076        // advance by body.len(), not the stride. Encrypted leaf addresses hash
1077        // the ciphertext and are not reconstructible from plaintext here, so the
1078        // head cannot be singled out; a uniform delay across all fetches still
1079        // overlaps concurrently admitted leaves, and the occupancy hook proves a
1080        // correct walk holds at most `window` leaf bodies on short bodies.
1081        use crate::file::EncryptedJoiner;
1082        use crate::file::split_encrypted;
1083
1084        let leaves = 30usize;
1085        let window = 5usize;
1086        let total = (DEFAULT_BODY_SIZE * leaves) as u64;
1087        let data: Vec<u8> = (0..total).map(unique_byte).collect();
1088        let (root_ref, store) = split_encrypted::<DEFAULT_BODY_SIZE>(&data).unwrap();
1089        let store = store.into_chunks();
1090
1091        PEAK_LEAF_OCCUPANCY.with(|p| p.set(0));
1092        let getter = UniformSlow {
1093            chunks: Arc::new(store),
1094        };
1095
1096        block_on(async {
1097            let joiner = EncryptedJoiner::new(getter, root_ref)
1098                .await
1099                .unwrap()
1100                .with_concurrency(leaves);
1101            let mut reader = joiner.into_windowed_reader(window);
1102            let stream = reader.stream();
1103            futures::pin_mut!(stream);
1104            let mut out = Vec::new();
1105            while let Some(item) = stream.next().await {
1106                out.extend_from_slice(&item.unwrap());
1107            }
1108            assert_eq!(out, data, "encrypted short-body slide stays in order");
1109        });
1110
1111        let peak = PEAK_LEAF_OCCUPANCY.with(std::cell::Cell::get);
1112        assert!(
1113            peak <= window,
1114            "encrypted leaf-body occupancy peak {peak} exceeds window {window}"
1115        );
1116    }
1117
1118    /// A getter that holds every fetch open across several self-waking yields so
1119    /// concurrently admitted leaves overlap. Used where addresses cannot be
1120    /// mapped to offsets (encrypted leaves), so the head cannot be singled out;
1121    /// the occupancy hook still proves the window bound.
1122    #[cfg(feature = "encryption")]
1123    #[derive(Clone)]
1124    struct UniformSlow {
1125        chunks: Arc<HashMap<ChunkAddress, AnyChunk>>,
1126    }
1127
1128    #[cfg(feature = "encryption")]
1129    impl ChunkGet<DEFAULT_BODY_SIZE> for UniformSlow {
1130        type Error = crate::store::ChunkStoreError;
1131
1132        async fn get(&self, address: &ChunkAddress) -> std::result::Result<AnyChunk, Self::Error> {
1133            for _ in 0..4 {
1134                yield_now().await;
1135            }
1136            self.chunks
1137                .get(address)
1138                .cloned()
1139                .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
1140        }
1141    }
1142
1143    #[cfg(feature = "encryption")]
1144    mod encrypted {
1145        use super::*;
1146        use crate::file::EncryptedJoiner;
1147        use crate::file::split_encrypted;
1148
1149        async fn drain_enc(
1150            reader: &mut WindowedReader<
1151                HashMap<ChunkAddress, AnyChunk>,
1152                crate::file::mode::EncryptedMode,
1153            >,
1154        ) -> Vec<u8> {
1155            let stream = reader.stream();
1156            futures::pin_mut!(stream);
1157            let mut out = Vec::new();
1158            while let Some(item) = stream.next().await {
1159                out.extend_from_slice(&item.unwrap());
1160            }
1161            out
1162        }
1163
1164        #[test]
1165        fn encrypted_stream_and_seek() {
1166            let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 321)
1167                .map(|i| (i % 256) as u8)
1168                .collect();
1169            let bs = DEFAULT_BODY_SIZE as u64;
1170            let (root_ref, store) = split_encrypted::<DEFAULT_BODY_SIZE>(&data).unwrap();
1171            let store = store.into_chunks();
1172            block_on(async {
1173                // whole file
1174                let joiner = EncryptedJoiner::new(store.clone(), root_ref.clone())
1175                    .await
1176                    .unwrap();
1177                let mut reader = joiner.into_windowed_reader(16);
1178                assert_eq!(drain_enc(&mut reader).await, data);
1179
1180                // mid-leaf and leaf-aligned suffixes
1181                for k in [bs + 10, bs, bs * 3] {
1182                    let joiner = EncryptedJoiner::new(store.clone(), root_ref.clone())
1183                        .await
1184                        .unwrap();
1185                    let mut reader = joiner.into_windowed_reader(16);
1186                    reader.seek(SeekFrom::Start(k)).unwrap();
1187                    assert_eq!(drain_enc(&mut reader).await, &data[k as usize..]);
1188                }
1189            });
1190        }
1191    }
1192
1193    #[cfg(feature = "tokio")]
1194    #[tokio::test]
1195    async fn reader_seek_and_read() {
1196        use tokio::io::{AsyncReadExt, AsyncSeekExt};
1197
1198        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3 + 50)
1199            .map(|i| (i % 256) as u8)
1200            .collect();
1201        let (root, store) = split_and_store(&data);
1202        let joiner = Joiner::new(store, root).await.unwrap();
1203        let mut reader = joiner.into_windowed_reader(16).into_reader();
1204
1205        let mut all = Vec::new();
1206        reader.read_to_end(&mut all).await.unwrap();
1207        assert_eq!(all, data);
1208
1209        reader
1210            .seek(SeekFrom::Start(DEFAULT_BODY_SIZE as u64))
1211            .await
1212            .unwrap();
1213        let mut tail = Vec::new();
1214        reader.read_to_end(&mut tail).await.unwrap();
1215        assert_eq!(tail, &data[DEFAULT_BODY_SIZE..]);
1216    }
1217}