///|
/// One node's turn to run, as the executor sees it. `input` is a function so the
/// state is encoded only when it actually crosses a process boundary; `run` is
/// what an in-process executor calls, and what an executor that ships the task
/// elsewhere ignores — it finds the node by `graph` and `node` on the far side.
pub(all) struct Work {
graph : String
node : String
thread : String
step : Int32
attempt : Int32
input : () -> Json
run : async () -> Array[Json]
}
///|
/// What an executor reports back for one work item. Exactly one of `values` and
/// `error` is set: a failed node's error travels as text, because the executor
/// may not share a process with the graph that would have to match on it.
pub(all) struct Done {
values : Array[Json]?
error : String?
}
///|
/// Where a superstep's tasks run. The engine hands over the whole superstep at
/// once, which is what makes a fan-out parallel; the results come back in the
/// order the work was given, so merging stays deterministic.
pub(open) trait Executor {
async fn each(Self, Array[Work]) -> Array[Done]
}
///|
/// Runs every task of a superstep in this process, concurrently.
priv struct Inline {}
///|
impl Executor for Inline with fn each(_self, works) -> Array[Done] raise {
let jobs : Array[async () -> Done] = []
for work in works {
jobs.push(async fn() {
let outcome = Ok((work.run)()) catch { error => Err(error) }
match outcome {
Ok(values) => { values: Some(values), error: None, }
Err(error) => { values: None, error: Some(error_text(error)), }
}
})
}
@async.all(jobs[:])
}
///|
/// The default executor: every task in this process, at the same time.
pub let inline : &Executor = Inline::{ }