use std::future::Future;
use anyhow::Result;
use futures::stream::{FuturesUnordered, StreamExt};
pub async fn run<Jobs, Fut, T, R>(
jobs: Jobs,
max_in_flight: usize,
mut on_result: impl FnMut(T) -> Result<R>,
) -> Result<()>
where
Jobs: IntoIterator<Item = Fut>,
Fut: Future<Output = Result<T>>,
{
let cap = max_in_flight.max(1);
let mut jobs = jobs.into_iter();
let mut in_flight = FuturesUnordered::new();
let mut exhausted = false;
loop {
while !exhausted && in_flight.len() < cap {
match jobs.next() {
Some(fut) => in_flight.push(fut),
None => exhausted = true,
}
}
if in_flight.is_empty() {
break; }
match in_flight.next().await {
Some(res) => {
let value = res?;
on_result(value)?;
}
None => break,
}
}
Ok(())
}
pub async fn run_ordered<Jobs, Fut, T, R>(
jobs: Jobs,
max_in_flight: usize,
mut map_result: impl FnMut(T) -> R,
) -> Result<Vec<R>>
where
Jobs: IntoIterator<Item = Fut>,
Fut: Future<Output = Result<T>>,
{
let cap = max_in_flight.max(1);
let mut jobs = jobs.into_iter().enumerate();
let mut in_flight = FuturesUnordered::new();
let mut exhausted = false;
let mut pending: std::collections::BTreeMap<usize, R> = std::collections::BTreeMap::new();
let mut next_emit = 0usize;
let mut out: Vec<R> = Vec::new();
loop {
while !exhausted && in_flight.len() < cap {
match jobs.next() {
Some((idx, fut)) => {
in_flight.push(async move { (idx, fut.await) });
}
None => exhausted = true,
}
}
if in_flight.is_empty() {
break;
}
match in_flight.next().await {
Some((idx, res)) => {
pending.insert(idx, map_result(res?));
while let Some(val) = pending.remove(&next_emit) {
out.push(val);
next_emit += 1;
}
}
None => break,
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn run_ordered_keeps_cap_and_preserves_order() {
const N: usize = 100;
const CAP: usize = 8;
let ran = Arc::new(AtomicUsize::new(0));
let inflight = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let jobs = (0..N).map(|i| {
let ran = Arc::clone(&ran);
let inflight = Arc::clone(&inflight);
let peak = Arc::clone(&peak);
async move {
let now = inflight.fetch_add(1, Ordering::AcqRel) + 1;
peak.fetch_max(now, Ordering::AcqRel);
let delay = ((N - i) % 11 + 1) as u64;
tokio::time::sleep(Duration::from_millis(delay)).await;
ran.fetch_add(1, Ordering::AcqRel);
inflight.fetch_sub(1, Ordering::AcqRel);
Ok::<usize, anyhow::Error>(i * i)
}
});
let out = run_ordered(jobs, CAP, |v| v).await.expect("run_ordered failed");
assert_eq!(ran.load(Ordering::Acquire), N, "not every job ran");
assert_eq!(out.len(), N, "wrong number of results");
for (i, v) in out.iter().enumerate() {
assert_eq!(*v, i * i, "result {i} out of order or wrong value");
}
let peak = peak.load(Ordering::Acquire);
assert!(peak <= CAP, "exceeded in-flight cap: peak={peak} cap={CAP}");
assert_eq!(peak, CAP, "never reached the in-flight cap: peak={peak}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn run_unordered_bounds_concurrency_and_reduces_all() {
const N: usize = 50;
const CAP: usize = 5;
let inflight = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let jobs = (0..N).map(|i| {
let inflight = Arc::clone(&inflight);
let peak = Arc::clone(&peak);
async move {
let now = inflight.fetch_add(1, Ordering::AcqRel) + 1;
peak.fetch_max(now, Ordering::AcqRel);
tokio::time::sleep(Duration::from_millis(2)).await;
inflight.fetch_sub(1, Ordering::AcqRel);
Ok::<u64, anyhow::Error>(i as u64)
}
});
let sum = Arc::new(AtomicUsize::new(0));
let sum2 = Arc::clone(&sum);
run(jobs, CAP, move |v| {
sum2.fetch_add(v as usize, Ordering::AcqRel);
Ok(())
})
.await
.expect("run failed");
let expected: usize = (0..N).sum();
assert_eq!(sum.load(Ordering::Acquire), expected, "reduce dropped/dup'd a job");
let peak = peak.load(Ordering::Acquire);
assert!(peak <= CAP, "exceeded cap: peak={peak} cap={CAP}");
assert_eq!(peak, CAP, "never reached cap: peak={peak}");
}
#[tokio::test]
async fn run_propagates_first_error() {
let jobs = (0..10usize).map(|i| async move {
if i == 3 {
Err(anyhow::anyhow!("boom at {i}"))
} else {
Ok::<usize, anyhow::Error>(i)
}
});
let err = run(jobs, 4, |_v| Ok(())).await.expect_err("should have errored");
assert!(err.to_string().contains("boom"), "wrong error: {err}");
}
}