1use crate::{DownloadResult, Event, ProgressEntry, Puller, PullerError, Pusher, WorkerId};
4use bytes::Bytes;
5use core::{
6 sync::atomic::{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,
21 pub concurrent: usize,
22 pub retry_gap: Duration,
23 pub pull_timeout: Duration,
24 pub push_queue_cap: usize,
25 pub min_chunk_size: u64,
26 pub max_speculative: usize,
27}
28
29pub fn download_multi<R: Puller, W: Pusher, I: Iterator<Item = ProgressEntry>>(
30 puller: R,
31 mut pusher: W,
32 options: DownloadOptions<I>,
33) -> DownloadResult<TokioExecutor<R, W::Error>, R::Error, W::Error> {
34 let token = CancellationToken::new();
35 let (tx, event_chain) = mpmc::unbounded_async();
36 pusher.set_listener({
37 let tx = tx.clone();
38 Box::new(move |p| {
39 let _ = tx.send(Event::PushProgress(p));
40 })
41 });
42 let (tx_push, rx_push) =
43 mpsc::bounded_async_blocking::<(WorkerId, ProgressEntry, Bytes)>(options.push_queue_cap);
44
45 let push_thread = Arc::new(OnceLock::new());
46 let push_handle = tokio::task::spawn_blocking({
47 let push_thread = push_thread.clone();
48 let token = token.clone();
49 let tx = tx.clone();
50 move || {
51 let _ = push_thread.set(std::thread::current());
52 while let Ok((id, mut spin, mut data)) = rx_push.recv() {
53 loop {
54 if token.is_cancelled() {
55 return;
56 }
57 let _ = tx.send(Event::Pushing(id, spin.clone()));
58 let len_before_push = data.len();
59 match pusher.push(&spin, data) {
60 Ok(()) => break,
61 Err((err, bytes)) => {
62 let _ = tx.send(Event::PushError(id, spin.clone(), err));
63 let written = len_before_push.saturating_sub(bytes.len());
64 data = bytes;
65 spin.start += written as u64;
66 }
67 }
68 std::thread::park_timeout(options.retry_gap);
69 }
70 }
71 loop {
72 if token.is_cancelled() {
73 return;
74 }
75 let _ = tx.send(Event::Flushing);
76 match pusher.flush() {
77 Ok(()) => break,
78 Err(err) => {
79 let _ = tx.send(Event::FlushError(err));
80 }
81 }
82 std::thread::park_timeout(options.retry_gap);
83 }
84 }
85 });
86 tokio::spawn({
87 let token = token.clone();
88 async move {
89 tokio::select! {
90 _ = push_handle => {},
91 () = token.cancelled() => {
92 if let Some(t) = push_thread.get() {
93 t.unpark();
94 }
95 }
96 }
97 }
98 });
99
100 let executor: TokioExecutor<R, W::Error> = TokioExecutor {
101 token: token.clone(),
102 tx: tx.downgrade(),
103 tx_push: tx_push.downgrade(),
104 puller,
105 id: AtomicUsize::new(0),
106 retry_gap: options.retry_gap,
107 pull_timeout: options.pull_timeout,
108 min_chunk_size: options.min_chunk_size,
109 max_speculative: options.max_speculative,
110 };
111 let task_queue = TaskQueue::new(options.download_chunks);
112 let _ = task_queue.set_threads(options.concurrent, options.min_chunk_size, Some(&executor));
113
114 DownloadResult::new(event_chain, Some((executor, task_queue)), token)
115}
116
117#[derive(Debug, Clone)]
120pub struct TokioHandle {
121 id: usize,
122 token: CancellationToken,
123}
124impl Handle for TokioHandle {
125 type Id = usize;
126 fn abort(&mut self) {
127 self.token.cancel();
128 }
129 fn is_self(&self, id: &Self::Id) -> bool {
130 self.id == *id
131 }
132}
133pub struct TokioExecutor<R, WE>
144where
145 R: Puller,
146 WE: Send + Unpin + 'static,
147{
148 tx: WeakTx<mpmc::List<Event<R::Error, WE>>>,
149 tx_push: WeakTx<mpsc::Array<(WorkerId, ProgressEntry, Bytes)>>,
150 token: CancellationToken,
155 puller: R,
156 retry_gap: Duration,
157 pull_timeout: Duration,
158 id: AtomicUsize,
159 min_chunk_size: u64,
160 max_speculative: usize,
161}
162impl<R, WE> Executor for TokioExecutor<R, WE>
163where
164 R: Puller,
165 WE: Send + Unpin + 'static,
166{
167 type Handle = TokioHandle;
168 #[allow(clippy::too_many_lines)]
169 fn execute(&self, mut task: Task, task_queue: TaskQueue<Self::Handle>) -> Self::Handle {
170 let id = self.id.fetch_add(1, Ordering::SeqCst);
171 let token = self.token.child_token();
172
173 let tx: Option<MTx<_>> = self.tx.upgrade();
174 let tx_push: Option<MAsyncTx<_>> = self.tx_push.upgrade();
175 let (Some(tx), Some(tx_push)) = (tx, tx_push) else {
176 return TokioHandle { id, token };
177 };
178
179 let mut puller = self.puller.clone();
180 let min_chunk_size = self.min_chunk_size;
181 let pull_timeout = self.pull_timeout;
182 let cfg_retry_gap = self.retry_gap;
183 let max_speculative = self.max_speculative;
184 let worker_token = token.clone();
185 tokio::spawn(async move {
186 'task: loop {
187 if worker_token.is_cancelled() {
188 break 'task;
189 }
190 let mut start = task.start();
191 if start >= task.end() {
192 if task_queue.steal(&id, &mut task, min_chunk_size, max_speculative) {
193 continue 'task;
194 }
195 break 'task;
196 }
197 let _ = tx.send(Event::Pulling(id));
198 let download_range = start..task.end();
199 let mut stream = loop {
200 let t = tokio::select! {
201 () = worker_token.cancelled() => break 'task,
202 t = puller.pull(Some(&download_range)) => t
203 };
204 match t {
205 Ok(t) => break t,
206 Err((e, retry_gap)) => {
207 let _ = tx.send(Event::PullError(id, e));
208 tokio::select! {
209 () = worker_token.cancelled() => break 'task,
210 () = tokio::time::sleep(retry_gap.unwrap_or(cfg_retry_gap)) => {}
211 };
212 }
213 }
214 };
215 loop {
216 let t = tokio::select! {
217 () = worker_token.cancelled() => break 'task,
218 () = tokio::time::sleep(pull_timeout) => {
219 let _ = tx.send(Event::PullTimeout(id));
220 drop(stream);
221 puller = puller.clone();
222 continue 'task;
223 },
224 t = stream.try_next() => t,
225 };
226 match t {
227 Ok(Some(mut chunk)) => {
228 if chunk.is_empty() {
229 continue;
230 }
231 let len = chunk.len() as u64;
232 let Ok(span) = task.safe_add_start(start, len) else {
233 start += len;
234 continue;
235 };
236 if span.end >= task.end() {
237 task_queue.cancel_task(&task, &id);
238 }
239 #[allow(clippy::cast_possible_truncation)]
240 let slice_span =
241 (span.start - start) as usize..(span.end - start) as usize;
242 chunk = chunk.slice(slice_span);
243 start = span.end;
244 let _ = tx.send(Event::PullProgress(id, span.clone()));
245 let _ = tx_push.send((id, span, chunk)).await;
246 if start >= task.end() {
247 continue 'task;
248 }
249 }
250 Ok(None) => continue 'task,
251 Err((e, retry_gap)) => {
252 let is_irrecoverable = e.is_irrecoverable();
253 let _ = tx.send(Event::PullError(id, e));
254 tokio::select! {
255 () = worker_token.cancelled() => break 'task,
256 () = tokio::time::sleep(retry_gap.unwrap_or(cfg_retry_gap)) => {}
257 };
258 if is_irrecoverable {
259 continue 'task;
260 }
261 }
262 }
263 }
264 }
265 let _ = tx.send(Event::Finished(id));
266 });
267 TokioHandle { id, token }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 #![allow(clippy::cast_possible_truncation)]
274 use vec::Vec;
275
276 use super::*;
277 use crate::{BufWriterPusher, CacheSeqPusher, PullResult};
278 use crate::{
279 MemPusher, Merge, ProgressEntry,
280 mock::{MockPuller, build_mock_data},
281 };
282 use futures::{StreamExt, stream};
283 use std::{dbg, vec};
284 use tokio::time::{sleep, timeout};
285
286 #[tokio::test(flavor = "multi_thread")]
287 async fn test_concurrent_download() {
288 let mock_data = build_mock_data(3 * 1024);
289 let puller = MockPuller::new(&mock_data);
290 let pusher = MemPusher::with_capacity(mock_data.len());
291 let receive = pusher.receive.clone();
296 #[allow(clippy::single_range_in_vec_init)]
297 let download_chunks = [0..mock_data.len() as u64];
298 let result = download_multi(
299 puller,
300 pusher,
301 DownloadOptions {
302 concurrent: 32,
303 retry_gap: Duration::from_secs(1),
304 push_queue_cap: 1024,
305 download_chunks: download_chunks.iter().cloned(),
306 pull_timeout: Duration::from_secs(5),
307 min_chunk_size: 1,
308 max_speculative: 3,
309 },
310 );
311
312 let mut pull_progress: Vec<ProgressEntry> = Vec::new();
313 let mut push_progress: Vec<ProgressEntry> = Vec::new();
314 let mut pull_ids = [false; 32];
315 while let Ok(e) = result.event_chain().recv().await {
316 match e {
317 Event::PullProgress(id, p) => {
318 pull_ids[id] = true;
319 pull_progress.merge_progress(p);
320 }
321 Event::PushProgress(p) => push_progress.merge_progress(p),
322 _ => {}
323 }
324 }
325 dbg!(&pull_progress);
326 dbg!(&push_progress);
327 assert_eq!(pull_progress, download_chunks);
328 assert_eq!(push_progress, download_chunks);
329 assert!(pull_ids.iter().any(|x| *x));
330
331 assert_eq!(&**receive.lock(), mock_data);
332 }
333
334 #[tokio::test(flavor = "multi_thread")]
335 async fn test_concurrent_download_abort_discards() {
336 let mock_data = build_mock_data(3 * 1024);
337 let puller = MockPuller::new(&mock_data);
338 let pusher = MemPusher::with_capacity(mock_data.len());
339 let receive = pusher.receive.clone();
340 #[allow(clippy::single_range_in_vec_init)]
341 let download_chunks = [0..mock_data.len() as u64];
342 let result = download_multi(
343 puller,
344 pusher,
345 DownloadOptions {
346 concurrent: 32,
347 retry_gap: Duration::from_secs(1),
348 push_queue_cap: 1024,
349 download_chunks: download_chunks.iter().cloned(),
350 pull_timeout: Duration::from_secs(5),
351 min_chunk_size: 1,
352 max_speculative: 3,
353 },
354 );
355
356 result.abort();
359 assert!(result.is_aborted());
360
361 tokio::time::timeout(Duration::from_secs(10), drain(&result))
362 .await
363 .expect("event loop hung after abort");
364
365 let written = receive.lock().len();
366 assert!(
367 written <= mock_data.len(),
368 "abort must not write beyond the source"
369 );
370 }
371
372 #[derive(Debug, Clone)]
391 struct SlowMockPuller {
392 data: Arc<[u8]>,
393 delay: Duration,
394 }
395 impl Puller for SlowMockPuller {
396 type Error = std::convert::Infallible;
397 #[allow(clippy::cast_possible_truncation)]
398 fn pull(
399 &mut self,
400 range: Option<&ProgressEntry>,
401 ) -> impl Future<
402 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
403 > + Send {
404 type PullItem = PullResult<Bytes, std::convert::Infallible>;
405 let owned: Vec<u8> = match range {
406 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
407 None => self.data.to_vec(),
408 };
409 let delay = self.delay;
410 async move {
411 sleep(delay).await;
412 let items: Vec<PullItem> = owned
413 .chunks(2)
414 .map(|c| Ok(Bytes::from(c.to_vec())))
415 .collect();
416 Ok(stream::iter(items))
417 }
418 }
419 }
420
421 #[tokio::test(flavor = "multi_thread")]
422 async fn test_concurrent_download_abort_discards_buffered() {
423 let mock_data = build_mock_data(64 * 1024);
425 let puller = SlowMockPuller {
426 data: Arc::from(mock_data.as_slice()),
427 delay: Duration::from_millis(50),
428 };
429 let inner = MemPusher::with_capacity(mock_data.len());
436 let receive = inner.receive.clone();
437 let buf = BufWriterPusher::new(inner, mock_data.len() + 1);
438 let pusher = CacheSeqPusher::new(buf, mock_data.len() + 1, 0);
439 #[allow(clippy::single_range_in_vec_init)]
440 let download_chunks = [0..mock_data.len() as u64];
441 let result = download_multi(
442 puller,
443 pusher,
444 DownloadOptions {
445 concurrent: 32,
446 retry_gap: Duration::from_secs(1),
447 push_queue_cap: 1024,
448 download_chunks: download_chunks.iter().cloned(),
449 pull_timeout: Duration::from_secs(5),
450 min_chunk_size: 1,
451 max_speculative: 3,
452 },
453 );
454
455 let mut aborted = false;
457 while let Ok(e) = result.event_chain().recv().await {
458 if matches!(e, Event::Pushing(_, _)) {
459 result.abort();
460 assert!(result.is_aborted());
461 aborted = true;
462 break;
463 }
464 }
465 assert!(aborted, "expected a Pushing event before aborting");
466
467 timeout(Duration::from_secs(10), drain(&result))
468 .await
469 .expect("event loop hung after abort");
470
471 let written = receive.lock().len();
475 assert!(
476 written < mock_data.len(),
477 "abort must stop before the full source is written (got {written} of {})",
478 mock_data.len()
479 );
480 }
481
482 use parking_lot::Mutex;
489 use std::sync::Arc;
490 use std::sync::atomic::{AtomicBool, Ordering};
491
492 #[derive(Debug)]
493 struct FatalErr;
494 impl std::fmt::Display for FatalErr {
495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496 f.write_str("fatal")
497 }
498 }
499 impl std::error::Error for FatalErr {}
500 impl crate::PullerError for FatalErr {
501 fn is_irrecoverable(&self) -> bool {
502 true
503 }
504 }
505
506 #[derive(Debug)]
507 struct RecoverableErr;
508 impl std::fmt::Display for RecoverableErr {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.write_str("recoverable")
511 }
512 }
513 impl std::error::Error for RecoverableErr {}
514 impl crate::PullerError for RecoverableErr {
515 fn is_irrecoverable(&self) -> bool {
516 false
517 }
518 }
519
520 struct FlakySink {
523 fail_push: Arc<AtomicBool>,
524 fail_flush: Arc<AtomicBool>,
525 receive: Arc<Mutex<Vec<u8>>>,
526 listener: Option<crate::ProgressListener>,
527 }
528 impl FlakySink {
529 fn new() -> Self {
530 Self {
531 fail_push: Arc::new(AtomicBool::new(false)),
532 fail_flush: Arc::new(AtomicBool::new(false)),
533 receive: Arc::new(Mutex::new(Vec::new())),
534 listener: None,
535 }
536 }
537 }
538 impl crate::Pusher for FlakySink {
539 type Error = std::io::Error;
540 fn set_listener(&mut self, cb: crate::ProgressListener) {
541 self.listener = Some(cb);
542 }
543 fn push(
544 &mut self,
545 range: &crate::ProgressEntry,
546 bytes: Bytes,
547 ) -> Result<(), (Self::Error, Bytes)> {
548 if self.fail_push.swap(false, Ordering::SeqCst) {
549 return Err((std::io::Error::other("push"), bytes));
550 }
551 let mut g = self.receive.lock();
552 if range.start as usize == g.len() {
553 g.extend_from_slice(&bytes);
554 } else {
555 if g.len() < range.end as usize {
556 g.resize(range.end as usize, 0);
557 }
558 g[range.start as usize..range.end as usize].copy_from_slice(&bytes);
559 }
560 drop(g);
561 if let Some(l) = &mut self.listener {
562 l(range.clone());
563 }
564 Ok(())
565 }
566 fn flush(&mut self) -> Result<(), Self::Error> {
567 if self.fail_flush.swap(false, Ordering::SeqCst) {
568 Err(std::io::Error::other("flush"))
569 } else {
570 Ok(())
571 }
572 }
573 }
574
575 #[derive(Debug, Clone)]
576 struct EmptyChunkPuller {
577 data: Arc<[u8]>,
578 }
579 impl crate::Puller for EmptyChunkPuller {
580 type Error = std::convert::Infallible;
581 fn pull(
582 &mut self,
583 range: Option<&crate::ProgressEntry>,
584 ) -> impl Future<
585 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
586 > + Send {
587 let data = match range {
588 Some(r) => &self.data[r.start as usize..r.end as usize],
589 None => &self.data,
590 };
591 let mut items: Vec<crate::PullResult<Bytes, std::convert::Infallible>> =
592 vec![Ok(Bytes::new())];
593 items.extend(data.chunks(2).map(|c| Ok(Bytes::copy_from_slice(c))));
594 std::future::ready(Ok(stream::iter(items)))
595 }
596 }
597
598 #[derive(Debug, Clone)]
599 struct PullErrOncePuller {
600 data: Arc<[u8]>,
601 failed: Arc<AtomicBool>,
602 }
603 impl crate::Puller for PullErrOncePuller {
604 type Error = RecoverableErr;
605 fn pull(
606 &mut self,
607 range: Option<&crate::ProgressEntry>,
608 ) -> impl Future<
609 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
610 > + Send {
611 if !self.failed.swap(true, Ordering::SeqCst) {
612 return std::future::ready(Err((RecoverableErr, Some(Duration::ZERO))));
613 }
614 let data = match range {
615 Some(r) => &self.data[r.start as usize..r.end as usize],
616 None => &self.data,
617 };
618 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
619 .chunks(2)
620 .map(|c| Ok(Bytes::copy_from_slice(c)))
621 .collect();
622 std::future::ready(Ok(stream::iter(items)))
623 }
624 }
625
626 #[derive(Debug, Clone)]
627 struct StreamErrOncePuller {
628 data: Arc<[u8]>,
629 failed: Arc<AtomicBool>,
630 }
631 impl crate::Puller for StreamErrOncePuller {
632 type Error = FatalErr;
633 fn pull(
634 &mut self,
635 range: Option<&crate::ProgressEntry>,
636 ) -> impl Future<
637 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
638 > + Send {
639 if !self.failed.swap(true, Ordering::SeqCst) {
640 let items: Vec<crate::PullResult<Bytes, FatalErr>> =
641 vec![Err((FatalErr, Some(Duration::ZERO)))];
642 return std::future::ready(Ok(stream::iter(items)));
643 }
644 let data = match range {
645 Some(r) => &self.data[r.start as usize..r.end as usize],
646 None => &self.data,
647 };
648 let items: Vec<crate::PullResult<Bytes, FatalErr>> = data
649 .chunks(2)
650 .map(|c| Ok(Bytes::copy_from_slice(c)))
651 .collect();
652 std::future::ready(Ok(stream::iter(items)))
653 }
654 }
655
656 #[derive(Debug, Clone)]
659 struct RecoverableStreamErrOncePuller {
660 data: Arc<[u8]>,
661 failed: Arc<AtomicBool>,
662 }
663 impl crate::Puller for RecoverableStreamErrOncePuller {
664 type Error = RecoverableErr;
665 fn pull(
666 &mut self,
667 range: Option<&crate::ProgressEntry>,
668 ) -> impl Future<
669 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
670 > + Send {
671 if !self.failed.swap(true, Ordering::SeqCst) {
672 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> =
673 vec![Err((RecoverableErr, Some(Duration::ZERO)))];
674 return std::future::ready(Ok(stream::iter(items)));
675 }
676 let data = match range {
677 Some(r) => &self.data[r.start as usize..r.end as usize],
678 None => &self.data,
679 };
680 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
681 .chunks(2)
682 .map(|c| Ok(Bytes::copy_from_slice(c)))
683 .collect();
684 std::future::ready(Ok(stream::iter(items)))
685 }
686 }
687
688 async fn drain<R, WE>(result: &DownloadResult<TokioExecutor<R, WE>, R::Error, WE>)
689 where
690 R: crate::Puller,
691 WE: Send + Unpin + 'static,
692 {
693 while result.event_chain().recv().await.is_ok() {}
694 }
695
696 #[tokio::test(flavor = "multi_thread")]
697 async fn test_multi_push_error_retries() {
698 let mock_data = build_mock_data(3 * 1024);
700 let puller = MockPuller::new(&mock_data);
701 let sink = FlakySink::new();
702 sink.fail_push.store(true, Ordering::SeqCst);
703 let receive = sink.receive.clone();
704 #[allow(clippy::single_range_in_vec_init)]
705 let download_chunks = [0..mock_data.len() as u64];
706 let result = download_multi(
707 puller,
708 sink,
709 DownloadOptions {
710 concurrent: 32,
711 retry_gap: Duration::ZERO,
712 push_queue_cap: 1024,
713 download_chunks: download_chunks.iter().cloned(),
714 pull_timeout: Duration::from_secs(5),
715 min_chunk_size: 1,
716 max_speculative: 3,
717 },
718 );
719 drain(&result).await;
720 assert_eq!(&**receive.lock(), mock_data);
721 }
722
723 #[tokio::test(flavor = "multi_thread")]
724 async fn test_multi_flush_error_retries() {
725 let mock_data = build_mock_data(3 * 1024);
727 let puller = MockPuller::new(&mock_data);
728 let sink = FlakySink::new();
729 sink.fail_flush.store(true, Ordering::SeqCst);
730 let receive = sink.receive.clone();
731 #[allow(clippy::single_range_in_vec_init)]
732 let download_chunks = [0..mock_data.len() as u64];
733 let result = download_multi(
734 puller,
735 sink,
736 DownloadOptions {
737 concurrent: 32,
738 retry_gap: Duration::ZERO,
739 push_queue_cap: 1024,
740 download_chunks: download_chunks.iter().cloned(),
741 pull_timeout: Duration::from_secs(5),
742 min_chunk_size: 1,
743 max_speculative: 3,
744 },
745 );
746 drain(&result).await;
747 assert_eq!(&**receive.lock(), mock_data);
748 }
749
750 #[tokio::test(flavor = "multi_thread")]
751 async fn test_multi_pull_error_retries() {
752 let mock_data = build_mock_data(3 * 1024);
754 let puller = PullErrOncePuller {
755 data: Arc::from(mock_data.as_slice()),
756 failed: Arc::new(AtomicBool::new(false)),
757 };
758 let pusher = MemPusher::with_capacity(mock_data.len());
759 let receive = pusher.receive.clone();
760 #[allow(clippy::single_range_in_vec_init)]
761 let download_chunks = [0..mock_data.len() as u64];
762 let result = download_multi(
763 puller,
764 pusher,
765 DownloadOptions {
766 concurrent: 32,
767 retry_gap: Duration::ZERO,
768 push_queue_cap: 1024,
769 download_chunks: download_chunks.iter().cloned(),
770 pull_timeout: Duration::from_secs(5),
771 min_chunk_size: 1,
772 max_speculative: 3,
773 },
774 );
775 drain(&result).await;
776 assert_eq!(&**receive.lock(), mock_data);
777 }
778
779 #[tokio::test(flavor = "multi_thread")]
780 async fn test_multi_empty_chunk_is_skipped() {
781 let mock_data = build_mock_data(3 * 1024);
783 let puller = EmptyChunkPuller {
784 data: Arc::from(mock_data.as_slice()),
785 };
786 let pusher = MemPusher::with_capacity(mock_data.len());
787 let receive = pusher.receive.clone();
788 #[allow(clippy::single_range_in_vec_init)]
789 let download_chunks = [0..mock_data.len() as u64];
790 let result = download_multi(
791 puller,
792 pusher,
793 DownloadOptions {
794 concurrent: 32,
795 retry_gap: Duration::ZERO,
796 push_queue_cap: 1024,
797 download_chunks: download_chunks.iter().cloned(),
798 pull_timeout: Duration::from_secs(5),
799 min_chunk_size: 1,
800 max_speculative: 3,
801 },
802 );
803 drain(&result).await;
804 assert_eq!(&**receive.lock(), mock_data);
805 }
806
807 #[tokio::test(flavor = "multi_thread")]
808 async fn test_multi_stream_error_irrecoverable_retries() {
809 let mock_data = build_mock_data(3 * 1024);
812 let puller = StreamErrOncePuller {
813 data: Arc::from(mock_data.as_slice()),
814 failed: Arc::new(AtomicBool::new(false)),
815 };
816 let pusher = MemPusher::with_capacity(mock_data.len());
817 let receive = pusher.receive.clone();
818 #[allow(clippy::single_range_in_vec_init)]
819 let download_chunks = [0..mock_data.len() as u64];
820 let result = download_multi(
821 puller,
822 pusher,
823 DownloadOptions {
824 concurrent: 32,
825 retry_gap: Duration::ZERO,
826 push_queue_cap: 1024,
827 download_chunks: download_chunks.iter().cloned(),
828 pull_timeout: Duration::from_secs(5),
829 min_chunk_size: 1,
830 max_speculative: 3,
831 },
832 );
833 drain(&result).await;
834 assert_eq!(&**receive.lock(), mock_data);
835 }
836
837 #[tokio::test(flavor = "multi_thread")]
838 async fn test_multi_stream_error_recoverable_retries() {
839 let mock_data = build_mock_data(3 * 1024);
843 let puller = RecoverableStreamErrOncePuller {
844 data: Arc::from(mock_data.as_slice()),
845 failed: Arc::new(AtomicBool::new(false)),
846 };
847 let pusher = MemPusher::with_capacity(mock_data.len());
848 let receive = pusher.receive.clone();
849 #[allow(clippy::single_range_in_vec_init)]
850 let download_chunks = [0..mock_data.len() as u64];
851 let result = download_multi(
852 puller,
853 pusher,
854 DownloadOptions {
855 concurrent: 32,
856 retry_gap: Duration::ZERO,
857 push_queue_cap: 1024,
858 download_chunks: download_chunks.iter().cloned(),
859 pull_timeout: Duration::from_secs(5),
860 min_chunk_size: 1,
861 max_speculative: 3,
862 },
863 );
864 drain(&result).await;
865 assert_eq!(&**receive.lock(), mock_data);
866 }
867
868 #[tokio::test]
869 async fn puller_and_error_coverage() {
870 assert_eq!(format!("{FatalErr}"), "fatal");
873 assert_eq!(format!("{RecoverableErr}"), "recoverable");
874
875 let mut empty = EmptyChunkPuller {
876 data: Arc::from(b"abcdef".as_slice()),
877 };
878 let _ = empty.pull(Some(&(0..2u64))).await;
879 let _ = empty.pull(None).await;
880
881 let mut pull_err = PullErrOncePuller {
882 data: Arc::from(b"abcdef".as_slice()),
883 failed: Arc::new(AtomicBool::new(false)),
884 };
885 let _ = pull_err.pull(Some(&(0..2u64))).await; let _ = pull_err.pull(None).await; let mut stream_err = StreamErrOncePuller {
889 data: Arc::from(b"abcdef".as_slice()),
890 failed: Arc::new(AtomicBool::new(false)),
891 };
892 let _ = stream_err.pull(Some(&(0..2u64))).await;
893 let _ = stream_err.pull(None).await;
894
895 let mut rec_stream_err = RecoverableStreamErrOncePuller {
896 data: Arc::from(b"abcdef".as_slice()),
897 failed: Arc::new(AtomicBool::new(false)),
898 };
899 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; }
903
904 #[tokio::test]
905 async fn test_slow_mock_puller_none_range() {
906 let mut p = SlowMockPuller {
909 data: Arc::from(b"hello world".as_slice()),
910 delay: Duration::ZERO,
911 };
912 assert!(p.pull(None).await.is_ok());
913 let mut p2 = SlowMockPuller {
914 data: Arc::from(b"hello world".as_slice()),
915 delay: Duration::ZERO,
916 };
917 assert!(p2.pull(Some(&(0..5))).await.is_ok());
918 }
919
920 #[derive(Debug, Clone)]
925 struct TimeoutOncePuller {
926 data: Arc<[u8]>,
927 first: Arc<AtomicBool>,
928 }
929 impl crate::Puller for TimeoutOncePuller {
930 type Error = std::convert::Infallible;
931 fn pull(
932 &mut self,
933 range: Option<&crate::ProgressEntry>,
934 ) -> impl Future<
935 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
936 > + Send {
937 let is_first = !self.first.swap(true, Ordering::SeqCst);
938 let data: Vec<u8> = match range {
939 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
940 None => self.data.to_vec(),
941 };
942 async move {
943 if is_first {
944 let head = data.get(..2).unwrap_or(&data);
948 let items = vec![Ok(Bytes::copy_from_slice(head))];
949 let pending =
950 stream::pending::<crate::PullResult<Bytes, std::convert::Infallible>>();
951 Ok(stream::iter(items).chain(pending))
952 } else {
953 let items: Vec<crate::PullResult<Bytes, std::convert::Infallible>> = data
957 .chunks(2)
958 .map(|c| Ok(Bytes::copy_from_slice(c)))
959 .collect();
960 let pending =
961 stream::pending::<crate::PullResult<Bytes, std::convert::Infallible>>();
962 Ok(stream::iter(items).chain(pending))
963 }
964 }
965 }
966 }
967
968 #[tokio::test(flavor = "multi_thread")]
969 async fn test_multi_pull_timeout_recovers_by_repulling() {
970 let mock_data = build_mock_data(3 * 1024);
974 let puller = TimeoutOncePuller {
975 data: Arc::from(mock_data.as_slice()),
976 first: Arc::new(AtomicBool::new(false)),
977 };
978 let pusher = MemPusher::with_capacity(mock_data.len());
979 let receive = pusher.receive.clone();
980 #[allow(clippy::single_range_in_vec_init)]
981 let download_chunks = [0..mock_data.len() as u64];
982 let result = download_multi(
983 puller,
984 pusher,
985 DownloadOptions {
986 concurrent: 32,
987 retry_gap: Duration::ZERO,
988 push_queue_cap: 1024,
989 download_chunks: download_chunks.iter().cloned(),
990 pull_timeout: Duration::from_millis(50),
991 min_chunk_size: 1,
992 max_speculative: 3,
993 },
994 );
995 drain(&result).await;
996 assert_eq!(&**receive.lock(), mock_data);
997 }
998
999 #[tokio::test(flavor = "multi_thread")]
1000 async fn test_concurrent_download_empty_chunks() {
1001 let mock_data = build_mock_data(3 * 1024);
1005 let puller = MockPuller::new(&mock_data);
1006 let pusher = MemPusher::with_capacity(mock_data.len());
1007 let receive = pusher.receive.clone();
1008 let result = download_multi(
1009 puller,
1010 pusher,
1011 DownloadOptions {
1012 concurrent: 32,
1013 retry_gap: Duration::from_secs(1),
1014 push_queue_cap: 1024,
1015 download_chunks: std::iter::empty(),
1016 pull_timeout: Duration::from_secs(5),
1017 min_chunk_size: 1,
1018 max_speculative: 3,
1019 },
1020 );
1021 timeout(Duration::from_secs(10), drain(&result))
1022 .await
1023 .expect("event loop hung on empty chunks");
1024 assert_eq!(receive.lock().len(), 0);
1025 }
1026}