use crate::Result;
use crate::error::CoreError;
use std::future::Future;
pub(crate) async fn spawn_aborting<R, Fut>(fut: Fut) -> Result<R>
where
R: Send + 'static,
Fut: Future<Output = Result<R>> + Send + 'static,
{
struct AbortOnDrop<R>(tokio::task::JoinHandle<R>);
impl<R> Drop for AbortOnDrop<R> {
fn drop(&mut self) {
self.0.abort();
}
}
let handle = match tokio::runtime::Handle::try_current() {
Ok(h) if h.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => h,
_ => return fut.await,
};
let mut task = AbortOnDrop(handle.spawn(fut));
(&mut task.0).await.unwrap_or_else(|e| {
Err(CoreError::ReadFileSliceError(format!(
"read task failed: {e}"
)))
})
}
pub(crate) async fn bounded_in_order<'a, T, R, F, Fut>(
items: &'a [T],
concurrency: usize,
op: F,
) -> Result<Vec<R>>
where
F: Fn(&'a T) -> Fut,
Fut: Future<Output = Result<R>>,
{
let concurrency = concurrency.max(1);
let mut out = Vec::with_capacity(items.len());
for chunk in items.chunks(concurrency) {
let running = chunk.iter().map(&op);
out.extend(futures::future::try_join_all(running).await?);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_aborting_aborts_the_task_on_drop() {
let finished = Arc::new(AtomicBool::new(false));
let flag = finished.clone();
let read = spawn_aborting(async move {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
flag.store(true, Ordering::SeqCst);
Ok(())
});
let _ = tokio::time::timeout(std::time::Duration::from_millis(10), read).await;
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
assert!(
!finished.load(Ordering::SeqCst),
"the dropped task kept running"
);
}
#[test]
fn spawn_aborting_runs_in_place_without_a_runtime() {
let got = futures::executor::block_on(spawn_aborting(async { Ok(7) })).unwrap();
assert_eq!(got, 7);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_aborting_reports_a_panic_as_an_error() {
let result: Result<()> = spawn_aborting(async { panic!("boom") }).await;
let err = result.expect_err("a panicking task must fail the read");
assert!(err.to_string().contains("read task failed"), "{err}");
}
#[tokio::test]
async fn results_keep_input_order() -> Result<()> {
let items: Vec<u32> = (0..10).collect();
let got = bounded_in_order(&items, 4, |n| {
let n = *n;
async move { Ok(n * 2) }
})
.await?;
assert_eq!(got, (0..10).map(|n| n * 2).collect::<Vec<_>>());
Ok(())
}
#[tokio::test]
async fn no_more_than_concurrency_run_at_once() -> Result<()> {
let live = AtomicUsize::new(0);
let peak = AtomicUsize::new(0);
let items: Vec<u32> = (0..6).collect();
bounded_in_order(&items, 2, |_| {
let (live, peak) = (&live, &peak);
async move {
let now = live.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
tokio::task::yield_now().await;
live.fetch_sub(1, Ordering::SeqCst);
Ok(())
}
})
.await?;
assert_eq!(
peak.load(Ordering::SeqCst),
2,
"at most `concurrency` operations may be in flight"
);
Ok(())
}
#[tokio::test]
async fn zero_concurrency_reads_sequentially() -> Result<()> {
let items: Vec<u32> = (0..5).collect();
let got = bounded_in_order(&items, 0, |n| {
let n = *n;
async move { Ok(n) }
})
.await?;
assert_eq!(got, items, "0 must clamp to 1, not drop every item");
Ok(())
}
}