use super::host::{CallResult, Invoked, Layers, Settled, call_start, invoke, keep_records, settle};
use super::interp::{Over, RunEnv, elements};
use super::policy::{self, Decision, Flow, Policy, decide};
use super::record::{self, Completed};
use crate::base::{Change, Error, ErrorKind};
use crate::expr::path::MAX_PATH_SEGMENTS;
use crate::expr::{MAX_ARRAY_INDEX, Path, Seg};
use crate::function::{Binding, BoundCall, CallInfo, CallStart, Outcome};
use crate::program::{CallSite, CompiledWorkflow, FanSite};
use crate::state::RunContext;
use datavalue::OwnedDataValue;
use futures_util::stream::{FuturesUnordered, StreamExt};
use std::fmt::Write as _;
use std::sync::Arc;
struct Element {
invoked: Invoked,
decision: Decision,
}
impl Element {
fn new(mut invoked: Invoked, policy: Policy, task_id: &str) -> Self {
let result = match &mut invoked.result {
CallResult::Returned(Ok(outcome)) => Ok(*outcome),
CallResult::Returned(Err(e)) => Err(std::mem::replace(e, Error::service("", ""))),
CallResult::CutShort(..) => unreachable!("a call cut short breaks out before Element"),
};
let decision = decide(policy, task_id, result);
Self { invoked, decision }
}
fn stops(&self) -> bool {
matches!(self.decision.flow, Flow::Fail(_) | Flow::Halt)
}
}
#[allow(
clippy::too_many_arguments,
reason = "one call site's per-element facts, passed straight to `invoke`"
)]
async fn call_element<'r>(
start: CallStart<'r>,
fan: &'r FanSite,
index: usize,
item: &'r OwnedDataValue,
bound: &'r dyn BoundCall,
layers: Layers<'r>,
timed: bool,
timers: bool,
) -> (usize, Invoked) {
let binding = Binding {
item: &fan.r#as,
value: item,
index: &fan.index,
index_value: OwnedDataValue::from(index),
};
let start = CallStart {
binding: Some(&binding),
info: CallInfo {
element_index: Some(index),
..start.info
},
..start
};
let started = timed.then(record::now);
let mut invoked = invoke(start, bound, layers, fan.collect.as_ref(), timers).await;
invoked.elapsed = started.map(record::since);
(index, invoked)
}
fn fits_into<'e>(
fan: &FanSite,
task: &str,
items: std::borrow::Cow<'e, [OwnedDataValue]>,
) -> Result<std::borrow::Cow<'e, [OwnedDataValue]>, Error> {
if fan.into.is_none() || items.len() <= MAX_ARRAY_INDEX + 1 {
return Ok(items);
}
Err(Error::new(
ErrorKind::Eval,
format!(
"{} has {} elements, and into holds at most {}, one result per index up to {MAX_ARRAY_INDEX}",
Over::FanOut(task),
items.len(),
MAX_ARRAY_INDEX + 1
),
))
}
fn slot_segs(into: &Path) -> Vec<Seg> {
debug_assert!(
into.segs().len() < MAX_PATH_SEGMENTS,
"the compiler leaves room below `into` for an index"
);
let mut segs = Vec::with_capacity(into.segs().len() + 1);
segs.extend_from_slice(into.segs());
segs.push(Seg::index(0));
segs
}
pub(crate) async fn fan_out<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
wf: &'p CompiledWorkflow,
site: &'p CallSite,
fan: &'p FanSite,
bound: &'p dyn BoundCall,
) -> Flow {
let task_id = env.ids(site.task).task;
let policy = site.policy;
let over = elements(rc, env.exprs, &fan.over, Over::FanOut(task_id))
.and_then(|items| fits_into(fan, task_id, items));
let items = match over {
Ok(items) if !items.is_empty() => items,
no_calls => {
let errors_before = env.errors.len();
let started = env.watch.timed.then(record::now);
let result = no_calls.map(|_| {
if let Some(into) = &fan.into {
rc.set(into, OwnedDataValue::Array(Vec::new()), None);
if env.capture.captures() {
env.changes.push(Change::write(
Arc::clone(into.dotted()),
OwnedDataValue::Null,
OwnedDataValue::Array(Vec::new()),
));
}
}
Outcome::Ok
});
let decision = decide(policy, task_id, result);
let done = Completed::new(
env,
site.task,
None,
started.map(record::since),
errors_before,
false,
);
return record::record(rc, env, wf, done, decision);
}
};
let n = items.len();
let per_element = policy.per_element();
let mut finished: Vec<Option<Element>> = (0..n).map(|_| None).collect();
let mut cut = None;
{
let reader = rc.reader();
let (start, layers) = call_start(env, reader, wf, site);
let mut pending = items.iter().enumerate();
let mut in_flight = FuturesUnordered::new();
let mut stop = false;
loop {
while !stop && in_flight.len() < fan.max_concurrency {
let Some((index, item)) = pending.next() else {
break;
};
in_flight.push(call_element(
start,
fan,
index,
item,
bound,
layers,
env.watch.timed,
env.limits.timers,
));
}
let Some((index, invoked)) = in_flight.next().await else {
break;
};
if let CallResult::CutShort(stop, error) = invoked.result {
cut = Some((index, invoked.errors, stop, error, invoked.elapsed));
break;
}
let element = Element::new(invoked, per_element, task_id);
stop |= element.stops();
finished[index] = Some(element);
}
}
drop(items);
if let Some((cut_index, cut_errors, stop, e, elapsed)) = cut {
let errors_before = env.errors.len();
let ids = env.ids(site.task);
let mut cut_errors = Some(cut_errors);
for (index, slot) in finished.into_iter().enumerate() {
let errors = if index == cut_index {
cut_errors.take()
} else {
slot.map(|element| element.invoked.errors)
};
if let Some(errors) = errors {
keep_records(ids, env.now, env.now_text, env.errors, errors, Some(index));
}
}
let decision = policy::cut_short(task_id, stop, &e);
let done = Completed::new(env, site.task, None, elapsed, errors_before, false);
return record::record(rc, env, wf, done, decision);
}
if let Some(into) = &fan.into {
let prefill = OwnedDataValue::Array(vec![OwnedDataValue::Null; n]);
if let Some(trace) = env.watch.trace.as_deref_mut().filter(|t| t.changes()) {
trace.push_prelude(Change::write(
Arc::clone(into.dotted()),
rc.get_or_null(into),
prefill.clone(),
));
}
rc.set(into, prefill, None);
}
let mut slot = fan.into.as_ref().map(|into| {
let at = format!("{into}.");
let base = at.len();
(slot_segs(into), at, base)
});
let mut halted = false;
let mut rejected = false;
let mut failed = false;
for (index, element) in finished.into_iter().enumerate() {
let Some(Element { invoked, decision }) = element else {
continue;
};
let Settled {
result,
collected,
elapsed,
errors_before,
marked,
..
} = settle(rc, env, site.task, invoked, Some(index));
let ok = result.is_ok();
let (rejected_here, failed_here) = (decision.rejected(), decision.failed());
if let Some((segs, at, base)) = slot.as_mut() {
if let Some(last) = segs.last_mut() {
last.set_index(index);
}
let value = match collected {
Some(value) if !rejected_here && !failed_here => value,
_ => OwnedDataValue::Null,
};
if ok && env.capture.captures() {
at.truncate(*base);
let _ = write!(at, "{index}");
env.changes.push(Change::write(
at.as_str(),
OwnedDataValue::Null,
value.clone(),
));
}
rc.set_segs(segs, value, None);
}
rejected |= rejected_here;
failed |= failed_here;
let done = Completed::new(env, site.task, Some(index), elapsed, errors_before, marked);
let flow = record::record(rc, env, wf, done, decision);
rc.tick();
match flow {
flow @ (Flow::Fail(_) | Flow::Stop(_)) => return flow,
Flow::Halt => {
halted = true;
break;
}
Flow::Next | Flow::Jump(_) => {}
}
}
policy.after_fan_out(halted, rejected, failed)
}