1use crate::Event;
11use crossfire::{MAsyncRx, mpmc};
12use fast_steal::{Executor, TaskQueue};
13use std::fmt;
14use std::sync::Arc;
15use tokio_util::sync::CancellationToken;
16
17pub mod mock;
18pub mod multi;
19pub mod single;
20
21struct DownloadResultInner<E, PullError, PushError>
30where
31 E: Executor + Send + Sync,
32 PullError: Send + Unpin + 'static,
33 PushError: Send + Unpin + 'static,
34{
35 event_chain: MAsyncRx<mpmc::List<Event<PullError, PushError>>>,
36 task_queue: Option<(E, TaskQueue<E::Handle>)>,
44 abort_token: CancellationToken,
52}
53
54impl<E, PullError, PushError> fmt::Debug for DownloadResultInner<E, PullError, PushError>
55where
56 E: Executor + Send + Sync,
57 PullError: Send + Unpin + 'static,
58 PushError: Send + Unpin + 'static,
59{
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.debug_struct("DownloadResultInner")
62 .field("event_chain", &self.event_chain)
63 .field("is_aborted", &self.abort_token.is_cancelled())
64 .finish_non_exhaustive()
65 }
66}
67
68impl<E, PullError, PushError> DownloadResultInner<E, PullError, PushError>
69where
70 E: Executor + Send + Sync,
71 PullError: Send + Unpin + 'static,
72 PushError: Send + Unpin + 'static,
73{
74 pub fn abort(&self) {
80 self.abort_token.cancel();
81 }
82
83 pub fn set_threads(&self, threads: usize, min_chunk_size: u64) -> Option<()> {
84 let (executor, task_queue) = self.task_queue.as_ref()?;
85 task_queue.set_threads(threads, min_chunk_size, Some(executor))
86 }
87
88 #[must_use]
89 pub fn is_aborted(&self) -> bool {
90 self.abort_token.is_cancelled()
91 }
92}
93
94impl<E, PullError, PushError> Drop for DownloadResultInner<E, PullError, PushError>
95where
96 E: Executor + Send + Sync,
97 PullError: Send + Unpin + 'static,
98 PushError: Send + Unpin + 'static,
99{
100 fn drop(&mut self) {
101 self.abort();
102 }
103}
104
105pub struct DownloadResult<E, PullError, PushError>
122where
123 E: Executor + Send + Sync,
124 PullError: Send + Unpin + 'static,
125 PushError: Send + Unpin + 'static,
126{
127 inner: Arc<DownloadResultInner<E, PullError, PushError>>,
128}
129
130impl<E, PullError, PushError> fmt::Debug for DownloadResult<E, PullError, PushError>
131where
132 E: Executor + Send + Sync,
133 PullError: Send + Unpin + 'static,
134 PushError: Send + Unpin + 'static,
135{
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("DownloadResult")
138 .field("inner", &self.inner)
139 .finish()
140 }
141}
142
143impl<E, PullError, PushError> Clone for DownloadResult<E, PullError, PushError>
144where
145 E: Executor + Send + Sync,
146 PullError: Send + Unpin + 'static,
147 PushError: Send + Unpin + 'static,
148{
149 fn clone(&self) -> Self {
150 Self {
151 inner: self.inner.clone(),
152 }
153 }
154}
155
156impl<E, PullError, PushError> DownloadResult<E, PullError, PushError>
157where
158 E: Executor + Send + Sync,
159 PullError: Send + Unpin + 'static,
160 PushError: Send + Unpin + 'static,
161{
162 pub fn new(
169 event_chain: MAsyncRx<mpmc::List<Event<PullError, PushError>>>,
170 task_queue: Option<(E, TaskQueue<E::Handle>)>,
171 abort_token: CancellationToken,
172 ) -> Self {
173 Self {
174 inner: Arc::new(DownloadResultInner {
175 event_chain,
176 task_queue,
177 abort_token,
178 }),
179 }
180 }
181
182 #[must_use]
187 pub fn event_chain(&self) -> &MAsyncRx<mpmc::List<Event<PullError, PushError>>> {
188 &self.inner.event_chain
189 }
190
191 pub fn abort(&self) {
197 self.inner.abort();
198 }
199
200 pub fn set_threads(&self, threads: usize, min_chunk_size: u64) {
221 self.inner.set_threads(threads, min_chunk_size);
222 }
223
224 #[must_use]
226 pub fn is_aborted(&self) -> bool {
227 self.inner.is_aborted()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 #![allow(clippy::unwrap_used)]
234 #![allow(clippy::cast_possible_truncation)]
235 use crate::MemPusher;
236 use crate::mock::{MockPuller, build_mock_data};
237 use crate::multi::{DownloadOptions, download_multi};
238 use crate::{Event, ProgressEntry, PullResult, PullStream, Puller};
239 use bytes::Bytes;
240 use futures::{StreamExt, stream};
241 use std::collections::BTreeSet;
242 use std::sync::Arc;
243 use tokio::time::{Duration, sleep, timeout};
244
245 #[derive(Debug, Clone)]
248 struct SlowPuller {
249 data: Arc<[u8]>,
250 delay: Duration,
251 }
252 impl Puller for SlowPuller {
253 type Error = std::convert::Infallible;
254 fn pull(
255 &mut self,
256 range: Option<&ProgressEntry>,
257 ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send
258 {
259 type PullItem = PullResult<Bytes, std::convert::Infallible>;
260 let owned: Vec<u8> = match range {
261 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
262 None => self.data.to_vec(),
263 };
264 let delay = self.delay;
265 async move {
266 sleep(delay).await;
267 let items: Vec<PullItem> = vec![Ok(Bytes::from(owned))];
268 Ok(stream::iter(items))
269 }
270 }
271 }
272
273 #[derive(Debug, Clone)]
279 struct ChunkedPuller {
280 data: Arc<[u8]>,
281 piece: usize,
282 delay: Duration,
283 }
284 impl Puller for ChunkedPuller {
285 type Error = std::convert::Infallible;
286 fn pull(
287 &mut self,
288 range: Option<&ProgressEntry>,
289 ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send
290 {
291 let owned: Vec<u8> = match range {
292 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
293 None => self.data.to_vec(),
294 };
295 let piece = self.piece;
296 let delay = self.delay;
297 async move {
298 Ok(
299 stream::unfold(Bytes::from(owned), move |mut buf| async move {
300 if buf.is_empty() {
301 return None;
302 }
303 let next = buf.split_to(piece.min(buf.len()));
304 sleep(delay).await;
305 Some((Ok(next), buf))
306 })
307 .boxed(),
308 )
309 }
310 }
311 }
312
313 fn next_rand(state: &mut u64) -> u64 {
316 let mut x = *state;
317 x ^= x << 13;
318 x ^= x >> 7;
319 x ^= x << 17;
320 *state = x;
321 x
322 }
323
324 fn eight_chunks(size: u64) -> Vec<ProgressEntry> {
327 let step = size / 8;
328 (0..8).map(|i| i * step..(i + 1) * step).collect()
329 }
330
331 #[tokio::test(flavor = "multi_thread")]
332 async fn download_result_debug_and_set_threads() {
333 let mock_data = build_mock_data(1024);
334 let puller = MockPuller::new(&mock_data);
335 let pusher = MemPusher::with_capacity(mock_data.len());
336 let receive = pusher.receive.clone();
337 #[allow(clippy::single_range_in_vec_init)]
338 let download_chunks = [0..mock_data.len() as u64];
339 let result = download_multi(
340 puller,
341 pusher,
342 DownloadOptions {
343 concurrent: 4,
344 retry_gap: Duration::from_secs(1),
345 push_queue_cap: 1024,
346 download_chunks: download_chunks.iter().cloned(),
347 pull_timeout: Duration::from_secs(5),
348 min_chunk_size: 1,
349 max_speculative: 3,
350 },
351 );
352 let _ = format!("{result:?}");
355 result.set_threads(4, 1);
357 while result.event_chain().recv().await.is_ok() {}
360 assert_eq!(&**receive.lock(), mock_data);
361 }
362
363 #[tokio::test(flavor = "multi_thread")]
364 async fn download_result_clone_and_set_threads_no_queue() {
365 use crate::single::download_single;
366 let mock_data = build_mock_data(1024);
367 let puller = MockPuller::new(&mock_data);
368 let pusher = MemPusher::with_capacity(mock_data.len());
369 let receive = pusher.receive.clone();
370 let result = download_single(
373 puller,
374 pusher,
375 crate::single::DownloadOptions {
376 retry_gap: Duration::from_secs(1),
377 push_queue_cap: 1024,
378 },
379 );
380 let _clone = result.clone();
382 result.set_threads(4, 1);
383 while result.event_chain().recv().await.is_ok() {}
384 assert_eq!(&**receive.lock(), mock_data);
385 }
386
387 #[tokio::test(flavor = "multi_thread")]
393 async fn set_threads_does_not_unabort_an_aborted_session() {
394 let mock_data = build_mock_data(1024);
395 let puller = MockPuller::new(&mock_data);
396 let pusher = MemPusher::with_capacity(mock_data.len());
397 let result = download_multi(
398 puller,
399 pusher,
400 DownloadOptions {
401 concurrent: 4,
402 retry_gap: Duration::from_secs(1),
403 push_queue_cap: 1024,
404 download_chunks: std::iter::once(0..mock_data.len() as u64),
405 pull_timeout: Duration::from_secs(5),
406 min_chunk_size: 1,
407 max_speculative: 3,
408 },
409 );
410 assert!(!result.is_aborted());
412 result.set_threads(4, 1);
413 assert!(!result.is_aborted());
414 result.abort();
416 assert!(result.is_aborted());
417 result.set_threads(4, 1);
418 assert!(result.is_aborted());
419 while result.event_chain().recv().await.is_ok() {}
420 }
421
422 #[tokio::test(flavor = "multi_thread")]
426 async fn set_threads_growth_spawns_additional_workers() {
427 let mock_data = build_mock_data(8 * 1024);
428 let download_chunks = eight_chunks(mock_data.len() as u64);
429 let puller = SlowPuller {
430 data: Arc::from(mock_data.as_slice()),
431 delay: Duration::from_millis(150),
432 };
433 let pusher = MemPusher::with_capacity(mock_data.len());
434 let receive = pusher.receive.clone();
435 let result = download_multi(
436 puller,
437 pusher,
438 DownloadOptions {
439 concurrent: 1,
440 retry_gap: Duration::from_secs(1),
441 push_queue_cap: 1024,
442 download_chunks: download_chunks.iter().cloned(),
443 pull_timeout: Duration::from_secs(5),
444 min_chunk_size: 1,
445 max_speculative: 1,
446 },
447 );
448
449 let grower = result.clone();
451 tokio::spawn(async move {
452 sleep(Duration::from_millis(50)).await;
453 grower.set_threads(8, 1);
454 });
455
456 let mut pulling_ids = BTreeSet::new();
457 while let Ok(e) = result.event_chain().recv().await {
458 if let Event::Pulling(id) = e {
459 pulling_ids.insert(id);
460 }
461 }
462 assert!(
463 pulling_ids.len() > 1,
464 "growth spawned no additional worker (pulling ids: {pulling_ids:?})"
465 );
466 assert_eq!(&**receive.lock(), mock_data);
467 }
468
469 #[tokio::test(flavor = "multi_thread")]
473 async fn set_threads_growth_after_abort_does_not_resume() {
474 let mock_data = build_mock_data(8 * 1024);
475 let download_chunks = eight_chunks(mock_data.len() as u64);
476 let puller = SlowPuller {
477 data: Arc::from(mock_data.as_slice()),
478 delay: Duration::from_millis(150),
479 };
480 let pusher = MemPusher::with_capacity(mock_data.len());
481 let receive = pusher.receive.clone();
482 let result = download_multi(
483 puller,
484 pusher,
485 DownloadOptions {
486 concurrent: 1,
487 retry_gap: Duration::from_secs(1),
488 push_queue_cap: 1024,
489 download_chunks: download_chunks.iter().cloned(),
490 pull_timeout: Duration::from_secs(5),
491 min_chunk_size: 1,
492 max_speculative: 1,
493 },
494 );
495
496 result.abort();
497 result.set_threads(8, 1);
498 timeout(Duration::from_secs(10), async {
501 while result.event_chain().recv().await.is_ok() {}
502 })
503 .await
504 .expect("join() hung after abort followed by growth");
505 assert!(
506 receive.lock().len() < mock_data.len(),
507 "an aborted session must not be resumed by a later resize"
508 );
509 }
510
511 #[tokio::test(flavor = "multi_thread")]
519 async fn set_threads_random_churn_preserves_all_bytes() {
520 let mock_data = build_mock_data(64 * 1024);
521 let download_chunks = eight_chunks(mock_data.len() as u64);
522 let puller = ChunkedPuller {
523 data: Arc::from(mock_data.as_slice()),
524 piece: 512,
525 delay: Duration::from_millis(2),
526 };
527 let pusher = MemPusher::with_capacity(mock_data.len());
528 let receive = pusher.receive.clone();
529 let result = download_multi(
530 puller,
531 pusher,
532 DownloadOptions {
533 concurrent: 4,
534 retry_gap: Duration::from_millis(10),
535 push_queue_cap: 1024,
536 download_chunks: download_chunks.iter().cloned(),
537 pull_timeout: Duration::from_secs(5),
538 min_chunk_size: 1,
539 max_speculative: 3,
540 },
541 );
542
543 let churner = result.clone();
546 let probe = receive.clone();
547 let total = mock_data.len();
548 let churn = tokio::spawn(async move {
549 let mut state = 0x2545_F491_4F6C_DD1D_u64;
550 let mut seen = BTreeSet::new();
551 let mut inflight = 0usize;
554 for _ in 0..40 {
555 let threads = (next_rand(&mut state) % 8 + 1) as usize;
556 seen.insert(threads);
557 if probe.lock().len() < total {
558 inflight += 1;
559 }
560 churner.set_threads(threads, 1);
561 sleep(Duration::from_millis(3)).await;
562 }
563 churner.set_threads(8, 1);
565 (seen, inflight)
566 });
567
568 let chunk_count = download_chunks.len();
569 let mut pulling_total = 0usize;
570 while let Ok(e) = result.event_chain().recv().await {
571 if matches!(e, Event::Pulling(_)) {
572 pulling_total += 1;
573 }
574 }
575 let (seen, inflight) = churn.await.unwrap();
576 assert!(
577 seen.len() > 2,
578 "churn never varied the pool size, so nothing was exercised: {seen:?}"
579 );
580 assert!(
583 inflight > 0,
584 "every resize landed after the download finished, so no running pool was churned"
585 );
586 assert!(
589 pulling_total > chunk_count,
590 "ranges were never redistributed ({pulling_total} pulls for {chunk_count} chunks)"
591 );
592 assert_eq!(
593 &**receive.lock(),
594 mock_data,
595 "repeated resizing corrupted the downloaded bytes"
596 );
597 }
598}