mod types;
pub use types::{FailurePolicy, ItemOutcome, ParallelOptions, ParallelOutcome};
use futures::stream::StreamExt;
use std::future::Future;
use crate::error::{Result, TinyAgentsError};
pub async fn map_reduce<I, T, F, Fut>(
items: Vec<I>,
options: ParallelOptions,
f: F,
) -> Result<ParallelOutcome<T>>
where
F: Fn(usize, I) -> Fut,
Fut: Future<Output = Result<T>>,
{
let total = items.len();
let concurrency = if options.max_concurrency == 0 {
total.max(1)
} else {
options.max_concurrency
};
let item_timeout = options.item_timeout;
let mut stream = futures::stream::iter(items.into_iter().enumerate().map(|(index, item)| {
let fut = f(index, item);
async move {
let result = match item_timeout {
Some(limit) => match tokio::time::timeout(limit, fut).await {
Ok(r) => r,
Err(_) => Err(TinyAgentsError::Timeout(format!(
"parallel item {index} exceeded {} ms",
limit.as_millis()
))),
},
None => fut.await,
};
(index, result)
}
}))
.buffer_unordered(concurrency);
let mut slots: Vec<Option<std::result::Result<T, String>>> = (0..total).map(|_| None).collect();
let mut fail_fast_error: Option<(usize, TinyAgentsError)> = None;
let collect = async {
while let Some((index, result)) = stream.next().await {
match result {
Ok(value) => slots[index] = Some(Ok(value)),
Err(err) => {
if options.failure_policy == FailurePolicy::FailFast {
slots[index] = Some(Err(String::new()));
if fail_fast_error
.as_ref()
.is_none_or(|(seen, _)| index < *seen)
{
fail_fast_error = Some((index, err));
}
let min_index = fail_fast_error.as_ref().map(|(i, _)| *i).unwrap_or(index);
if slots[..min_index].iter().all(Option::is_some) {
break; }
continue;
}
slots[index] = Some(Err(err.to_string()));
}
}
}
};
let cancelled = async {
match &options.cancellation {
Some(token) => token.cancelled().await,
None => std::future::pending::<()>().await,
}
};
let raced = async {
tokio::select! {
biased;
_ = cancelled => Err(TinyAgentsError::Cancelled),
() = collect => Ok(()),
}
};
match options.total_timeout {
Some(limit) => match tokio::time::timeout(limit, raced).await {
Ok(inner) => inner?,
Err(_) => {
return Err(TinyAgentsError::Timeout(format!(
"parallel map/reduce exceeded {} ms",
limit.as_millis()
)));
}
},
None => raced.await?,
}
if let Some((_, err)) = fail_fast_error {
return Err(err);
}
let mut outcomes: Vec<ItemOutcome<T>> = Vec::with_capacity(total);
for (index, slot) in slots.into_iter().enumerate() {
if let Some(result) = slot {
outcomes.push(ItemOutcome { index, result });
}
}
let success_count = outcomes.iter().filter(|o| o.is_ok()).count();
match options.failure_policy {
FailurePolicy::Quorum(required) if success_count < required => Err(TinyAgentsError::Graph(
format!("parallel quorum not met: {success_count}/{required} items succeeded"),
)),
FailurePolicy::BestEffort => {
outcomes.retain(|o| o.is_ok());
Ok(ParallelOutcome { outcomes })
}
_ => Ok(ParallelOutcome { outcomes }),
}
}
#[cfg(test)]
mod test;