1use std::io::SeekFrom;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7const DEFAULT_ASYNC_CONCURRENCY: usize = 8;
9
10const DEFAULT_LEAF_RETRIES: u32 = 4;
13
14pub(crate) const MAX_INTERMEDIATE_IN_FLIGHT: usize = 4;
28
29#[cfg(feature = "tokio")]
30use bytes::Buf;
31use bytes::Bytes;
32use futures::stream::{self, FuturesUnordered, Stream, StreamExt};
33
34use crate::bmt::DEFAULT_BODY_SIZE;
35use crate::chunk::ChunkAddress;
36
37use super::error::{FileError, Result};
38use super::frontier::{
39 SubtreeNode, expand_frontier, frontier_seed, overlapping_children, read_subtree_bodies,
40};
41use super::mode::{JoinMode, PlainMode};
42use super::tree::{ChunkRange, TreeParams};
43use crate::store::{ChunkGet, MaybeSend};
44
45#[cfg(feature = "encryption")]
46use super::mode::EncryptedMode;
47
48pub struct GenericJoiner<G, M: JoinMode, const BODY_SIZE: usize = DEFAULT_BODY_SIZE>
50where
51 G: ChunkGet<BODY_SIZE>,
52{
53 getter: Arc<G>,
54 root: ChunkAddress,
55 context: M::JoinerContext,
56 span: u64,
57 tree: TreeParams<BODY_SIZE>,
58 subtrees: Vec<SubtreeNode<M>>,
60 position: u64,
61 concurrency: usize,
62 _mode: PhantomData<M>,
63}
64
65pub type Joiner<G, const BODY_SIZE: usize = DEFAULT_BODY_SIZE> =
67 GenericJoiner<G, PlainMode, BODY_SIZE>;
68
69#[cfg(feature = "encryption")]
71pub type EncryptedJoiner<G, const BODY_SIZE: usize = DEFAULT_BODY_SIZE> =
72 GenericJoiner<G, EncryptedMode, BODY_SIZE>;
73
74impl<G, M, const BODY_SIZE: usize> std::fmt::Debug for GenericJoiner<G, M, BODY_SIZE>
75where
76 G: ChunkGet<BODY_SIZE>,
77 M: JoinMode,
78{
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("GenericJoiner")
81 .field("root", &self.root)
82 .field("span", &self.span)
83 .field("position", &self.position)
84 .field("concurrency", &self.concurrency)
85 .finish_non_exhaustive()
86 }
87}
88
89async fn collect_subtree_bodies<G, M, const BODY_SIZE: usize>(
91 getter: &Arc<G>,
92 subtrees: Vec<SubtreeNode<M>>,
93 chunk_range: ChunkRange,
94 concurrency: usize,
95) -> Result<Vec<Bytes>>
96where
97 G: ChunkGet<BODY_SIZE>,
98 M: JoinMode + MaybeSend + Sync,
99{
100 let bodies: Vec<Bytes> = stream::iter(subtrees)
101 .map(|st| {
102 let getter = Arc::clone(getter);
103 async move { read_subtree_bodies::<G, M, BODY_SIZE>(&*getter, &st, &chunk_range).await }
104 })
105 .buffered(concurrency)
106 .collect::<Vec<_>>()
107 .await
108 .into_iter()
109 .collect::<Result<Vec<Vec<Bytes>>>>()?
110 .into_iter()
111 .flatten()
112 .collect();
113 Ok(bodies)
114}
115
116impl<G, M, const BODY_SIZE: usize> GenericJoiner<G, M, BODY_SIZE>
117where
118 G: ChunkGet<BODY_SIZE>,
119 M: JoinMode + MaybeSend + Sync,
120{
121 pub async fn new(getter: G, input: M::RootRef) -> Result<Self> {
123 const { super::constants::assert_valid_body_size::<BODY_SIZE>() };
124
125 let (root, span, context) =
126 super::mode::joiner_init::<M, G, BODY_SIZE>(&getter, input).await?;
127 let tree = TreeParams::<BODY_SIZE>::new(span);
128
129 let target = DEFAULT_ASYNC_CONCURRENCY * 2;
130 let full_range = tree.chunks_for_range(0, span);
131 let subtrees =
132 expand_frontier::<G, M, BODY_SIZE>(&getter, &root, &context, span, &full_range, target)
133 .await?;
134
135 Ok(Self {
136 getter: Arc::new(getter),
137 root,
138 context,
139 span,
140 tree,
141 subtrees,
142 position: 0,
143 concurrency: DEFAULT_ASYNC_CONCURRENCY,
144 _mode: PhantomData,
145 })
146 }
147
148 pub async fn open_streaming(getter: G, input: M::RootRef) -> Result<Self> {
165 const { super::constants::assert_valid_body_size::<BODY_SIZE>() };
166
167 let (root, span, context) =
168 super::mode::joiner_init::<M, G, BODY_SIZE>(&getter, input).await?;
169 let tree = TreeParams::<BODY_SIZE>::new(span);
170 let subtrees = vec![frontier_seed::<M>(&root, &context, span)];
171
172 Ok(Self {
173 getter: Arc::new(getter),
174 root,
175 context,
176 span,
177 tree,
178 subtrees,
179 position: 0,
180 concurrency: DEFAULT_ASYNC_CONCURRENCY,
181 _mode: PhantomData,
182 })
183 }
184
185 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
187 self.concurrency = concurrency.max(1);
188 self
189 }
190
191 #[inline]
193 pub const fn size(&self) -> u64 {
194 self.span
195 }
196
197 #[inline]
199 pub const fn position(&self) -> u64 {
200 self.position
201 }
202
203 #[inline]
205 pub const fn root(&self) -> &ChunkAddress {
206 &self.root
207 }
208
209 #[allow(dead_code, reason = "consumed by sibling-module joiner extensions")]
211 pub(crate) const fn getter(&self) -> &Arc<G> {
212 &self.getter
213 }
214 #[allow(dead_code, reason = "consumed by sibling-module joiner extensions")]
215 pub(crate) fn subtrees(&self) -> &[SubtreeNode<M>] {
216 &self.subtrees
217 }
218 #[allow(dead_code, reason = "consumed by sibling-module joiner extensions")]
219 pub(crate) const fn tree(&self) -> TreeParams<BODY_SIZE> {
220 self.tree
221 }
222 #[allow(dead_code, reason = "consumed by sibling-module joiner extensions")]
223 pub(crate) const fn concurrency(&self) -> usize {
224 self.concurrency
225 }
226 #[allow(dead_code, reason = "consumed by sibling-module joiner extensions")]
227 pub(crate) const fn context(&self) -> &M::JoinerContext {
228 &self.context
229 }
230
231 pub async fn read_range(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
233 Self::read_range_with(
234 &self.getter,
235 &self.subtrees,
236 &self.root,
237 &self.context,
238 self.span,
239 self.tree,
240 self.concurrency,
241 offset,
242 len,
243 )
244 .await
245 }
246
247 pub async fn read_all(&self) -> Result<Vec<u8>> {
249 self.read_range(0, self.span as usize).await
250 }
251
252 #[allow(
254 clippy::too_many_arguments,
255 reason = "internal helper threading already-decomposed reader state from two call sites"
256 )]
257 async fn read_range_with(
258 getter: &Arc<G>,
259 subtrees: &[SubtreeNode<M>],
260 root: &ChunkAddress,
261 context: &M::JoinerContext,
262 span: u64,
263 tree: TreeParams<BODY_SIZE>,
264 concurrency: usize,
265 offset: u64,
266 len: usize,
267 ) -> Result<Vec<u8>> {
268 use super::helpers::{ReadRangeCheck, validate_read_range};
269
270 let (offset, actual_len) = match validate_read_range::<BODY_SIZE>(offset, len, span) {
271 ReadRangeCheck::Empty => return Ok(Vec::new()),
272 ReadRangeCheck::SingleChunk { offset, actual_len } => {
273 let chunk = getter.get(root).await.map_err(FileError::getter)?;
274 let chunk = chunk.into_content().ok_or(FileError::InvalidChunkType {
275 type_name: "non-content",
276 })?;
277 let body = M::decode_body::<BODY_SIZE>(chunk, context, span)?;
278 let start = offset as usize;
279 let end = start + actual_len;
280 return Ok(body[start..end].to_vec());
281 }
282 ReadRangeCheck::MultiChunk { offset, actual_len } => (offset, actual_len),
283 };
284
285 let chunk_range = tree.chunks_for_range(offset, actual_len as u64);
286 let range_start_byte = chunk_range.start * BODY_SIZE as u64;
287 let range_end_byte = chunk_range.end * BODY_SIZE as u64;
288
289 let relevant: Vec<_> = subtrees
290 .iter()
291 .filter(|st| {
292 st.byte_offset < range_end_byte && st.byte_offset + st.span > range_start_byte
293 })
294 .cloned()
295 .collect();
296
297 let bodies =
298 collect_subtree_bodies::<G, M, BODY_SIZE>(getter, relevant, chunk_range, concurrency)
299 .await?;
300
301 Ok(super::tree::assemble_range(
302 &tree,
303 offset,
304 actual_len,
305 &chunk_range,
306 &bodies,
307 ))
308 }
309
310 pub fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
312 self.position = super::resolve_seek_position(pos, self.position, self.span)?;
313 Ok(self.position)
314 }
315
316 pub fn into_stream(self) -> impl Stream<Item = Result<Bytes>> {
318 let getter = self.getter;
319 let chunk_range = self.tree.chunks_for_range(0, self.span);
320
321 struct State<M: JoinMode> {
322 subtrees: std::vec::IntoIter<SubtreeNode<M>>,
323 pending: std::vec::IntoIter<Bytes>,
324 }
325
326 let state = State {
327 subtrees: self.subtrees.into_iter(),
328 pending: Vec::new().into_iter(),
329 };
330
331 stream::unfold(state, move |mut state| {
332 let getter = Arc::clone(&getter);
333 async move {
334 if let Some(body) = state.pending.next() {
336 return Some((Ok(body), state));
337 }
338
339 let st = state.subtrees.next()?;
341 match read_subtree_bodies::<G, M, BODY_SIZE>(&*getter, &st, &chunk_range).await {
342 Ok(bodies) => {
343 let mut iter = bodies.into_iter();
344 match iter.next() {
345 Some(first) => {
346 state.pending = iter;
347 Some((Ok(first), state))
348 }
349 None => Some((Ok(Bytes::new()), state)),
350 }
351 }
352 Err(e) => Some((Err(e), state)),
353 }
354 }
355 })
356 }
357
358 pub fn into_offset_stream(self) -> impl Stream<Item = Result<(u64, Bytes)>> {
370 let getter = self.getter;
371 let concurrency = self.concurrency;
372 let chunk_range = self.tree.chunks_for_range(0, self.span);
373
374 let subtrees = stream::iter(self.subtrees)
377 .map(move |st| {
378 let getter = Arc::clone(&getter);
379 async move {
380 let base = st.byte_offset;
381 let bodies =
382 read_subtree_bodies::<G, M, BODY_SIZE>(&*getter, &st, &chunk_range).await?;
383 Ok::<(u64, Vec<Bytes>), FileError>((base, bodies))
384 }
385 })
386 .buffer_unordered(concurrency.max(1));
387
388 struct State<S> {
391 subtrees: S,
392 base: u64,
393 leaf_index: usize,
394 pending: std::vec::IntoIter<Bytes>,
395 }
396
397 let state = State {
398 subtrees: Box::pin(subtrees),
399 base: 0,
400 leaf_index: 0,
401 pending: Vec::new().into_iter(),
402 };
403
404 stream::unfold(state, move |mut state| async move {
405 loop {
406 if let Some(body) = state.pending.next() {
409 let offset = state.base + state.leaf_index as u64 * BODY_SIZE as u64;
410 state.leaf_index += 1;
411 return Some((Ok((offset, body)), state));
412 }
413
414 match state.subtrees.next().await? {
416 Ok((base, bodies)) => {
417 state.base = base;
418 state.leaf_index = 0;
419 state.pending = bodies.into_iter();
420 }
422 Err(e) => return Some((Err(e), state)),
423 }
424 }
425 })
426 }
427
428 pub fn into_offset_stream_chunked(self) -> impl Stream<Item = Result<(u64, Bytes)>>
453 where
454 G: 'static,
455 {
456 let getter = self.getter;
457 let width = self.concurrency.max(1);
458 let chunk_range = self.tree.chunks_for_range(0, self.span);
459
460 struct Pending<M: JoinMode> {
463 node: SubtreeNode<M>,
464 retries: u32,
465 }
466
467 enum Resolved<M: JoinMode> {
469 Leaf(u64, Bytes),
471 Children(Vec<SubtreeNode<M>>),
473 Retry(Pending<M>),
475 Failed(FileError),
477 }
478
479 #[cfg(not(target_arch = "wasm32"))]
480 type BoxResolvedFuture<M> =
481 std::pin::Pin<Box<dyn std::future::Future<Output = Resolved<M>> + Send>>;
482 #[cfg(target_arch = "wasm32")]
483 type BoxResolvedFuture<M> =
484 std::pin::Pin<Box<dyn std::future::Future<Output = Resolved<M>>>>;
485
486 async fn fetch_one<G, M, const BS: usize>(
489 getter: &G,
490 chunk_range: &ChunkRange,
491 pending: Pending<M>,
492 ) -> Resolved<M>
493 where
494 G: ChunkGet<BS>,
495 M: JoinMode + MaybeSend + Sync,
496 {
497 let node = &pending.node;
498 let body = match super::mode::read_chunk_body::<M, G, BS>(
499 getter,
500 &node.addr,
501 &node.context,
502 node.span,
503 )
504 .await
505 {
506 Ok(body) => body,
507 Err(e) => {
508 if node.span <= BS as u64 && pending.retries > 0 {
509 return Resolved::Retry(Pending {
510 node: pending.node,
511 retries: pending.retries - 1,
512 });
513 }
514 return Resolved::Failed(e);
515 }
516 };
517
518 if node.span <= BS as u64 {
519 return Resolved::Leaf(node.byte_offset, body);
520 }
521 match overlapping_children::<M, BS>(&body, node, chunk_range) {
522 Ok(children) => Resolved::Children(children),
523 Err(e) => Resolved::Failed(e),
524 }
525 }
526
527 struct State<G, M: JoinMode, const BS: usize> {
533 getter: Arc<G>,
534 chunk_range: ChunkRange,
535 width: usize,
536 node_queue: std::collections::VecDeque<Pending<M>>,
537 leaf_queue: std::collections::VecDeque<Pending<M>>,
538 intermediate_in_flight: usize,
539 in_flight: FuturesUnordered<BoxResolvedFuture<M>>,
540 }
541
542 let mut node_queue = std::collections::VecDeque::new();
543 let mut leaf_queue = std::collections::VecDeque::new();
544 for st in self.subtrees {
545 let pending = Pending {
546 node: st,
547 retries: DEFAULT_LEAF_RETRIES,
548 };
549 if pending.node.span <= BODY_SIZE as u64 {
550 leaf_queue.push_back(pending);
551 } else {
552 node_queue.push_back(pending);
553 }
554 }
555
556 let state = State::<G, M, BODY_SIZE> {
557 getter,
558 chunk_range,
559 width,
560 node_queue,
561 leaf_queue,
562 intermediate_in_flight: 0,
563 in_flight: FuturesUnordered::new(),
564 };
565
566 stream::unfold(state, move |mut state| async move {
567 loop {
568 while state.in_flight.len() < state.width {
575 let leaf_ready = !state.leaf_queue.is_empty();
576 let can_admit_intermediate = state.intermediate_in_flight
577 < MAX_INTERMEDIATE_IN_FLIGHT
578 && !state.node_queue.is_empty();
579 let admit_intermediate = can_admit_intermediate
580 && (!leaf_ready || state.intermediate_in_flight + 1 < state.width);
581
582 let pending = if admit_intermediate {
583 state.intermediate_in_flight += 1;
584 state.node_queue.pop_front().expect("node queue non-empty")
585 } else if let Some(leaf) = state.leaf_queue.pop_front() {
586 leaf
587 } else {
588 break;
589 };
590
591 let getter = Arc::clone(&state.getter);
592 let range = state.chunk_range;
593 state.in_flight.push(Box::pin(async move {
594 fetch_one::<G, M, BODY_SIZE>(&*getter, &range, pending).await
595 }) as BoxResolvedFuture<M>);
596 }
597
598 let resolved = state.in_flight.next().await?;
600 match resolved {
601 Resolved::Leaf(offset, body) => {
602 return Some((Ok((offset, body)), state));
603 }
604 Resolved::Children(children) => {
605 state.intermediate_in_flight -= 1;
606 for child in children {
607 let pending = Pending {
608 node: child,
609 retries: DEFAULT_LEAF_RETRIES,
610 };
611 if pending.node.span <= BODY_SIZE as u64 {
612 state.leaf_queue.push_back(pending);
613 } else {
614 state.node_queue.push_back(pending);
615 }
616 }
617 }
618 Resolved::Retry(pending) => {
619 state.leaf_queue.push_back(pending);
622 }
623 Resolved::Failed(e) => return Some((Err(e), state)),
624 }
625 }
626 })
627 }
628
629 pub fn into_offset_stream_chunked_range(
640 self,
641 start: u64,
642 len: u64,
643 ) -> impl Stream<Item = Result<(u64, Bytes)>>
644 where
645 G: 'static,
646 {
647 chunked_range_stream_from::<G, M, BODY_SIZE>(
648 self.getter,
649 self.subtrees,
650 self.tree,
651 self.span,
652 self.concurrency,
653 start,
654 len,
655 )
656 }
657
658 #[cfg(feature = "tokio")]
660 pub fn into_reader(self) -> JoinerReader<G, M, BODY_SIZE> {
661 JoinerReader {
662 joiner: self,
663 buffer: Bytes::new(),
664 future: None,
665 }
666 }
667}
668
669pub(crate) fn chunked_range_stream_from<G, M, const BODY_SIZE: usize>(
679 getter: Arc<G>,
680 subtrees: Vec<SubtreeNode<M>>,
681 tree: TreeParams<BODY_SIZE>,
682 span: u64,
683 concurrency: usize,
684 start: u64,
685 len: u64,
686) -> impl Stream<Item = Result<(u64, Bytes)>> + 'static
687where
688 G: ChunkGet<BODY_SIZE> + 'static,
689 M: JoinMode + MaybeSend + Sync,
690{
691 let width = concurrency.max(1);
692
693 let range_start = start.min(span);
697 let range_end = (start.saturating_add(len)).min(span);
698 let chunk_range = tree.chunks_for_range(range_start, range_end - range_start);
699
700 struct Pending<M: JoinMode> {
703 node: SubtreeNode<M>,
704 retries: u32,
705 }
706
707 enum Resolved<M: JoinMode> {
709 Leaf(u64, Bytes),
711 Children(Vec<SubtreeNode<M>>),
713 Retry(Pending<M>),
715 Failed(FileError),
717 }
718
719 #[cfg(not(target_arch = "wasm32"))]
720 type BoxResolvedFuture<M> =
721 std::pin::Pin<Box<dyn std::future::Future<Output = Resolved<M>> + Send>>;
722 #[cfg(target_arch = "wasm32")]
723 type BoxResolvedFuture<M> = std::pin::Pin<Box<dyn std::future::Future<Output = Resolved<M>>>>;
724
725 async fn fetch_one<G, M, const BS: usize>(
729 getter: &G,
730 chunk_range: &ChunkRange,
731 pending: Pending<M>,
732 ) -> Resolved<M>
733 where
734 G: ChunkGet<BS>,
735 M: JoinMode + MaybeSend + Sync,
736 {
737 let node = &pending.node;
738 let body = match super::mode::read_chunk_body::<M, G, BS>(
739 getter,
740 &node.addr,
741 &node.context,
742 node.span,
743 )
744 .await
745 {
746 Ok(body) => body,
747 Err(e) => {
748 if node.span <= BS as u64 && pending.retries > 0 {
749 return Resolved::Retry(Pending {
750 node: pending.node,
751 retries: pending.retries - 1,
752 });
753 }
754 return Resolved::Failed(e);
755 }
756 };
757
758 if node.span <= BS as u64 {
759 return Resolved::Leaf(node.byte_offset, body);
760 }
761 match overlapping_children::<M, BS>(&body, node, chunk_range) {
762 Ok(children) => Resolved::Children(children),
763 Err(e) => Resolved::Failed(e),
764 }
765 }
766
767 struct State<G, M: JoinMode> {
772 getter: Arc<G>,
773 chunk_range: ChunkRange,
774 range_start: u64,
775 range_end: u64,
776 width: usize,
777 node_queue: std::collections::VecDeque<Pending<M>>,
778 leaf_queue: std::collections::VecDeque<Pending<M>>,
779 intermediate_in_flight: usize,
780 in_flight: FuturesUnordered<BoxResolvedFuture<M>>,
781 }
782
783 let mut node_queue = std::collections::VecDeque::new();
787 let mut leaf_queue = std::collections::VecDeque::new();
788 if range_end > range_start {
789 let range_start_byte = chunk_range.start * BODY_SIZE as u64;
790 let range_end_byte = chunk_range.end * BODY_SIZE as u64;
791 for st in subtrees {
792 if st.byte_offset < range_end_byte && st.byte_offset + st.span > range_start_byte {
793 let pending = Pending {
794 node: st,
795 retries: DEFAULT_LEAF_RETRIES,
796 };
797 if pending.node.span <= BODY_SIZE as u64 {
798 leaf_queue.push_back(pending);
799 } else {
800 node_queue.push_back(pending);
801 }
802 }
803 }
804 }
805
806 let state = State::<G, M> {
807 getter,
808 chunk_range,
809 range_start,
810 range_end,
811 width,
812 node_queue,
813 leaf_queue,
814 intermediate_in_flight: 0,
815 in_flight: FuturesUnordered::new(),
816 };
817
818 stream::unfold(state, move |mut state| async move {
819 loop {
820 while state.in_flight.len() < state.width {
823 let leaf_ready = !state.leaf_queue.is_empty();
824 let can_admit_intermediate = state.intermediate_in_flight
825 < MAX_INTERMEDIATE_IN_FLIGHT
826 && !state.node_queue.is_empty();
827 let admit_intermediate = can_admit_intermediate
828 && (!leaf_ready || state.intermediate_in_flight + 1 < state.width);
829
830 let pending = if admit_intermediate {
831 state.intermediate_in_flight += 1;
832 state.node_queue.pop_front().expect("node queue non-empty")
833 } else if let Some(leaf) = state.leaf_queue.pop_front() {
834 leaf
835 } else {
836 break;
837 };
838
839 let getter = Arc::clone(&state.getter);
840 let range = state.chunk_range;
841 state.in_flight.push(Box::pin(async move {
842 fetch_one::<G, M, BODY_SIZE>(&*getter, &range, pending).await
843 }) as BoxResolvedFuture<M>);
844 }
845
846 let resolved = state.in_flight.next().await?;
848 match resolved {
849 Resolved::Leaf(leaf_start, body) => {
850 let leaf_end = leaf_start + body.len() as u64;
855 if leaf_end <= state.range_start || leaf_start >= state.range_end {
856 continue;
857 }
858 let clip_lo = state.range_start.saturating_sub(leaf_start) as usize;
859 let clip_hi = (state.range_end - leaf_start).min(body.len() as u64) as usize;
860 let offset = leaf_start.max(state.range_start);
861 return Some((Ok((offset, body.slice(clip_lo..clip_hi))), state));
862 }
863 Resolved::Children(children) => {
864 state.intermediate_in_flight -= 1;
865 for child in children {
866 let pending = Pending {
867 node: child,
868 retries: DEFAULT_LEAF_RETRIES,
869 };
870 if pending.node.span <= BODY_SIZE as u64 {
871 state.leaf_queue.push_back(pending);
872 } else {
873 state.node_queue.push_back(pending);
874 }
875 }
876 }
877 Resolved::Retry(pending) => {
878 state.leaf_queue.push_back(pending);
881 }
882 Resolved::Failed(e) => return Some((Err(e), state)),
883 }
884 }
885 })
886}
887
888#[cfg(feature = "tokio")]
892pub struct JoinerReader<G, M: JoinMode, const BODY_SIZE: usize = DEFAULT_BODY_SIZE>
893where
894 G: ChunkGet<BODY_SIZE>,
895{
896 joiner: GenericJoiner<G, M, BODY_SIZE>,
897 buffer: Bytes,
898 #[allow(clippy::type_complexity)]
899 future: Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>>> + Send>>>,
900}
901
902#[cfg(feature = "tokio")]
903impl<G, M, const BODY_SIZE: usize> std::fmt::Debug for JoinerReader<G, M, BODY_SIZE>
904where
905 G: ChunkGet<BODY_SIZE>,
906 M: JoinMode,
907{
908 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
909 f.debug_struct("JoinerReader")
910 .field("joiner", &self.joiner)
911 .field("buffer_len", &self.buffer.len())
912 .field("has_pending_future", &self.future.is_some())
913 .finish()
914 }
915}
916
917#[cfg(feature = "tokio")]
920impl<G: ChunkGet<BODY_SIZE>, M: JoinMode, const BODY_SIZE: usize> Unpin
921 for JoinerReader<G, M, BODY_SIZE>
922{
923}
924
925#[cfg(feature = "tokio")]
926impl<G, M, const BODY_SIZE: usize> tokio::io::AsyncRead for JoinerReader<G, M, BODY_SIZE>
927where
928 G: ChunkGet<BODY_SIZE> + 'static,
929 M: JoinMode + Send + Sync + 'static,
930{
931 fn poll_read(
932 self: std::pin::Pin<&mut Self>,
933 cx: &mut std::task::Context<'_>,
934 buf: &mut tokio::io::ReadBuf<'_>,
935 ) -> std::task::Poll<std::io::Result<()>> {
936 use std::task::Poll;
937
938 let this = self.get_mut();
939
940 if !this.buffer.is_empty() {
942 let to_copy = this.buffer.len().min(buf.remaining());
943 buf.put_slice(&this.buffer[..to_copy]);
944 this.buffer.advance(to_copy);
945 return Poll::Ready(Ok(()));
946 }
947
948 if this.joiner.position >= this.joiner.span {
950 return Poll::Ready(Ok(()));
951 }
952
953 if this.future.is_none() {
955 let position = this.joiner.position;
956 let remaining = (this.joiner.span - position) as usize;
957 let read_len = remaining.min(BODY_SIZE);
958 let getter = Arc::clone(&this.joiner.getter);
959 let root = this.joiner.root;
960 let context = this.joiner.context.clone();
961 let span = this.joiner.span;
962 let tree = this.joiner.tree;
963 let concurrency = this.joiner.concurrency;
964 let subtrees: Vec<SubtreeNode<M>> = this.joiner.subtrees.clone();
965
966 let fut = async move {
967 GenericJoiner::<G, M, BODY_SIZE>::read_range_with(
968 &getter,
969 &subtrees,
970 &root,
971 &context,
972 span,
973 tree,
974 concurrency,
975 position,
976 read_len,
977 )
978 .await
979 };
980 this.future = Some(Box::pin(fut));
981 }
982
983 let fut = this.future.as_mut().unwrap();
985 match fut.as_mut().poll(cx) {
986 Poll::Ready(Ok(data)) => {
987 this.future = None;
988 let bytes = Bytes::from(data);
989 this.joiner.position += bytes.len() as u64;
990 let to_copy = bytes.len().min(buf.remaining());
991 buf.put_slice(&bytes[..to_copy]);
992 if to_copy < bytes.len() {
993 this.buffer = bytes.slice(to_copy..);
994 }
995 Poll::Ready(Ok(()))
996 }
997 Poll::Ready(Err(e)) => {
998 this.future = None;
999 Poll::Ready(Err(std::io::Error::other(e)))
1000 }
1001 Poll::Pending => Poll::Pending,
1002 }
1003 }
1004}
1005
1006#[cfg(feature = "tokio")]
1007impl<G, M, const BODY_SIZE: usize> tokio::io::AsyncSeek for JoinerReader<G, M, BODY_SIZE>
1008where
1009 G: ChunkGet<BODY_SIZE> + 'static,
1010 M: JoinMode + Send + Sync + 'static,
1011{
1012 fn start_seek(self: std::pin::Pin<&mut Self>, pos: SeekFrom) -> std::io::Result<()> {
1013 let this = self.get_mut();
1014 this.joiner.position =
1015 super::resolve_seek_position(pos, this.joiner.position, this.joiner.span)?;
1016 this.buffer = Bytes::new();
1017 this.future = None;
1018 Ok(())
1019 }
1020
1021 fn poll_complete(
1022 self: std::pin::Pin<&mut Self>,
1023 _cx: &mut std::task::Context<'_>,
1024 ) -> std::task::Poll<std::io::Result<u64>> {
1025 std::task::Poll::Ready(Ok(self.get_mut().joiner.position))
1026 }
1027}
1028
1029#[cfg(all(test, feature = "tokio"))]
1030mod tests {
1031 use super::*;
1032 use crate::chunk::AnyChunk;
1033 use crate::file::split;
1034 use std::collections::HashMap;
1035
1036 fn split_and_store(data: &[u8]) -> (ChunkAddress, HashMap<ChunkAddress, AnyChunk>) {
1037 let (root, store) = split::<DEFAULT_BODY_SIZE>(data).unwrap();
1038 (root, store.into_chunks())
1039 }
1040
1041 generate_plain_joiner_tests!(tokio::test, Joiner, [async], [await]);
1043
1044 use std::sync::atomic::{AtomicUsize, Ordering};
1047
1048 #[derive(Clone)]
1052 struct ProbeStore {
1053 inner: Arc<HashMap<ChunkAddress, AnyChunk>>,
1054 gets: Arc<AtomicUsize>,
1055 }
1056
1057 impl ChunkGet<DEFAULT_BODY_SIZE> for ProbeStore {
1058 type Error = crate::store::ChunkStoreError;
1059 async fn get(&self, address: &ChunkAddress) -> std::result::Result<AnyChunk, Self::Error> {
1060 self.gets.fetch_add(1, Ordering::SeqCst);
1061 self.inner
1062 .get(address)
1063 .cloned()
1064 .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
1065 }
1066 }
1067
1068 async fn drain_chunked_to_buf<G>(joiner: GenericJoiner<G, PlainMode>, total: usize) -> Vec<u8>
1069 where
1070 G: ChunkGet<DEFAULT_BODY_SIZE> + 'static,
1071 {
1072 let stream = joiner.into_offset_stream_chunked();
1073 futures::pin_mut!(stream);
1074 let mut buf = vec![0u8; total];
1075 while let Some(item) = stream.next().await {
1076 let (offset, body) = item.unwrap();
1077 let start = offset as usize;
1078 buf[start..start + body.len()].copy_from_slice(&body);
1079 }
1080 buf
1081 }
1082
1083 #[tokio::test]
1086 async fn streaming_open_assembles_byte_exact() {
1087 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 600 + 123)
1090 .map(|i| (i % 256) as u8)
1091 .collect();
1092 let (root, store) = split_and_store(&data);
1093
1094 let joiner = Joiner::open_streaming(store, root).await.unwrap();
1095 let total = joiner.size() as usize;
1096 let got = drain_chunked_to_buf(joiner, total).await;
1097 assert_eq!(got, data);
1098 }
1099
1100 #[tokio::test]
1104 async fn streaming_open_fetches_only_the_root() {
1105 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 600)
1106 .map(|i| (i % 256) as u8)
1107 .collect();
1108 let (root, store) = split_and_store(&data);
1109 let store = Arc::new(store);
1110
1111 let lazy_gets = Arc::new(AtomicUsize::new(0));
1112 let lazy = ProbeStore {
1113 inner: Arc::clone(&store),
1114 gets: Arc::clone(&lazy_gets),
1115 };
1116 let _joiner = Joiner::open_streaming(lazy, root).await.unwrap();
1117 assert_eq!(
1118 lazy_gets.load(Ordering::SeqCst),
1119 1,
1120 "lazy open fetches only the root"
1121 );
1122
1123 let eager_gets = Arc::new(AtomicUsize::new(0));
1124 let eager = ProbeStore {
1125 inner: store,
1126 gets: Arc::clone(&eager_gets),
1127 };
1128 let _joiner = Joiner::new(eager, root).await.unwrap();
1129 assert!(
1130 eager_gets.load(Ordering::SeqCst) > 1,
1131 "eager open pre-expands the frontier (root + intermediates)"
1132 );
1133 }
1134
1135 #[tokio::test]
1138 async fn streaming_open_is_byte_exact_under_latency() {
1139 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 300 + 7)
1140 .map(|i| (i % 256) as u8)
1141 .collect();
1142 let (root, store) = split_and_store(&data);
1143 let joiner = Joiner::open_streaming(store, root)
1144 .await
1145 .unwrap()
1146 .with_concurrency(4);
1147 let total = joiner.size() as usize;
1148 let got = drain_chunked_to_buf(joiner, total).await;
1149 assert_eq!(got, data);
1150 }
1151
1152 #[tokio::test]
1155 async fn test_joiner_stream() {
1156 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3)
1157 .map(|i| (i % 256) as u8)
1158 .collect();
1159 let (root, store) = split_and_store(&data);
1160
1161 let joiner = Joiner::new(store, root).await.unwrap();
1162 let chunks: Vec<Result<Bytes>> = joiner.into_stream().collect().await;
1163
1164 let mut recovered = Vec::new();
1165 for chunk in chunks {
1166 recovered.extend_from_slice(&chunk.unwrap());
1167 }
1168 assert_eq!(recovered, data);
1169 }
1170
1171 async fn assert_offset_stream_matches(data: &[u8]) {
1174 let (root, store) = split_and_store(data);
1175
1176 let expected = Joiner::new(store.clone(), root)
1177 .await
1178 .unwrap()
1179 .read_all()
1180 .await
1181 .unwrap();
1182
1183 let joiner = Joiner::new(store, root).await.unwrap();
1184 let total = joiner.size();
1185 let pairs: Vec<Result<(u64, Bytes)>> = joiner.into_offset_stream().collect().await;
1186
1187 let mut reassembled = vec![0u8; total as usize];
1188 let mut covered = 0u64;
1189 let mut seen_offsets = std::collections::HashSet::new();
1190 for pair in pairs {
1191 let (offset, body) = pair.unwrap();
1192 assert!(
1193 seen_offsets.insert(offset),
1194 "offset {offset} yielded more than once"
1195 );
1196 let start = offset as usize;
1197 let end = start + body.len();
1198 reassembled[start..end].copy_from_slice(&body);
1199 covered += body.len() as u64;
1200 }
1201
1202 assert_eq!(covered, total, "every byte covered exactly once");
1203 assert_eq!(reassembled, expected, "offset reassembly equals read_all");
1204 assert_eq!(reassembled, data, "offset reassembly equals input");
1205 }
1206
1207 #[tokio::test]
1208 async fn test_offset_stream_small() {
1209 assert_offset_stream_matches(b"hello world").await;
1210 }
1211
1212 #[tokio::test]
1213 async fn test_offset_stream_exact_chunk() {
1214 assert_offset_stream_matches(&vec![0xAB; DEFAULT_BODY_SIZE]).await;
1215 }
1216
1217 #[tokio::test]
1218 async fn test_offset_stream_multi_chunk() {
1219 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3 + 123)
1220 .map(|i| (i % 256) as u8)
1221 .collect();
1222 assert_offset_stream_matches(&data).await;
1223 }
1224
1225 #[tokio::test]
1226 async fn test_offset_stream_129_chunks() {
1227 let refs_per_chunk = DEFAULT_BODY_SIZE / super::super::constants::REF_SIZE;
1228 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * (refs_per_chunk + 1))
1229 .map(|i| (i % 256) as u8)
1230 .collect();
1231 assert_offset_stream_matches(&data).await;
1232 }
1233
1234 #[tokio::test]
1235 async fn test_offset_stream_concurrency_one() {
1236 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 7)
1239 .map(|i| (i % 256) as u8)
1240 .collect();
1241 let (root, store) = split_and_store(&data);
1242 let joiner = Joiner::new(store, root).await.unwrap().with_concurrency(1);
1243 let total = joiner.size();
1244 let mut reassembled = vec![0u8; total as usize];
1245 let stream = joiner.into_offset_stream();
1246 futures::pin_mut!(stream);
1247 while let Some(pair) = stream.next().await {
1248 let (offset, body) = pair.unwrap();
1249 let start = offset as usize;
1250 reassembled[start..start + body.len()].copy_from_slice(&body);
1251 }
1252 assert_eq!(reassembled, data);
1253 }
1254
1255 async fn assert_offset_stream_chunked_matches(data: &[u8]) {
1258 let (root, store) = split_and_store(data);
1259
1260 let expected = Joiner::new(store.clone(), root)
1261 .await
1262 .unwrap()
1263 .read_all()
1264 .await
1265 .unwrap();
1266
1267 let joiner = Joiner::new(store, root).await.unwrap();
1268 let total = joiner.size();
1269 let pairs: Vec<Result<(u64, Bytes)>> = joiner.into_offset_stream_chunked().collect().await;
1270
1271 let mut reassembled = vec![0u8; total as usize];
1272 let mut covered = 0u64;
1273 let mut seen_offsets = std::collections::HashSet::new();
1274 for pair in pairs {
1275 let (offset, body) = pair.unwrap();
1276 assert!(
1277 seen_offsets.insert(offset),
1278 "offset {offset} yielded more than once"
1279 );
1280 let start = offset as usize;
1281 let end = start + body.len();
1282 reassembled[start..end].copy_from_slice(&body);
1283 covered += body.len() as u64;
1284 }
1285
1286 assert_eq!(covered, total, "every byte covered exactly once");
1287 assert_eq!(reassembled, expected, "chunked reassembly equals read_all");
1288 assert_eq!(reassembled, data, "chunked reassembly equals input");
1289 }
1290
1291 #[tokio::test]
1292 async fn test_offset_stream_chunked_small() {
1293 assert_offset_stream_chunked_matches(b"hello world").await;
1294 }
1295
1296 #[tokio::test]
1297 async fn test_offset_stream_chunked_exact_chunk() {
1298 assert_offset_stream_chunked_matches(&vec![0xAB; DEFAULT_BODY_SIZE]).await;
1299 }
1300
1301 #[tokio::test]
1302 async fn test_offset_stream_chunked_multi_chunk() {
1303 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3 + 123)
1304 .map(|i| (i % 256) as u8)
1305 .collect();
1306 assert_offset_stream_chunked_matches(&data).await;
1307 }
1308
1309 #[tokio::test]
1310 async fn test_offset_stream_chunked_three_level_tree() {
1311 let refs_per_chunk = DEFAULT_BODY_SIZE / super::super::constants::REF_SIZE;
1314 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * (refs_per_chunk + 1))
1315 .map(|i| (i % 256) as u8)
1316 .collect();
1317 assert_offset_stream_chunked_matches(&data).await;
1318 }
1319
1320 #[tokio::test]
1321 async fn test_offset_stream_chunked_concurrency_one() {
1322 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 7)
1324 .map(|i| (i % 256) as u8)
1325 .collect();
1326 let (root, store) = split_and_store(&data);
1327 let joiner = Joiner::new(store, root).await.unwrap().with_concurrency(1);
1328 let total = joiner.size();
1329 let mut reassembled = vec![0u8; total as usize];
1330 let stream = joiner.into_offset_stream_chunked();
1331 futures::pin_mut!(stream);
1332 while let Some(pair) = stream.next().await {
1333 let (offset, body) = pair.unwrap();
1334 let start = offset as usize;
1335 reassembled[start..start + body.len()].copy_from_slice(&body);
1336 }
1337 assert_eq!(reassembled, data);
1338 }
1339
1340 #[derive(Clone)]
1344 struct ConcurrencyProbe {
1345 chunks: Arc<HashMap<ChunkAddress, AnyChunk>>,
1346 in_flight: Arc<std::sync::atomic::AtomicUsize>,
1347 max_in_flight: Arc<std::sync::atomic::AtomicUsize>,
1348 }
1349
1350 impl crate::store::ChunkGet<DEFAULT_BODY_SIZE> for ConcurrencyProbe {
1351 type Error = crate::store::ChunkStoreError;
1352
1353 async fn get(&self, address: &ChunkAddress) -> std::result::Result<AnyChunk, Self::Error> {
1354 use std::sync::atomic::Ordering;
1355 let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
1356 self.max_in_flight.fetch_max(now, Ordering::SeqCst);
1357 for _ in 0..8 {
1360 tokio::task::yield_now().await;
1361 }
1362 self.in_flight.fetch_sub(1, Ordering::SeqCst);
1363 self.chunks
1364 .get(address)
1365 .cloned()
1366 .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
1367 }
1368 }
1369
1370 #[tokio::test(flavor = "current_thread")]
1371 async fn test_offset_stream_chunked_per_chunk_concurrency() {
1372 let leaves = 40usize;
1377 let width = 16usize;
1378 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * leaves)
1379 .map(|i| (i % 256) as u8)
1380 .collect();
1381 let (root, store) = split_and_store(&data);
1382
1383 let probe = ConcurrencyProbe {
1384 chunks: Arc::new(store),
1385 in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1386 max_in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1387 };
1388 let max_seen = Arc::clone(&probe.max_in_flight);
1389
1390 let joiner = Joiner::new(probe, root)
1391 .await
1392 .unwrap()
1393 .with_concurrency(width);
1394
1395 let total = joiner.size();
1396 let mut reassembled = vec![0u8; total as usize];
1397 let stream = joiner.into_offset_stream_chunked();
1398 futures::pin_mut!(stream);
1399 while let Some(pair) = stream.next().await {
1400 let (offset, body) = pair.unwrap();
1401 let start = offset as usize;
1402 reassembled[start..start + body.len()].copy_from_slice(&body);
1403 }
1404 assert_eq!(reassembled, data, "concurrency probe still reassembles");
1405
1406 let peak = max_seen.load(std::sync::atomic::Ordering::SeqCst);
1407 assert!(
1408 peak >= width,
1409 "chunk-granular stream should reach width {width} in-flight, saw {peak}"
1410 );
1411 }
1412
1413 const TINY_BODY: usize = 256;
1420
1421 fn tiny_leaf_addresses(data: &[u8]) -> std::collections::HashSet<ChunkAddress> {
1424 use crate::chunk::Chunk;
1425 let mut set = std::collections::HashSet::new();
1426 for block in data.chunks(TINY_BODY) {
1427 let chunk = crate::chunk::ContentChunk::<TINY_BODY>::new(block.to_vec()).unwrap();
1428 set.insert(*chunk.address());
1429 }
1430 set
1431 }
1432
1433 #[derive(Clone)]
1437 struct OrderProbe {
1438 chunks: Arc<HashMap<ChunkAddress, AnyChunk<TINY_BODY>>>,
1439 leaves: Arc<std::collections::HashSet<ChunkAddress>>,
1440 kinds: Arc<std::sync::Mutex<Vec<bool>>>,
1442 intermediate_in_flight: Arc<std::sync::atomic::AtomicUsize>,
1443 peak_intermediate_in_flight: Arc<std::sync::atomic::AtomicUsize>,
1444 delivered_leaves: Arc<std::sync::atomic::AtomicUsize>,
1445 slow_addr: Option<ChunkAddress>,
1446 slow_gate: usize,
1447 }
1448
1449 impl OrderProbe {
1450 fn new(store: HashMap<ChunkAddress, AnyChunk<TINY_BODY>>, data: &[u8]) -> Self {
1451 Self {
1452 chunks: Arc::new(store),
1453 leaves: Arc::new(tiny_leaf_addresses(data)),
1454 kinds: Arc::new(std::sync::Mutex::new(Vec::new())),
1455 intermediate_in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1456 peak_intermediate_in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1457 delivered_leaves: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1458 slow_addr: None,
1459 slow_gate: 0,
1460 }
1461 }
1462
1463 fn reset(&self) {
1466 use std::sync::atomic::Ordering;
1467 self.kinds.lock().unwrap().clear();
1468 self.peak_intermediate_in_flight.store(0, Ordering::SeqCst);
1469 }
1470
1471 fn intermediates_before_first_leaf(&self) -> usize {
1474 let kinds = self.kinds.lock().unwrap();
1475 kinds.iter().take_while(|is_leaf| !**is_leaf).count()
1476 }
1477
1478 fn intermediate_fetches(&self) -> usize {
1479 self.kinds.lock().unwrap().iter().filter(|l| !**l).count()
1480 }
1481 }
1482
1483 impl crate::store::ChunkGet<TINY_BODY> for OrderProbe {
1484 type Error = crate::store::ChunkStoreError;
1485
1486 async fn get(
1487 &self,
1488 address: &ChunkAddress,
1489 ) -> std::result::Result<AnyChunk<TINY_BODY>, Self::Error> {
1490 use std::sync::atomic::Ordering;
1491 let is_leaf = self.leaves.contains(address);
1492 self.kinds.lock().unwrap().push(is_leaf);
1493 if !is_leaf {
1494 let now = self.intermediate_in_flight.fetch_add(1, Ordering::SeqCst) + 1;
1495 self.peak_intermediate_in_flight
1496 .fetch_max(now, Ordering::SeqCst);
1497 }
1498 if self.slow_addr == Some(*address) {
1499 while self.delivered_leaves.load(Ordering::SeqCst) < self.slow_gate {
1500 tokio::task::yield_now().await;
1501 }
1502 }
1503 for _ in 0..4 {
1504 tokio::task::yield_now().await;
1505 }
1506 if !is_leaf {
1507 self.intermediate_in_flight.fetch_sub(1, Ordering::SeqCst);
1508 }
1509 self.chunks
1510 .get(address)
1511 .cloned()
1512 .ok_or_else(|| crate::store::ChunkStoreError::not_found(address))
1513 }
1514 }
1515
1516 fn tiny_deep_data(leaves: usize) -> Vec<u8> {
1517 (0..TINY_BODY * leaves).map(|i| (i % 251) as u8).collect()
1518 }
1519
1520 #[tokio::test(flavor = "current_thread")]
1525 async fn test_offset_stream_chunked_first_leaf_before_frontier() {
1526 let data = tiny_deep_data(900);
1527 let (root, store) = split::<TINY_BODY>(&data).unwrap();
1528 let probe = OrderProbe::new(store.into_chunks(), &data);
1529
1530 let joiner = Joiner::<_, TINY_BODY>::new(probe.clone(), root)
1531 .await
1532 .unwrap();
1533 probe.reset();
1535
1536 let total = joiner.size();
1537 let mut reassembled = vec![0u8; total as usize];
1538 let stream = joiner.into_offset_stream_chunked();
1539 futures::pin_mut!(stream);
1540 while let Some(pair) = stream.next().await {
1541 let (offset, body) = pair.unwrap();
1542 reassembled[offset as usize..offset as usize + body.len()].copy_from_slice(&body);
1543 }
1544 assert_eq!(reassembled, data, "deep-tree reassembly is byte-exact");
1545
1546 let frontier = probe.intermediate_fetches();
1547 assert!(
1548 frontier >= 40,
1549 "test needs a frontier far larger than the cap, saw {frontier}"
1550 );
1551 let before = probe.intermediates_before_first_leaf();
1552 assert!(
1553 before <= 4 * MAX_INTERMEDIATE_IN_FLIGHT,
1554 "first leaf fetched after {before} intermediates (frontier {frontier}); \
1555 expected a short descent, not the whole frontier"
1556 );
1557 }
1558
1559 #[tokio::test(flavor = "current_thread")]
1561 async fn test_offset_stream_chunked_intermediate_cap() {
1562 let data = tiny_deep_data(900);
1563 let (root, store) = split::<TINY_BODY>(&data).unwrap();
1564 let probe = OrderProbe::new(store.into_chunks(), &data);
1565
1566 let joiner = Joiner::<_, TINY_BODY>::new(probe.clone(), root)
1567 .await
1568 .unwrap()
1569 .with_concurrency(16);
1570 probe.reset();
1571
1572 let stream = joiner.into_offset_stream_chunked();
1573 futures::pin_mut!(stream);
1574 while let Some(pair) = stream.next().await {
1575 pair.unwrap();
1576 }
1577
1578 let peak = probe
1579 .peak_intermediate_in_flight
1580 .load(std::sync::atomic::Ordering::SeqCst);
1581 assert!(
1582 peak <= MAX_INTERMEDIATE_IN_FLIGHT,
1583 "intermediate in-flight peak {peak} exceeds cap {MAX_INTERMEDIATE_IN_FLIGHT}"
1584 );
1585 }
1586
1587 #[tokio::test(flavor = "current_thread")]
1592 async fn test_offset_stream_chunked_slow_intermediate_does_not_stall() {
1593 let data = tiny_deep_data(900);
1594 let (root, store) = split::<TINY_BODY>(&data).unwrap();
1595 let store = store.into_chunks();
1596
1597 let leaves = tiny_leaf_addresses(&data);
1599 let slow = *store
1600 .keys()
1601 .find(|a| !leaves.contains(*a) && **a != root)
1602 .expect("an intermediate exists");
1603
1604 let mut probe = OrderProbe::new(store, &data);
1605 probe.slow_addr = Some(slow);
1606 probe.slow_gate = 100;
1607 let delivered = Arc::clone(&probe.delivered_leaves);
1608
1609 let joiner = Joiner::<_, TINY_BODY>::new(probe.clone(), root)
1610 .await
1611 .unwrap();
1612
1613 let total = joiner.size();
1614 let mut reassembled = vec![0u8; total as usize];
1615 let stream = joiner.into_offset_stream_chunked();
1616 futures::pin_mut!(stream);
1617 while let Some(pair) = stream.next().await {
1618 let (offset, body) = pair.unwrap();
1619 reassembled[offset as usize..offset as usize + body.len()].copy_from_slice(&body);
1620 delivered.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1621 }
1622 assert_eq!(
1623 reassembled, data,
1624 "stream completes past the slow intermediate"
1625 );
1626 }
1627
1628 async fn assert_offset_stream_chunked_range_matches(data: &[u8], start: u64, len: u64) {
1633 for width in [DEFAULT_ASYNC_CONCURRENCY, 1] {
1634 let (root, store) = split_and_store(data);
1635
1636 let expected = Joiner::new(store.clone(), root)
1637 .await
1638 .unwrap()
1639 .read_range(start, len as usize)
1640 .await
1641 .unwrap();
1642
1643 let joiner = Joiner::new(store, root)
1644 .await
1645 .unwrap()
1646 .with_concurrency(width);
1647 let pairs: Vec<Result<(u64, Bytes)>> = joiner
1648 .into_offset_stream_chunked_range(start, len)
1649 .collect()
1650 .await;
1651
1652 let mut reassembled = vec![0u8; expected.len()];
1653 let mut seen_offsets = std::collections::HashSet::new();
1654 for pair in pairs {
1655 let (offset, body) = pair.unwrap();
1656 assert!(
1657 offset >= start && offset + body.len() as u64 <= start + len,
1658 "offset {offset} (+{}) outside [{start}, {})",
1659 body.len(),
1660 start + len
1661 );
1662 assert!(
1663 seen_offsets.insert(offset),
1664 "offset {offset} yielded more than once (width {width})"
1665 );
1666 let rel = (offset - start) as usize;
1667 reassembled[rel..rel + body.len()].copy_from_slice(&body);
1668 }
1669
1670 assert_eq!(
1671 reassembled, expected,
1672 "range reassembly equals read_range (width {width}, start {start}, len {len})"
1673 );
1674 }
1675 }
1676
1677 #[tokio::test]
1678 async fn test_offset_stream_chunked_range_windows() {
1679 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 321)
1680 .map(|i| (i % 256) as u8)
1681 .collect();
1682 let bs = DEFAULT_BODY_SIZE as u64;
1683 let total = data.len() as u64;
1684
1685 assert_offset_stream_chunked_range_matches(&data, bs + 10, 50).await;
1687 assert_offset_stream_chunked_range_matches(&data, bs, bs).await;
1689 assert_offset_stream_chunked_range_matches(&data, bs / 2, bs * 3 + 7).await;
1691 assert_offset_stream_chunked_range_matches(&data, bs * 5, total - bs * 5).await;
1693 assert_offset_stream_chunked_range_matches(&data, 0, total).await;
1695 assert_offset_stream_chunked_range_matches(&data, bs, 0).await;
1697 }
1698
1699 #[tokio::test]
1700 async fn test_offset_stream_chunked_range_whole_equals_chunked() {
1701 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3 + 99)
1704 .map(|i| (i % 256) as u8)
1705 .collect();
1706 let (root, store) = split_and_store(&data);
1707
1708 let full = Joiner::new(store.clone(), root).await.unwrap();
1709 let total = full.size();
1710 let mut from_full: Vec<(u64, Vec<u8>)> = full
1711 .into_offset_stream_chunked()
1712 .collect::<Vec<_>>()
1713 .await
1714 .into_iter()
1715 .map(|p| {
1716 let (o, b) = p.unwrap();
1717 (o, b.to_vec())
1718 })
1719 .collect();
1720 from_full.sort_by_key(|(o, _)| *o);
1721
1722 let ranged = Joiner::new(store, root).await.unwrap();
1723 let mut from_range: Vec<(u64, Vec<u8>)> = ranged
1724 .into_offset_stream_chunked_range(0, total)
1725 .collect::<Vec<_>>()
1726 .await
1727 .into_iter()
1728 .map(|p| {
1729 let (o, b) = p.unwrap();
1730 (o, b.to_vec())
1731 })
1732 .collect();
1733 from_range.sort_by_key(|(o, _)| *o);
1734
1735 assert_eq!(
1736 from_range, from_full,
1737 "whole-file range equals chunked walk"
1738 );
1739
1740 let expected = Joiner::new(split_and_store(&data).1, root)
1742 .await
1743 .unwrap()
1744 .read_all()
1745 .await
1746 .unwrap();
1747 let mut reassembled = vec![0u8; total as usize];
1748 for (o, b) in &from_range {
1749 reassembled[*o as usize..*o as usize + b.len()].copy_from_slice(b);
1750 }
1751 assert_eq!(reassembled, expected);
1752 }
1753
1754 #[cfg(feature = "tokio")]
1755 #[tokio::test]
1756 async fn test_reader_small() {
1757 use tokio::io::AsyncReadExt;
1758
1759 let data = b"hello world";
1760 let (root, store) = split_and_store(data);
1761
1762 let joiner = Joiner::new(store, root).await.unwrap();
1763 let mut reader = joiner.into_reader();
1764 let mut result = Vec::new();
1765 reader.read_to_end(&mut result).await.unwrap();
1766 assert_eq!(result, data);
1767 }
1768
1769 #[cfg(feature = "tokio")]
1770 #[tokio::test]
1771 async fn test_reader_multi_chunk() {
1772 use tokio::io::AsyncReadExt;
1773
1774 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3 + 123)
1775 .map(|i| (i % 256) as u8)
1776 .collect();
1777 let (root, store) = split_and_store(&data);
1778
1779 let joiner = Joiner::new(store, root).await.unwrap();
1780 let mut reader = joiner.into_reader();
1781 let mut result = Vec::new();
1782 reader.read_to_end(&mut result).await.unwrap();
1783 assert_eq!(result, data);
1784 }
1785
1786 #[cfg(feature = "tokio")]
1787 #[tokio::test]
1788 async fn test_reader_seek() {
1789 use tokio::io::{AsyncReadExt, AsyncSeekExt};
1790
1791 let data = b"hello world";
1792 let (root, store) = split_and_store(data);
1793
1794 let joiner = Joiner::new(store, root).await.unwrap();
1795 let mut reader = joiner.into_reader();
1796
1797 reader.seek(SeekFrom::Start(6)).await.unwrap();
1798 let mut buf = vec![0u8; 5];
1799 reader.read_exact(&mut buf).await.unwrap();
1800 assert_eq!(&buf, b"world");
1801 }
1802
1803 #[cfg(feature = "tokio")]
1804 #[tokio::test]
1805 async fn test_reader_seek_back_and_forth() {
1806 use tokio::io::{AsyncReadExt, AsyncSeekExt};
1807
1808 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3)
1809 .map(|i| (i % 256) as u8)
1810 .collect();
1811 let (root, store) = split_and_store(&data);
1812
1813 let joiner = Joiner::new(store, root).await.unwrap();
1814 let mut reader = joiner.into_reader();
1815
1816 reader
1818 .seek(SeekFrom::Start(DEFAULT_BODY_SIZE as u64))
1819 .await
1820 .unwrap();
1821 let mut buf1 = vec![0u8; 100];
1822 reader.read_exact(&mut buf1).await.unwrap();
1823 assert_eq!(&buf1, &data[DEFAULT_BODY_SIZE..DEFAULT_BODY_SIZE + 100]);
1824
1825 reader.seek(SeekFrom::Start(0)).await.unwrap();
1827 let mut buf2 = vec![0u8; 100];
1828 reader.read_exact(&mut buf2).await.unwrap();
1829 assert_eq!(&buf2, &data[..100]);
1830
1831 reader.seek(SeekFrom::End(-50)).await.unwrap();
1833 let mut buf3 = vec![0u8; 50];
1834 reader.read_exact(&mut buf3).await.unwrap();
1835 assert_eq!(&buf3, &data[data.len() - 50..]);
1836 }
1837
1838 #[cfg(feature = "encryption")]
1839 mod encrypted {
1840 use super::*;
1841 use crate::file::split_encrypted;
1842
1843 fn encrypted_split_and_store(
1844 data: &[u8],
1845 ) -> (
1846 crate::chunk::encryption::EncryptedChunkRef,
1847 HashMap<ChunkAddress, AnyChunk>,
1848 ) {
1849 let (root_ref, store) = split_encrypted::<DEFAULT_BODY_SIZE>(data).unwrap();
1850 (root_ref, store.into_chunks())
1851 }
1852
1853 generate_encrypted_joiner_tests!(tokio::test, EncryptedJoiner, [async], [await]);
1855
1856 #[tokio::test]
1859 async fn test_encrypted_joiner_stream() {
1860 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 3)
1861 .map(|i| (i % 256) as u8)
1862 .collect();
1863 let (root_ref, store) = encrypted_split_and_store(&data);
1864
1865 let joiner = EncryptedJoiner::new(store, root_ref).await.unwrap();
1866 let chunks: Vec<Result<Bytes>> = joiner.into_stream().collect().await;
1867
1868 let mut recovered = Vec::new();
1869 for chunk in chunks {
1870 recovered.extend_from_slice(&chunk.unwrap());
1871 }
1872 assert_eq!(recovered, data);
1873 }
1874
1875 async fn assert_encrypted_range_matches(data: &[u8], start: u64, len: u64) {
1879 for width in [DEFAULT_ASYNC_CONCURRENCY, 1] {
1880 let (root_ref, store) = encrypted_split_and_store(data);
1881
1882 let expected = EncryptedJoiner::new(store.clone(), root_ref.clone())
1883 .await
1884 .unwrap()
1885 .read_range(start, len as usize)
1886 .await
1887 .unwrap();
1888
1889 let joiner = EncryptedJoiner::new(store, root_ref)
1890 .await
1891 .unwrap()
1892 .with_concurrency(width);
1893 let pairs: Vec<Result<(u64, Bytes)>> = joiner
1894 .into_offset_stream_chunked_range(start, len)
1895 .collect()
1896 .await;
1897
1898 let mut reassembled = vec![0u8; expected.len()];
1899 let mut seen_offsets = std::collections::HashSet::new();
1900 for pair in pairs {
1901 let (offset, body) = pair.unwrap();
1902 assert!(
1903 offset >= start && offset + body.len() as u64 <= start + len,
1904 "offset {offset} (+{}) outside [{start}, {})",
1905 body.len(),
1906 start + len
1907 );
1908 assert!(
1909 seen_offsets.insert(offset),
1910 "offset {offset} yielded more than once (width {width})"
1911 );
1912 let rel = (offset - start) as usize;
1913 reassembled[rel..rel + body.len()].copy_from_slice(&body);
1914 }
1915
1916 assert_eq!(
1917 reassembled, expected,
1918 "encrypted range equals read_range (width {width}, start {start}, len {len})"
1919 );
1920 }
1921 }
1922
1923 #[tokio::test]
1924 async fn test_encrypted_offset_stream_chunked_range_windows() {
1925 let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 321)
1926 .map(|i| (i % 256) as u8)
1927 .collect();
1928 let bs = DEFAULT_BODY_SIZE as u64;
1929 let total = data.len() as u64;
1930
1931 assert_encrypted_range_matches(&data, bs + 10, 50).await;
1933 assert_encrypted_range_matches(&data, bs, bs).await;
1935 assert_encrypted_range_matches(&data, bs / 2, bs * 3 + 7).await;
1937 assert_encrypted_range_matches(&data, bs * 5, total - bs * 5).await;
1939 assert_encrypted_range_matches(&data, 0, total).await;
1941 assert_encrypted_range_matches(&data, bs, 0).await;
1943 }
1944 }
1945}