1use crate::{DownloadResult, Event, ProgressEntry, Puller, PullerError, Pusher, WorkerId};
4use bytes::Bytes;
5use core::{
6 sync::atomic::{AtomicU64, AtomicUsize, Ordering},
7 time::Duration,
8};
9use crossfire::{MAsyncTx, MTx, WeakTx, mpmc, mpsc};
10use fast_steal::{Executor, Handle, Task, TaskQueue};
11use futures::TryStreamExt;
12use std::sync::{Arc, OnceLock};
13use tokio_util::sync::CancellationToken;
14
15#[derive(Debug, Clone)]
19pub struct DownloadOptions<I: Iterator<Item = ProgressEntry>> {
20 pub download_chunks: I,
22 pub concurrent: usize,
24 pub retry_gap: Duration,
26 pub pull_timeout: Duration,
28 pub push_queue_cap: usize,
30 pub min_chunk_size: u64,
32 pub max_speculative: usize,
34}
35
36pub fn download_multi<R: Puller, W: Pusher, I: Iterator<Item = ProgressEntry>>(
38 puller: R,
39 mut pusher: W,
40 options: DownloadOptions<I>,
41) -> DownloadResult<TokioExecutor<R, W::Error>, R::Error, W::Error> {
42 let token = CancellationToken::new();
43 let (tx, event_chain) = mpmc::unbounded_async();
44 pusher.set_listener({
45 let tx = tx.clone();
46 Box::new(move |p| {
47 let _ = tx.send(Event::PushProgress(p));
48 })
49 });
50 let (tx_push, rx_push) =
51 mpsc::bounded_async_blocking::<(WorkerId, ProgressEntry, Bytes)>(options.push_queue_cap);
52
53 let push_thread = Arc::new(OnceLock::new());
54 let push_handle = tokio::task::spawn_blocking({
55 let push_thread = push_thread.clone();
56 let token = token.clone();
57 let tx = tx.clone();
58 move || {
59 let _ = push_thread.set(std::thread::current());
60 while let Ok((id, mut spin, mut data)) = rx_push.recv() {
61 loop {
62 if token.is_cancelled() {
63 return;
64 }
65 let _ = tx.send(Event::Pushing(id, spin.clone()));
66 let len_before_push = data.len();
67 match pusher.push(&spin, data) {
68 Ok(()) => break,
69 Err((err, bytes)) => {
70 let _ = tx.send(Event::PushError(id, spin.clone(), err));
71 let written = len_before_push.saturating_sub(bytes.len());
72 data = bytes;
73 spin.start += written as u64;
74 }
75 }
76 std::thread::park_timeout(options.retry_gap);
77 }
78 }
79 loop {
80 if token.is_cancelled() {
81 return;
82 }
83 let _ = tx.send(Event::Flushing);
84 match pusher.flush() {
85 Ok(()) => break,
86 Err(err) => {
87 let _ = tx.send(Event::FlushError(err));
88 }
89 }
90 std::thread::park_timeout(options.retry_gap);
91 }
92 }
93 });
94 tokio::spawn({
95 let token = token.clone();
96 async move {
97 tokio::select! {
98 _ = push_handle => {},
99 () = token.cancelled() => {
100 if let Some(t) = push_thread.get() {
101 t.unpark();
102 }
103 }
104 }
105 }
106 });
107
108 let min_chunk_size = Arc::new(AtomicU64::new(options.min_chunk_size.max(1)));
109 let executor: TokioExecutor<R, W::Error> = TokioExecutor {
110 token: token.clone(),
111 tx: tx.downgrade(),
112 tx_push: tx_push.downgrade(),
113 puller,
114 id: AtomicUsize::new(0),
115 retry_gap: options.retry_gap,
116 pull_timeout: options.pull_timeout,
117 min_chunk_size: min_chunk_size.clone(),
118 max_speculative: options.max_speculative,
119 };
120 let task_queue = TaskQueue::new(options.download_chunks);
121 let _ = task_queue.set_threads(options.concurrent, options.min_chunk_size, Some(&executor));
122
123 DownloadResult::new(
124 event_chain,
125 Some((executor, task_queue)),
126 token,
127 Some(min_chunk_size),
128 )
129}
130
131#[derive(Debug, Clone)]
134pub struct TokioHandle {
135 id: usize,
136 token: CancellationToken,
137}
138impl Handle for TokioHandle {
139 type Id = usize;
140 fn abort(&mut self) {
141 self.token.cancel();
142 }
143 fn is_self(&self, id: &Self::Id) -> bool {
144 self.id == *id
145 }
146}
147pub struct TokioExecutor<R, WE>
158where
159 R: Puller,
160 WE: Send + Unpin + 'static,
161{
162 tx: WeakTx<mpmc::List<Event<R::Error, WE>>>,
163 tx_push: WeakTx<mpsc::Array<(WorkerId, ProgressEntry, Bytes)>>,
164 token: CancellationToken,
169 puller: R,
170 retry_gap: Duration,
171 pull_timeout: Duration,
172 id: AtomicUsize,
173 min_chunk_size: Arc<AtomicU64>,
178 max_speculative: usize,
179}
180impl<R, WE> Executor for TokioExecutor<R, WE>
181where
182 R: Puller,
183 WE: Send + Unpin + 'static,
184{
185 type Handle = TokioHandle;
186 #[allow(clippy::too_many_lines)]
187 fn execute(&self, mut task: Task, task_queue: TaskQueue<Self::Handle>) -> Self::Handle {
188 let id = self.id.fetch_add(1, Ordering::SeqCst);
189 let token = self.token.child_token();
190
191 let tx: Option<MTx<_>> = self.tx.upgrade();
192 let tx_push: Option<MAsyncTx<_>> = self.tx_push.upgrade();
193 let (Some(tx), Some(tx_push)) = (tx, tx_push) else {
194 return TokioHandle { id, token };
195 };
196
197 let mut puller = self.puller.clone();
198 let min_chunk_size = self.min_chunk_size.clone();
199 let pull_timeout = self.pull_timeout;
200 let cfg_retry_gap = self.retry_gap;
201 let max_speculative = self.max_speculative;
202 let worker_token = token.clone();
203 tokio::spawn(async move {
204 'task: loop {
205 if worker_token.is_cancelled() {
206 break 'task;
207 }
208 let mut start = task.start();
209 if start >= task.end() {
210 let current_min = min_chunk_size.load(Ordering::Relaxed);
211 if task_queue.steal(&id, &mut task, current_min, max_speculative) {
212 continue 'task;
213 }
214 break 'task;
215 }
216 let _ = tx.send(Event::Pulling(id));
217 let download_range = start..task.end();
218 let mut stream = loop {
219 let t = tokio::select! {
220 () = worker_token.cancelled() => break 'task,
221 t = puller.pull(Some(&download_range)) => t
222 };
223 match t {
224 Ok(t) => break t,
225 Err((e, retry_gap)) => {
226 let _ = tx.send(Event::PullError(id, e));
227 tokio::select! {
228 () = worker_token.cancelled() => break 'task,
229 () = tokio::time::sleep(retry_gap.unwrap_or(cfg_retry_gap)) => {}
230 };
231 }
232 }
233 };
234 loop {
235 let t = tokio::select! {
236 () = worker_token.cancelled() => break 'task,
237 () = tokio::time::sleep(pull_timeout) => {
238 let _ = tx.send(Event::PullTimeout(id));
239 drop(stream);
240 puller = puller.clone();
241 continue 'task;
242 },
243 t = stream.try_next() => t,
244 };
245 match t {
246 Ok(Some(mut chunk)) => {
247 if chunk.is_empty() {
248 continue;
249 }
250 let len = chunk.len() as u64;
251 let Ok(span) = task.safe_add_start(start, len) else {
252 start += len;
253 continue;
254 };
255 if span.end >= task.end() {
256 task_queue.cancel_task(&task, &id);
257 }
258 #[allow(clippy::cast_possible_truncation)]
259 let slice_span =
260 (span.start - start) as usize..(span.end - start) as usize;
261 chunk = chunk.slice(slice_span);
262 start = span.end;
263 let _ = tx.send(Event::PullProgress(id, span.clone()));
264 let _ = tx_push.send((id, span, chunk)).await;
265 if start >= task.end() {
266 continue 'task;
267 }
268 }
269 Ok(None) => continue 'task,
270 Err((e, retry_gap)) => {
271 let is_irrecoverable = e.is_irrecoverable();
272 let _ = tx.send(Event::PullError(id, e));
273 tokio::select! {
274 () = worker_token.cancelled() => break 'task,
275 () = tokio::time::sleep(retry_gap.unwrap_or(cfg_retry_gap)) => {}
276 };
277 if is_irrecoverable {
278 continue 'task;
279 }
280 }
281 }
282 }
283 }
284 let _ = tx.send(Event::Finished(id));
285 });
286 TokioHandle { id, token }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 #![allow(clippy::cast_possible_truncation)]
293 use vec::Vec;
294
295 use super::*;
296 use crate::{BufWriterPusher, CacheSeqPusher, PullResult};
297 use crate::{
298 MemPusher, Merge, ProgressEntry,
299 mock::{MockPuller, build_mock_data},
300 };
301 use futures::{StreamExt, stream};
302 use std::{dbg, vec};
303 use tokio::sync::watch;
304 use tokio::time::{sleep, timeout};
305
306 #[tokio::test(flavor = "multi_thread")]
307 async fn test_concurrent_download() {
308 let mock_data = build_mock_data(3 * 1024);
309 let puller = MockPuller::new(&mock_data);
310 let pusher = MemPusher::with_capacity(mock_data.len());
311 let receive = pusher.receive.clone();
316 #[allow(clippy::single_range_in_vec_init)]
317 let download_chunks = [0..mock_data.len() as u64];
318 let result = download_multi(
319 puller,
320 pusher,
321 DownloadOptions {
322 concurrent: 32,
323 retry_gap: Duration::from_secs(1),
324 push_queue_cap: 1024,
325 download_chunks: download_chunks.iter().cloned(),
326 pull_timeout: Duration::from_secs(5),
327 min_chunk_size: 1,
328 max_speculative: 3,
329 },
330 );
331
332 let mut pull_progress: Vec<ProgressEntry> = Vec::new();
333 let mut push_progress: Vec<ProgressEntry> = Vec::new();
334 let mut pull_ids = [false; 32];
335 while let Ok(e) = result.event_chain().recv().await {
336 match e {
337 Event::PullProgress(id, p) => {
338 pull_ids[id] = true;
339 pull_progress.merge_progress(p);
340 }
341 Event::PushProgress(p) => push_progress.merge_progress(p),
342 _ => {}
343 }
344 }
345 dbg!(&pull_progress);
346 dbg!(&push_progress);
347 assert_eq!(pull_progress, download_chunks);
348 assert_eq!(push_progress, download_chunks);
349 assert!(pull_ids.iter().any(|x| *x));
350
351 assert_eq!(&**receive.lock(), mock_data);
352 }
353
354 #[tokio::test(flavor = "multi_thread")]
355 async fn test_concurrent_download_abort_discards() {
356 let mock_data = build_mock_data(3 * 1024);
357 let puller = MockPuller::new(&mock_data);
358 let pusher = MemPusher::with_capacity(mock_data.len());
359 let receive = pusher.receive.clone();
360 #[allow(clippy::single_range_in_vec_init)]
361 let download_chunks = [0..mock_data.len() as u64];
362 let result = download_multi(
363 puller,
364 pusher,
365 DownloadOptions {
366 concurrent: 32,
367 retry_gap: Duration::from_secs(1),
368 push_queue_cap: 1024,
369 download_chunks: download_chunks.iter().cloned(),
370 pull_timeout: Duration::from_secs(5),
371 min_chunk_size: 1,
372 max_speculative: 3,
373 },
374 );
375
376 result.abort();
379 assert!(result.is_aborted());
380
381 tokio::time::timeout(Duration::from_secs(10), drain(&result))
382 .await
383 .expect("event loop hung after abort");
384
385 let written = receive.lock().len();
386 assert!(
387 written <= mock_data.len(),
388 "abort must not write beyond the source"
389 );
390 }
391
392 #[derive(Debug, Clone)]
411 struct SlowMockPuller {
412 data: Arc<[u8]>,
413 delay: Duration,
414 }
415 impl Puller for SlowMockPuller {
416 type Error = std::convert::Infallible;
417 #[allow(clippy::cast_possible_truncation)]
418 fn pull(
419 &mut self,
420 range: Option<&ProgressEntry>,
421 ) -> impl Future<
422 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
423 > + Send {
424 type PullItem = PullResult<Bytes, std::convert::Infallible>;
425 let owned: Vec<u8> = match range {
426 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
427 None => self.data.to_vec(),
428 };
429 let delay = self.delay;
430 async move {
431 sleep(delay).await;
432 let items: Vec<PullItem> = owned
433 .chunks(2)
434 .map(|c| Ok(Bytes::from(c.to_vec())))
435 .collect();
436 Ok(stream::iter(items))
437 }
438 }
439 }
440
441 #[tokio::test(flavor = "multi_thread")]
442 async fn test_concurrent_download_abort_discards_buffered() {
443 let mock_data = build_mock_data(64 * 1024);
445 let puller = SlowMockPuller {
446 data: Arc::from(mock_data.as_slice()),
447 delay: Duration::from_millis(50),
448 };
449 let inner = MemPusher::with_capacity(mock_data.len());
456 let receive = inner.receive.clone();
457 let buf = BufWriterPusher::new(inner, mock_data.len() + 1);
458 let pusher = CacheSeqPusher::new(buf, mock_data.len() + 1, 0);
459 #[allow(clippy::single_range_in_vec_init)]
460 let download_chunks = [0..mock_data.len() as u64];
461 let result = download_multi(
462 puller,
463 pusher,
464 DownloadOptions {
465 concurrent: 32,
466 retry_gap: Duration::from_secs(1),
467 push_queue_cap: 1024,
468 download_chunks: download_chunks.iter().cloned(),
469 pull_timeout: Duration::from_secs(5),
470 min_chunk_size: 1,
471 max_speculative: 3,
472 },
473 );
474
475 let mut aborted = false;
477 while let Ok(e) = result.event_chain().recv().await {
478 if matches!(e, Event::Pushing(_, _)) {
479 result.abort();
480 assert!(result.is_aborted());
481 aborted = true;
482 break;
483 }
484 }
485 assert!(aborted, "expected a Pushing event before aborting");
486
487 timeout(Duration::from_secs(10), drain(&result))
488 .await
489 .expect("event loop hung after abort");
490
491 let written = receive.lock().len();
495 assert!(
496 written < mock_data.len(),
497 "abort must stop before the full source is written (got {written} of {})",
498 mock_data.len()
499 );
500 }
501
502 use parking_lot::Mutex;
509 use std::sync::Arc;
510 use std::sync::atomic::{AtomicBool, Ordering};
511
512 #[derive(Debug)]
513 struct FatalErr;
514 impl std::fmt::Display for FatalErr {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 f.write_str("fatal")
517 }
518 }
519 impl std::error::Error for FatalErr {}
520 impl crate::PullerError for FatalErr {
521 fn is_irrecoverable(&self) -> bool {
522 true
523 }
524 }
525
526 #[derive(Debug)]
527 struct RecoverableErr;
528 impl std::fmt::Display for RecoverableErr {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.write_str("recoverable")
531 }
532 }
533 impl std::error::Error for RecoverableErr {}
534 impl crate::PullerError for RecoverableErr {
535 fn is_irrecoverable(&self) -> bool {
536 false
537 }
538 }
539
540 struct FlakySink {
543 fail_push: Arc<AtomicBool>,
544 fail_flush: Arc<AtomicBool>,
545 receive: Arc<Mutex<Vec<u8>>>,
546 listener: Option<crate::ProgressListener>,
547 }
548 impl FlakySink {
549 fn new() -> Self {
550 Self {
551 fail_push: Arc::new(AtomicBool::new(false)),
552 fail_flush: Arc::new(AtomicBool::new(false)),
553 receive: Arc::new(Mutex::new(Vec::new())),
554 listener: None,
555 }
556 }
557 }
558 impl crate::Pusher for FlakySink {
559 type Error = std::io::Error;
560 fn set_listener(&mut self, cb: crate::ProgressListener) {
561 self.listener = Some(cb);
562 }
563 fn push(
564 &mut self,
565 range: &crate::ProgressEntry,
566 bytes: Bytes,
567 ) -> Result<(), (Self::Error, Bytes)> {
568 if self.fail_push.swap(false, Ordering::SeqCst) {
569 return Err((std::io::Error::other("push"), bytes));
570 }
571 let mut g = self.receive.lock();
572 if range.start as usize == g.len() {
573 g.extend_from_slice(&bytes);
574 } else {
575 if g.len() < range.end as usize {
576 g.resize(range.end as usize, 0);
577 }
578 g[range.start as usize..range.end as usize].copy_from_slice(&bytes);
579 }
580 drop(g);
581 if let Some(l) = &mut self.listener {
582 l(range.clone());
583 }
584 Ok(())
585 }
586 fn flush(&mut self) -> Result<(), Self::Error> {
587 if self.fail_flush.swap(false, Ordering::SeqCst) {
588 Err(std::io::Error::other("flush"))
589 } else {
590 Ok(())
591 }
592 }
593 }
594
595 #[derive(Debug, Clone)]
596 struct EmptyChunkPuller {
597 data: Arc<[u8]>,
598 }
599 impl crate::Puller for EmptyChunkPuller {
600 type Error = std::convert::Infallible;
601 fn pull(
602 &mut self,
603 range: Option<&crate::ProgressEntry>,
604 ) -> impl Future<
605 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
606 > + Send {
607 let data = match range {
608 Some(r) => &self.data[r.start as usize..r.end as usize],
609 None => &self.data,
610 };
611 let mut items: Vec<crate::PullResult<Bytes, std::convert::Infallible>> =
612 vec![Ok(Bytes::new())];
613 items.extend(data.chunks(2).map(|c| Ok(Bytes::copy_from_slice(c))));
614 std::future::ready(Ok(stream::iter(items)))
615 }
616 }
617
618 #[derive(Debug, Clone)]
619 struct PullErrOncePuller {
620 data: Arc<[u8]>,
621 failed: Arc<AtomicBool>,
622 }
623 impl crate::Puller for PullErrOncePuller {
624 type Error = RecoverableErr;
625 fn pull(
626 &mut self,
627 range: Option<&crate::ProgressEntry>,
628 ) -> impl Future<
629 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
630 > + Send {
631 if !self.failed.swap(true, Ordering::SeqCst) {
632 return std::future::ready(Err((RecoverableErr, Some(Duration::ZERO))));
633 }
634 let data = match range {
635 Some(r) => &self.data[r.start as usize..r.end as usize],
636 None => &self.data,
637 };
638 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
639 .chunks(2)
640 .map(|c| Ok(Bytes::copy_from_slice(c)))
641 .collect();
642 std::future::ready(Ok(stream::iter(items)))
643 }
644 }
645
646 #[derive(Debug, Clone)]
647 struct StreamErrOncePuller {
648 data: Arc<[u8]>,
649 failed: Arc<AtomicBool>,
650 }
651 impl crate::Puller for StreamErrOncePuller {
652 type Error = FatalErr;
653 fn pull(
654 &mut self,
655 range: Option<&crate::ProgressEntry>,
656 ) -> impl Future<
657 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
658 > + Send {
659 if !self.failed.swap(true, Ordering::SeqCst) {
660 let items: Vec<crate::PullResult<Bytes, FatalErr>> =
661 vec![Err((FatalErr, Some(Duration::ZERO)))];
662 return std::future::ready(Ok(stream::iter(items)));
663 }
664 let data = match range {
665 Some(r) => &self.data[r.start as usize..r.end as usize],
666 None => &self.data,
667 };
668 let items: Vec<crate::PullResult<Bytes, FatalErr>> = data
669 .chunks(2)
670 .map(|c| Ok(Bytes::copy_from_slice(c)))
671 .collect();
672 std::future::ready(Ok(stream::iter(items)))
673 }
674 }
675
676 #[derive(Debug, Clone)]
679 struct RecoverableStreamErrOncePuller {
680 data: Arc<[u8]>,
681 failed: Arc<AtomicBool>,
682 }
683 impl crate::Puller for RecoverableStreamErrOncePuller {
684 type Error = RecoverableErr;
685 fn pull(
686 &mut self,
687 range: Option<&crate::ProgressEntry>,
688 ) -> impl Future<
689 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
690 > + Send {
691 if !self.failed.swap(true, Ordering::SeqCst) {
692 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> =
693 vec![Err((RecoverableErr, Some(Duration::ZERO)))];
694 return std::future::ready(Ok(stream::iter(items)));
695 }
696 let data = match range {
697 Some(r) => &self.data[r.start as usize..r.end as usize],
698 None => &self.data,
699 };
700 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
701 .chunks(2)
702 .map(|c| Ok(Bytes::copy_from_slice(c)))
703 .collect();
704 std::future::ready(Ok(stream::iter(items)))
705 }
706 }
707
708 async fn drain<R, WE>(result: &DownloadResult<TokioExecutor<R, WE>, R::Error, WE>)
709 where
710 R: crate::Puller,
711 WE: Send + Unpin + 'static,
712 {
713 while result.event_chain().recv().await.is_ok() {}
714 }
715
716 #[tokio::test(flavor = "multi_thread")]
717 async fn test_multi_push_error_retries() {
718 let mock_data = build_mock_data(3 * 1024);
720 let puller = MockPuller::new(&mock_data);
721 let sink = FlakySink::new();
722 sink.fail_push.store(true, Ordering::SeqCst);
723 let receive = sink.receive.clone();
724 #[allow(clippy::single_range_in_vec_init)]
725 let download_chunks = [0..mock_data.len() as u64];
726 let result = download_multi(
727 puller,
728 sink,
729 DownloadOptions {
730 concurrent: 32,
731 retry_gap: Duration::ZERO,
732 push_queue_cap: 1024,
733 download_chunks: download_chunks.iter().cloned(),
734 pull_timeout: Duration::from_secs(5),
735 min_chunk_size: 1,
736 max_speculative: 3,
737 },
738 );
739 drain(&result).await;
740 assert_eq!(&**receive.lock(), mock_data);
741 }
742
743 #[tokio::test(flavor = "multi_thread")]
744 async fn test_multi_flush_error_retries() {
745 let mock_data = build_mock_data(3 * 1024);
747 let puller = MockPuller::new(&mock_data);
748 let sink = FlakySink::new();
749 sink.fail_flush.store(true, Ordering::SeqCst);
750 let receive = sink.receive.clone();
751 #[allow(clippy::single_range_in_vec_init)]
752 let download_chunks = [0..mock_data.len() as u64];
753 let result = download_multi(
754 puller,
755 sink,
756 DownloadOptions {
757 concurrent: 32,
758 retry_gap: Duration::ZERO,
759 push_queue_cap: 1024,
760 download_chunks: download_chunks.iter().cloned(),
761 pull_timeout: Duration::from_secs(5),
762 min_chunk_size: 1,
763 max_speculative: 3,
764 },
765 );
766 drain(&result).await;
767 assert_eq!(&**receive.lock(), mock_data);
768 }
769
770 #[tokio::test(flavor = "multi_thread")]
771 async fn test_multi_pull_error_retries() {
772 let mock_data = build_mock_data(3 * 1024);
774 let puller = PullErrOncePuller {
775 data: Arc::from(mock_data.as_slice()),
776 failed: Arc::new(AtomicBool::new(false)),
777 };
778 let pusher = MemPusher::with_capacity(mock_data.len());
779 let receive = pusher.receive.clone();
780 #[allow(clippy::single_range_in_vec_init)]
781 let download_chunks = [0..mock_data.len() as u64];
782 let result = download_multi(
783 puller,
784 pusher,
785 DownloadOptions {
786 concurrent: 32,
787 retry_gap: Duration::ZERO,
788 push_queue_cap: 1024,
789 download_chunks: download_chunks.iter().cloned(),
790 pull_timeout: Duration::from_secs(5),
791 min_chunk_size: 1,
792 max_speculative: 3,
793 },
794 );
795 drain(&result).await;
796 assert_eq!(&**receive.lock(), mock_data);
797 }
798
799 #[tokio::test(flavor = "multi_thread")]
800 async fn test_multi_empty_chunk_is_skipped() {
801 let mock_data = build_mock_data(3 * 1024);
803 let puller = EmptyChunkPuller {
804 data: Arc::from(mock_data.as_slice()),
805 };
806 let pusher = MemPusher::with_capacity(mock_data.len());
807 let receive = pusher.receive.clone();
808 #[allow(clippy::single_range_in_vec_init)]
809 let download_chunks = [0..mock_data.len() as u64];
810 let result = download_multi(
811 puller,
812 pusher,
813 DownloadOptions {
814 concurrent: 32,
815 retry_gap: Duration::ZERO,
816 push_queue_cap: 1024,
817 download_chunks: download_chunks.iter().cloned(),
818 pull_timeout: Duration::from_secs(5),
819 min_chunk_size: 1,
820 max_speculative: 3,
821 },
822 );
823 drain(&result).await;
824 assert_eq!(&**receive.lock(), mock_data);
825 }
826
827 #[tokio::test(flavor = "multi_thread")]
828 async fn test_multi_stream_error_irrecoverable_retries() {
829 let mock_data = build_mock_data(3 * 1024);
832 let puller = StreamErrOncePuller {
833 data: Arc::from(mock_data.as_slice()),
834 failed: Arc::new(AtomicBool::new(false)),
835 };
836 let pusher = MemPusher::with_capacity(mock_data.len());
837 let receive = pusher.receive.clone();
838 #[allow(clippy::single_range_in_vec_init)]
839 let download_chunks = [0..mock_data.len() as u64];
840 let result = download_multi(
841 puller,
842 pusher,
843 DownloadOptions {
844 concurrent: 32,
845 retry_gap: Duration::ZERO,
846 push_queue_cap: 1024,
847 download_chunks: download_chunks.iter().cloned(),
848 pull_timeout: Duration::from_secs(5),
849 min_chunk_size: 1,
850 max_speculative: 3,
851 },
852 );
853 drain(&result).await;
854 assert_eq!(&**receive.lock(), mock_data);
855 }
856
857 #[tokio::test(flavor = "multi_thread")]
858 async fn test_multi_stream_error_recoverable_retries() {
859 let mock_data = build_mock_data(3 * 1024);
863 let puller = RecoverableStreamErrOncePuller {
864 data: Arc::from(mock_data.as_slice()),
865 failed: Arc::new(AtomicBool::new(false)),
866 };
867 let pusher = MemPusher::with_capacity(mock_data.len());
868 let receive = pusher.receive.clone();
869 #[allow(clippy::single_range_in_vec_init)]
870 let download_chunks = [0..mock_data.len() as u64];
871 let result = download_multi(
872 puller,
873 pusher,
874 DownloadOptions {
875 concurrent: 32,
876 retry_gap: Duration::ZERO,
877 push_queue_cap: 1024,
878 download_chunks: download_chunks.iter().cloned(),
879 pull_timeout: Duration::from_secs(5),
880 min_chunk_size: 1,
881 max_speculative: 3,
882 },
883 );
884 drain(&result).await;
885 assert_eq!(&**receive.lock(), mock_data);
886 }
887
888 #[tokio::test]
889 async fn puller_and_error_coverage() {
890 assert_eq!(format!("{FatalErr}"), "fatal");
893 assert_eq!(format!("{RecoverableErr}"), "recoverable");
894
895 let mut empty = EmptyChunkPuller {
896 data: Arc::from(b"abcdef".as_slice()),
897 };
898 let _ = empty.pull(Some(&(0..2u64))).await;
899 let _ = empty.pull(None).await;
900
901 let mut pull_err = PullErrOncePuller {
902 data: Arc::from(b"abcdef".as_slice()),
903 failed: Arc::new(AtomicBool::new(false)),
904 };
905 let _ = pull_err.pull(Some(&(0..2u64))).await; let _ = pull_err.pull(None).await; let mut stream_err = StreamErrOncePuller {
909 data: Arc::from(b"abcdef".as_slice()),
910 failed: Arc::new(AtomicBool::new(false)),
911 };
912 let _ = stream_err.pull(Some(&(0..2u64))).await;
913 let _ = stream_err.pull(None).await;
914
915 let mut rec_stream_err = RecoverableStreamErrOncePuller {
916 data: Arc::from(b"abcdef".as_slice()),
917 failed: Arc::new(AtomicBool::new(false)),
918 };
919 let _ = rec_stream_err.pull(Some(&(0..2u64))).await; let _ = rec_stream_err.pull(Some(&(0..2u64))).await; let _ = rec_stream_err.pull(None).await; }
923
924 #[tokio::test]
925 async fn test_slow_mock_puller_none_range() {
926 let mut p = SlowMockPuller {
929 data: Arc::from(b"hello world".as_slice()),
930 delay: Duration::ZERO,
931 };
932 assert!(p.pull(None).await.is_ok());
933 let mut p2 = SlowMockPuller {
934 data: Arc::from(b"hello world".as_slice()),
935 delay: Duration::ZERO,
936 };
937 assert!(p2.pull(Some(&(0..5))).await.is_ok());
938 }
939
940 #[derive(Debug, Clone)]
945 struct TimeoutOncePuller {
946 data: Arc<[u8]>,
947 first: Arc<AtomicBool>,
948 }
949 impl crate::Puller for TimeoutOncePuller {
950 type Error = std::convert::Infallible;
951 fn pull(
952 &mut self,
953 range: Option<&crate::ProgressEntry>,
954 ) -> impl Future<
955 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
956 > + Send {
957 let is_first = !self.first.swap(true, Ordering::SeqCst);
958 let data: Vec<u8> = match range {
959 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
960 None => self.data.to_vec(),
961 };
962 async move {
963 if is_first {
964 let head = data.get(..2).unwrap_or(&data);
968 let items = vec![Ok(Bytes::copy_from_slice(head))];
969 let pending =
970 stream::pending::<crate::PullResult<Bytes, std::convert::Infallible>>();
971 Ok(stream::iter(items).chain(pending))
972 } else {
973 let items: Vec<crate::PullResult<Bytes, std::convert::Infallible>> = data
977 .chunks(2)
978 .map(|c| Ok(Bytes::copy_from_slice(c)))
979 .collect();
980 let pending =
981 stream::pending::<crate::PullResult<Bytes, std::convert::Infallible>>();
982 Ok(stream::iter(items).chain(pending))
983 }
984 }
985 }
986 }
987
988 #[tokio::test(flavor = "multi_thread")]
989 async fn test_multi_pull_timeout_recovers_by_repulling() {
990 let mock_data = build_mock_data(3 * 1024);
994 let puller = TimeoutOncePuller {
995 data: Arc::from(mock_data.as_slice()),
996 first: Arc::new(AtomicBool::new(false)),
997 };
998 let pusher = MemPusher::with_capacity(mock_data.len());
999 let receive = pusher.receive.clone();
1000 #[allow(clippy::single_range_in_vec_init)]
1001 let download_chunks = [0..mock_data.len() as u64];
1002 let result = download_multi(
1003 puller,
1004 pusher,
1005 DownloadOptions {
1006 concurrent: 32,
1007 retry_gap: Duration::ZERO,
1008 push_queue_cap: 1024,
1009 download_chunks: download_chunks.iter().cloned(),
1010 pull_timeout: Duration::from_millis(50),
1011 min_chunk_size: 1,
1012 max_speculative: 3,
1013 },
1014 );
1015 drain(&result).await;
1016 assert_eq!(&**receive.lock(), mock_data);
1017 }
1018
1019 #[tokio::test(flavor = "multi_thread")]
1020 async fn test_concurrent_download_empty_chunks() {
1021 let mock_data = build_mock_data(3 * 1024);
1025 let puller = MockPuller::new(&mock_data);
1026 let pusher = MemPusher::with_capacity(mock_data.len());
1027 let receive = pusher.receive.clone();
1028 let result = download_multi(
1029 puller,
1030 pusher,
1031 DownloadOptions {
1032 concurrent: 32,
1033 retry_gap: Duration::from_secs(1),
1034 push_queue_cap: 1024,
1035 download_chunks: std::iter::empty(),
1036 pull_timeout: Duration::from_secs(5),
1037 min_chunk_size: 1,
1038 max_speculative: 3,
1039 },
1040 );
1041 timeout(Duration::from_secs(10), drain(&result))
1042 .await
1043 .expect("event loop hung on empty chunks");
1044 assert_eq!(receive.lock().len(), 0);
1045 }
1046
1047 #[derive(Clone)]
1053 struct GatedPuller {
1054 data: Arc<[u8]>,
1055 gate: watch::Receiver<bool>,
1057 hold: watch::Receiver<bool>,
1059 }
1060
1061 impl crate::Puller for GatedPuller {
1062 type Error = std::convert::Infallible;
1063 fn pull(
1064 &mut self,
1065 range: Option<&crate::ProgressEntry>,
1066 ) -> impl Future<
1067 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
1068 > + Send {
1069 let bytes: Vec<u8> = match range {
1070 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
1071 None => self.data.to_vec(),
1072 };
1073 let slow = range.is_some_and(|r| *r == (64..128u64));
1076 let mut gate = self.gate.clone();
1077 let mut hold = self.hold.clone();
1078 async move {
1079 loop {
1080 if *gate.borrow_and_update() {
1081 break;
1082 }
1083 if gate.changed().await.is_err() {
1084 break;
1085 }
1086 }
1087 if slow {
1088 loop {
1089 if *hold.borrow_and_update() {
1090 break;
1091 }
1092 if hold.changed().await.is_err() {
1093 break;
1094 }
1095 }
1096 }
1097 let items: Vec<crate::PullResult<Bytes, std::convert::Infallible>> =
1098 vec![Ok(Bytes::from(bytes))];
1099 Ok(stream::iter(items))
1100 }
1101 }
1102 }
1103
1104 async fn steal_granularity_probe(v: u64) -> Vec<ProgressEntry> {
1113 let mock_data = build_mock_data(128);
1114 let data: Arc<[u8]> = Arc::from(mock_data.as_slice());
1115 let (gate_tx, gate_rx) = watch::channel(false);
1116 let (hold_tx, hold_rx) = watch::channel(false);
1117 let puller = GatedPuller {
1118 data,
1119 gate: gate_rx,
1120 hold: hold_rx,
1121 };
1122 let pusher = MemPusher::with_capacity(mock_data.len());
1123 let receive = pusher.receive.clone();
1124 #[allow(clippy::single_range_in_vec_init)]
1125 let download_chunks = [0..64u64, 64..128u64];
1126 let result = download_multi(
1127 puller,
1128 pusher,
1129 DownloadOptions {
1130 concurrent: 2,
1131 retry_gap: Duration::ZERO,
1132 push_queue_cap: 1024,
1133 download_chunks: download_chunks.iter().cloned(),
1134 pull_timeout: Duration::from_secs(10),
1135 min_chunk_size: 1,
1136 max_speculative: 1,
1137 },
1138 );
1139
1140 result.set_min_chunk_size(v);
1143 gate_tx.send(true).expect("open gate");
1144
1145 let mut ranges: Vec<ProgressEntry> = Vec::new();
1146 let mut released = false;
1147 let drained = timeout(Duration::from_secs(10), async {
1148 while let Ok(event) = result.event_chain().recv().await {
1149 if let Event::PullProgress(_, range) = &event {
1150 ranges.push(range.clone());
1151 }
1152 if matches!(event, Event::Finished(0)) && !released {
1154 released = true;
1155 let _ = hold_tx.send(true);
1156 }
1157 }
1158 })
1159 .await;
1160 assert!(drained.is_ok(), "the download hung for v={v}");
1161 assert_eq!(&**receive.lock(), mock_data, "bytes corrupted for v={v}");
1162 ranges.sort_by_key(|r| r.start);
1163 ranges
1164 }
1165
1166 #[tokio::test(flavor = "multi_thread")]
1170 async fn set_min_chunk_size_at_half_allows_one_split() {
1171 assert_eq!(
1172 steal_granularity_probe(32).await,
1173 vec![0..64u64, 64..96, 96..128]
1174 );
1175 }
1176
1177 #[tokio::test(flavor = "multi_thread")]
1180 async fn set_min_chunk_size_just_above_half_blocks_split() {
1181 assert_eq!(steal_granularity_probe(33).await, vec![0..64u64, 64..128]);
1182 }
1183
1184 #[tokio::test(flavor = "multi_thread")]
1186 async fn set_min_chunk_size_above_remainder_blocks_split() {
1187 assert_eq!(steal_granularity_probe(65).await, vec![0..64u64, 64..128]);
1188 }
1189
1190 #[tokio::test(flavor = "multi_thread")]
1195 async fn set_min_chunk_size_zero_is_clamped() {
1196 let spans = steal_granularity_probe(0).await;
1197 assert!(
1198 spans.iter().all(|r| r.start < r.end),
1199 "a zero minimum must not yield empty ranges: {spans:?}"
1200 );
1201 assert_eq!(spans.first().map(|r| r.start), Some(0));
1202 assert_eq!(spans.last().map(|r| r.end), Some(128));
1203 }
1204}