use crate::Event;
use crossfire::{MAsyncRx, mpmc};
use fast_steal::{Executor, TaskQueue};
use std::fmt;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub mod mock;
pub mod multi;
pub mod single;
struct DownloadResultInner<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
event_chain: MAsyncRx<mpmc::List<Event<PullError, PushError>>>,
task_queue: Option<(E, TaskQueue<E::Handle>)>,
abort_token: CancellationToken,
}
impl<E, PullError, PushError> fmt::Debug for DownloadResultInner<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DownloadResultInner")
.field("event_chain", &self.event_chain)
.field("is_aborted", &self.abort_token.is_cancelled())
.finish_non_exhaustive()
}
}
impl<E, PullError, PushError> DownloadResultInner<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
pub fn abort(&self) {
self.abort_token.cancel();
}
pub fn set_threads(&self, threads: usize, min_chunk_size: u64) -> Option<()> {
let (executor, task_queue) = self.task_queue.as_ref()?;
task_queue.set_threads(threads, min_chunk_size, Some(executor))
}
#[must_use]
pub fn is_aborted(&self) -> bool {
self.abort_token.is_cancelled()
}
}
impl<E, PullError, PushError> Drop for DownloadResultInner<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
fn drop(&mut self) {
self.abort();
}
}
pub struct DownloadResult<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
inner: Arc<DownloadResultInner<E, PullError, PushError>>,
}
impl<E, PullError, PushError> fmt::Debug for DownloadResult<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DownloadResult")
.field("inner", &self.inner)
.finish()
}
}
impl<E, PullError, PushError> Clone for DownloadResult<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<E, PullError, PushError> DownloadResult<E, PullError, PushError>
where
E: Executor + Send + Sync,
PullError: Send + Unpin + 'static,
PushError: Send + Unpin + 'static,
{
pub fn new(
event_chain: MAsyncRx<mpmc::List<Event<PullError, PushError>>>,
task_queue: Option<(E, TaskQueue<E::Handle>)>,
abort_token: CancellationToken,
) -> Self {
Self {
inner: Arc::new(DownloadResultInner {
event_chain,
task_queue,
abort_token,
}),
}
}
#[must_use]
pub fn event_chain(&self) -> &MAsyncRx<mpmc::List<Event<PullError, PushError>>> {
&self.inner.event_chain
}
pub fn abort(&self) {
self.inner.abort();
}
pub fn set_threads(&self, threads: usize, min_chunk_size: u64) {
self.inner.set_threads(threads, min_chunk_size);
}
#[must_use]
pub fn is_aborted(&self) -> bool {
self.inner.is_aborted()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::cast_possible_truncation)]
use crate::MemPusher;
use crate::mock::{MockPuller, build_mock_data};
use crate::multi::{DownloadOptions, download_multi};
use crate::{Event, ProgressEntry, PullResult, PullStream, Puller};
use bytes::Bytes;
use futures::{StreamExt, stream};
use std::collections::BTreeSet;
use std::sync::Arc;
use tokio::time::{Duration, sleep, timeout};
#[derive(Debug, Clone)]
struct SlowPuller {
data: Arc<[u8]>,
delay: Duration,
}
impl Puller for SlowPuller {
type Error = std::convert::Infallible;
fn pull(
&mut self,
range: Option<&ProgressEntry>,
) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send
{
type PullItem = PullResult<Bytes, std::convert::Infallible>;
let owned: Vec<u8> = match range {
Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
None => self.data.to_vec(),
};
let delay = self.delay;
async move {
sleep(delay).await;
let items: Vec<PullItem> = vec![Ok(Bytes::from(owned))];
Ok(stream::iter(items))
}
}
}
#[derive(Debug, Clone)]
struct ChunkedPuller {
data: Arc<[u8]>,
piece: usize,
delay: Duration,
}
impl Puller for ChunkedPuller {
type Error = std::convert::Infallible;
fn pull(
&mut self,
range: Option<&ProgressEntry>,
) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send
{
let owned: Vec<u8> = match range {
Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
None => self.data.to_vec(),
};
let piece = self.piece;
let delay = self.delay;
async move {
Ok(
stream::unfold(Bytes::from(owned), move |mut buf| async move {
if buf.is_empty() {
return None;
}
let next = buf.split_to(piece.min(buf.len()));
sleep(delay).await;
Some((Ok(next), buf))
})
.boxed(),
)
}
}
}
fn next_rand(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
fn eight_chunks(size: u64) -> Vec<ProgressEntry> {
let step = size / 8;
(0..8).map(|i| i * step..(i + 1) * step).collect()
}
#[tokio::test(flavor = "multi_thread")]
async fn download_result_debug_and_set_threads() {
let mock_data = build_mock_data(1024);
let puller = MockPuller::new(&mock_data);
let pusher = MemPusher::with_capacity(mock_data.len());
let receive = pusher.receive.clone();
#[allow(clippy::single_range_in_vec_init)]
let download_chunks = [0..mock_data.len() as u64];
let result = download_multi(
puller,
pusher,
DownloadOptions {
concurrent: 4,
retry_gap: Duration::from_secs(1),
push_queue_cap: 1024,
download_chunks: download_chunks.iter().cloned(),
pull_timeout: Duration::from_secs(5),
min_chunk_size: 1,
max_speculative: 3,
},
);
let _ = format!("{result:?}");
result.set_threads(4, 1);
while result.event_chain().recv().await.is_ok() {}
assert_eq!(&**receive.lock(), mock_data);
}
#[tokio::test(flavor = "multi_thread")]
async fn download_result_clone_and_set_threads_no_queue() {
use crate::single::download_single;
let mock_data = build_mock_data(1024);
let puller = MockPuller::new(&mock_data);
let pusher = MemPusher::with_capacity(mock_data.len());
let receive = pusher.receive.clone();
let result = download_single(
puller,
pusher,
crate::single::DownloadOptions {
retry_gap: Duration::from_secs(1),
push_queue_cap: 1024,
},
);
let _clone = result.clone();
result.set_threads(4, 1);
while result.event_chain().recv().await.is_ok() {}
assert_eq!(&**receive.lock(), mock_data);
}
#[tokio::test(flavor = "multi_thread")]
async fn set_threads_does_not_unabort_an_aborted_session() {
let mock_data = build_mock_data(1024);
let puller = MockPuller::new(&mock_data);
let pusher = MemPusher::with_capacity(mock_data.len());
let result = download_multi(
puller,
pusher,
DownloadOptions {
concurrent: 4,
retry_gap: Duration::from_secs(1),
push_queue_cap: 1024,
download_chunks: std::iter::once(0..mock_data.len() as u64),
pull_timeout: Duration::from_secs(5),
min_chunk_size: 1,
max_speculative: 3,
},
);
assert!(!result.is_aborted());
result.set_threads(4, 1);
assert!(!result.is_aborted());
result.abort();
assert!(result.is_aborted());
result.set_threads(4, 1);
assert!(result.is_aborted());
while result.event_chain().recv().await.is_ok() {}
}
#[tokio::test(flavor = "multi_thread")]
async fn set_threads_growth_spawns_additional_workers() {
let mock_data = build_mock_data(8 * 1024);
let download_chunks = eight_chunks(mock_data.len() as u64);
let puller = SlowPuller {
data: Arc::from(mock_data.as_slice()),
delay: Duration::from_millis(150),
};
let pusher = MemPusher::with_capacity(mock_data.len());
let receive = pusher.receive.clone();
let result = download_multi(
puller,
pusher,
DownloadOptions {
concurrent: 1,
retry_gap: Duration::from_secs(1),
push_queue_cap: 1024,
download_chunks: download_chunks.iter().cloned(),
pull_timeout: Duration::from_secs(5),
min_chunk_size: 1,
max_speculative: 1,
},
);
let grower = result.clone();
tokio::spawn(async move {
sleep(Duration::from_millis(50)).await;
grower.set_threads(8, 1);
});
let mut pulling_ids = BTreeSet::new();
while let Ok(e) = result.event_chain().recv().await {
if let Event::Pulling(id) = e {
pulling_ids.insert(id);
}
}
assert!(
pulling_ids.len() > 1,
"growth spawned no additional worker (pulling ids: {pulling_ids:?})"
);
assert_eq!(&**receive.lock(), mock_data);
}
#[tokio::test(flavor = "multi_thread")]
async fn set_threads_growth_after_abort_does_not_resume() {
let mock_data = build_mock_data(8 * 1024);
let download_chunks = eight_chunks(mock_data.len() as u64);
let puller = SlowPuller {
data: Arc::from(mock_data.as_slice()),
delay: Duration::from_millis(150),
};
let pusher = MemPusher::with_capacity(mock_data.len());
let receive = pusher.receive.clone();
let result = download_multi(
puller,
pusher,
DownloadOptions {
concurrent: 1,
retry_gap: Duration::from_secs(1),
push_queue_cap: 1024,
download_chunks: download_chunks.iter().cloned(),
pull_timeout: Duration::from_secs(5),
min_chunk_size: 1,
max_speculative: 1,
},
);
result.abort();
result.set_threads(8, 1);
timeout(Duration::from_secs(10), async {
while result.event_chain().recv().await.is_ok() {}
})
.await
.expect("join() hung after abort followed by growth");
assert!(
receive.lock().len() < mock_data.len(),
"an aborted session must not be resumed by a later resize"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn set_threads_random_churn_preserves_all_bytes() {
let mock_data = build_mock_data(64 * 1024);
let download_chunks = eight_chunks(mock_data.len() as u64);
let puller = ChunkedPuller {
data: Arc::from(mock_data.as_slice()),
piece: 512,
delay: Duration::from_millis(2),
};
let pusher = MemPusher::with_capacity(mock_data.len());
let receive = pusher.receive.clone();
let result = download_multi(
puller,
pusher,
DownloadOptions {
concurrent: 4,
retry_gap: Duration::from_millis(10),
push_queue_cap: 1024,
download_chunks: download_chunks.iter().cloned(),
pull_timeout: Duration::from_secs(5),
min_chunk_size: 1,
max_speculative: 3,
},
);
let churner = result.clone();
let probe = receive.clone();
let total = mock_data.len();
let churn = tokio::spawn(async move {
let mut state = 0x2545_F491_4F6C_DD1D_u64;
let mut seen = BTreeSet::new();
let mut inflight = 0usize;
for _ in 0..40 {
let threads = (next_rand(&mut state) % 8 + 1) as usize;
seen.insert(threads);
if probe.lock().len() < total {
inflight += 1;
}
churner.set_threads(threads, 1);
sleep(Duration::from_millis(3)).await;
}
churner.set_threads(8, 1);
(seen, inflight)
});
let chunk_count = download_chunks.len();
let mut pulling_total = 0usize;
while let Ok(e) = result.event_chain().recv().await {
if matches!(e, Event::Pulling(_)) {
pulling_total += 1;
}
}
let (seen, inflight) = churn.await.unwrap();
assert!(
seen.len() > 2,
"churn never varied the pool size, so nothing was exercised: {seen:?}"
);
assert!(
inflight > 0,
"every resize landed after the download finished, so no running pool was churned"
);
assert!(
pulling_total > chunk_count,
"ranges were never redistributed ({pulling_total} pulls for {chunk_count} chunks)"
);
assert_eq!(
&**receive.lock(),
mock_data,
"repeated resizing corrupted the downloaded bytes"
);
}
}