1use 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
29const DEFAULT_LEAF_RETRIES: u32 = 4;
32
33#[cfg(test)]
34thread_local! {
35 static PEAK_LEAF_OCCUPANCY: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
40}
41
42#[cfg(test)]
44fn record_occupancy(occupancy: usize) {
45 PEAK_LEAF_OCCUPANCY.with(|p| p.set(p.get().max(occupancy)));
46}
47
48pub 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 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 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 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 #[inline]
141 pub const fn position(&self) -> u64 {
142 self.position
143 }
144
145 #[inline]
147 pub const fn size(&self) -> u64 {
148 self.span
149 }
150}
151
152#[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
158struct Pending<M: JoinMode> {
161 node: SubtreeNode<M>,
162 retries: u32,
163}
164
165enum Resolved<M: JoinMode> {
167 Leaf(u64, Bytes),
169 Children(u64, Vec<SubtreeNode<M>>),
172 Retry(Pending<M>),
174 Failed(FileError),
176}
177
178#[inline]
180fn is_leaf<M: JoinMode, const BS: usize>(node: &SubtreeNode<M>) -> bool {
181 node.span <= BS as u64
182}
183
184async 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
226fn 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
233fn 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 leaf_queue: VecDeque<Pending<M>>,
284 node_queue: VecDeque<Pending<M>>,
287 pending_intermediate_offsets: BTreeMap<u64, usize>,
291 intermediate_in_flight: usize,
293 leaf_in_flight: usize,
295 in_flight: FuturesUnordered<BoxResolvedFuture<M>>,
296 buffered: BTreeMap<u64, Bytes>,
297 }
298
299 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 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 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 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 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 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 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 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#[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 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 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 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 #[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 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 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 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 #[derive(Clone)]
786 struct HeadSlow {
787 chunks: Arc<HashMap<ChunkAddress, AnyChunk>>,
788 slow_offset: u64,
790 released: Arc<AtomicUsize>,
792 gate: usize,
794 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 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 fn unique_byte(i: u64) -> u8 {
833 (i.wrapping_mul(2_654_435_761) >> 11) as u8
834 }
835
836 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 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 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 #[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 #[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 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 #[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 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 #[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 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 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}