use super::budget::{ConditionErrorPolicy, DispatchKind, TaskBudget};
use super::cancel::{Limits, Stop};
use super::error_context::ErrorContext;
use super::fan_out::fan_out;
use super::host::call_host;
use super::message::{AuditEntry, AuditMode};
use super::observers::Observers;
use super::policy::{self, Flow};
use super::record::{self, Completed, TaskIds};
use super::trace::Capture;
use crate::base::{
AuthoredPath, Change, Error, ErrorKind, ErrorRecord, ErrorSite, ExprCause, Instant, ProgramId,
SiteKind, TaskIx, WorkflowIx,
};
use crate::def::WorkflowFailurePolicy;
use crate::expr::path::kind;
use crate::expr::{Expr, ExprEngine, Path};
use crate::function::builtins::Builtin;
use crate::function::{CallLayer, SyncCtx, eval, test};
use crate::observe::{Event, Selection, StepResult, TraceStep};
use crate::program::{CallSite, Callee, CompiledWorkflow, CondKind, CondSite, LoopSite, Op};
use crate::state::{Owner, RunContext};
use chrono::{DateTime, Utc};
use datavalue::{DataValue, OwnedDataValue};
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;
#[derive(Debug)]
pub(crate) enum Exit {
Completed,
Halted,
Failed,
Stopped(Error),
Limited(Stop),
}
#[derive(Debug, Default)]
pub(crate) struct Frame {
pub(crate) loop_counter: Option<i64>,
pub(crate) sweeps: u32,
counter: i64,
items: Option<Vec<OwnedDataValue>>,
begun: bool,
pub(crate) started: Option<Instant>,
}
impl Frame {
fn in_sweep(&self, wf: &CompiledWorkflow) -> bool {
wf.loops.is_empty() || self.loop_counter.is_some()
}
}
pub(crate) struct RunEnv<'r, 'p> {
pub(crate) program: ProgramId,
pub(crate) workflow_ix: WorkflowIx,
pub(crate) task_base: u32,
pub(crate) channel: Option<&'r str>,
pub(crate) routing_bucket: Option<u8>,
pub(crate) layers: &'r [Arc<dyn CallLayer>],
pub(crate) limits: Limits<'r>,
pub(crate) condition_error_policy: ConditionErrorPolicy,
pub(crate) budget: &'r TaskBudget<'r>,
pub(crate) ops: u32,
pub(crate) exprs: &'p ExprEngine,
pub(crate) payload: &'r OwnedDataValue,
pub(crate) errors: &'r mut Vec<ErrorRecord>,
pub(crate) audit: &'r mut Vec<AuditEntry>,
pub(crate) mode: AuditMode,
pub(crate) capture: CaptureMode,
pub(crate) watch: &'r mut Watch<'p>,
pub(crate) now: DateTime<Utc>,
pub(crate) now_text: &'r mut Option<Arc<str>>,
pub(crate) changes: Vec<Change>,
pub(crate) workflow_id: &'p Arc<str>,
pub(crate) task_ids: &'p [Arc<str>],
pub(crate) failed: bool,
pub(crate) frame: Frame,
}
pub(crate) struct Watch<'p> {
pub(crate) observers: Observers<'p>,
pub(crate) timed: bool,
pub(crate) trace: Option<Box<Capture>>,
pub(crate) error_context: Option<&'p ErrorContext>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum CaptureMode {
Plain { audit: bool },
Watched { audit: bool, traced: bool },
}
impl CaptureMode {
pub(crate) fn new(capture_audit: bool, watch: &Watch<'_>) -> Self {
let traced = watch.trace.as_deref().is_some_and(Capture::changes);
if watch.trace.is_some() || watch.error_context.is_some() || watch.observers.tasks() {
Self::Watched {
audit: capture_audit,
traced,
}
} else {
debug_assert!(!watch.timed, "only a task observer or a trace times a call");
Self::Plain {
audit: capture_audit,
}
}
}
#[inline(always)]
pub(crate) fn captures(self) -> bool {
match self {
Self::Plain { audit } => audit,
Self::Watched { audit, traced } => audit || traced,
}
}
#[inline(always)]
pub(crate) fn audit(self) -> bool {
match self {
Self::Plain { audit } | Self::Watched { audit, .. } => audit,
}
}
#[inline(always)]
pub(crate) fn traced(self) -> bool {
matches!(self, Self::Watched { traced: true, .. })
}
#[inline(always)]
pub(crate) fn is_watched(self) -> bool {
matches!(self, Self::Watched { .. })
}
}
pub const CHECK_EVERY: u32 = 64;
impl<'p> RunEnv<'_, 'p> {
fn tick(&mut self, rc: &mut RunContext<'_, '_>) -> Result<(), Stop> {
rc.tick();
if self.limits.is_set() {
self.ops = self.ops.wrapping_add(1);
if self.ops.is_multiple_of(CHECK_EVERY) {
self.limits.check()?;
}
}
Ok(())
}
pub(crate) fn now_text(&mut self) -> &Arc<str> {
let now = self.now;
self.now_text
.get_or_insert_with(|| Arc::from(now.to_rfc3339()))
}
pub(crate) fn traces_changes(&self) -> bool {
self.capture.traced()
}
pub(crate) fn ids(&self, task: u32) -> TaskIds<'p> {
TaskIds {
workflow: self.workflow_id,
task: &self.task_ids[task as usize],
}
}
pub(crate) fn workflow_record(&self, error: &Error) -> ErrorRecord {
let mut record = ErrorRecord::from(error);
record.workflow_id = Some(Arc::clone(self.workflow_id));
record
}
pub(crate) fn task_error_record(&mut self, task: u32, error: &Error) -> ErrorRecord {
let mut record = ErrorRecord::from(error);
let ids = self.ids(task);
let now = self.now_text();
record.stamp(ids.workflow, Some(ids.task), now, None);
record
}
pub(crate) fn site(&self, kind: SiteKind, authored_path: AuthoredPath) -> ErrorSite {
ErrorSite {
kind,
workflow_id: Arc::clone(self.workflow_id),
task_id: None,
group_id: None,
rule_index: None,
mapping_index: None,
target_path: None,
authored_path: Some(authored_path),
loop_counter: self.frame.loop_counter,
}
}
pub(crate) fn task_site(
&self,
kind: SiteKind,
task: u32,
authored_path: AuthoredPath,
) -> ErrorSite {
ErrorSite {
task_id: Some(Arc::clone(self.ids(task).task)),
..self.site(kind, authored_path)
}
}
pub(crate) fn workflow_step(&self, result: StepResult) -> TraceStep {
TraceStep::new(self.workflow_ix, self.workflow_id, result)
}
pub(crate) fn task_step(&self, task: u32, result: StepResult) -> TraceStep {
let mut step = self.workflow_step(result);
step.task = Some(TaskIx::new(self.task_base + task));
step.task_id = Some(Arc::clone(&self.task_ids[task as usize]));
step.loop_counter = self.frame.loop_counter;
step
}
pub(crate) fn push_step(&mut self, step: TraceStep) {
if let Some(trace) = self.watch.trace.as_deref_mut() {
trace.push(step);
if let CaptureMode::Watched { traced, .. } = &mut self.capture {
*traced = trace.changes();
}
}
}
}
fn fail_to(op: Op) -> u32 {
match op {
Op::Call { fail_to, .. } | Op::FanOut { fail_to, .. } => fail_to,
Op::LoopInit { exit_to, .. } => exit_to,
Op::Cond { .. } | Op::LoopNext { .. } | Op::Halt => {
unreachable!("only a call, a fan-out or a loop's start fails")
}
}
}
pub(crate) async fn run_workflow<'p>(
rc: &mut RunContext<'_, 'p>,
wf: &'p CompiledWorkflow,
env: &mut RunEnv<'_, 'p>,
) -> (Exit, Selection) {
let mut selection = Selection::Ran;
let mut unjudged = true;
env.frame.sweeps = u32::from(wf.loops.is_empty());
if !wf.judged_first() {
begin(env);
}
let mut pc = 0;
let exit = loop {
let Some(&op) = wf.ops.get(pc) else {
break if env.failed {
Exit::Failed
} else {
Exit::Completed
};
};
if let Err(e) = env.tick(rc) {
break Exit::Limited(e);
}
let flow = match op {
Op::Cond {
cond,
kind,
else_to,
} => {
let cond_site = &wf.conds[cond as usize];
let passed = test(rc, env.exprs, env.budget, &cond_site.expr)
.map_err(|e| attach_cond_site(env, cond_site, kind, e));
if env.condition_error_policy == ConditionErrorPolicy::Stop
&& let Err(e) = passed
{
match kind {
CondKind::Workflow => {
if env.frame.in_sweep(wf) {
env.frame.sweeps -= 1;
}
if std::mem::take(&mut unjudged) {
selection = Selection::ConditionFailed(e.clone());
}
record::workflow_failure(env, &e);
}
CondKind::Group => {
record::workflow_failure(env, &e);
}
CondKind::Task => {
if let Some(Op::Call { site, .. } | Op::FanOut { site, .. }) =
wf.ops.get(pc + 1)
{
record::task_condition_failure(
env,
wf.calls[*site as usize].task,
&e,
);
} else {
record::workflow_failure(env, &e);
}
}
}
break Exit::Stopped(e);
}
let admitted = matches!(passed, Ok(true));
if kind == CondKind::Workflow {
if !admitted && env.frame.in_sweep(wf) {
env.frame.sweeps -= 1;
}
if std::mem::take(&mut unjudged) {
selection = match passed {
Ok(true) => {
begin(env);
Selection::Ran
}
Ok(false) => {
skipped_workflow(env, None);
Selection::SkippedByCondition
}
Err(e) => {
skipped_workflow(env, Some(&e));
Selection::ConditionFailed(e)
}
};
}
} else if !admitted && env.watch.trace.is_some() {
skipped_calls(env, wf, kind, pc, else_to, passed.as_ref().err());
}
if admitted {
Flow::Next
} else {
Flow::Jump(else_to)
}
}
Op::Call { site, .. } => {
let site = &wf.calls[site as usize];
match &site.callee {
Callee::Builtin(builtin) => call(rc, env, wf, site, builtin),
Callee::Host(bound) => call_host(rc, env, wf, site, &**bound).await,
}
}
Op::FanOut { site, fan, .. } => {
let site = &wf.calls[site as usize];
let Callee::Host(bound) = &site.callee else {
unreachable!("compile_one checks that a fan-out calls a host function")
};
Box::pin(fan_out(rc, env, wf, site, &wf.fans[fan as usize], &**bound)).await
}
Op::LoopInit { lp, exit_to } => loop_init(rc, env, &wf.loops[lp as usize], exit_to),
Op::LoopNext {
lp,
body_to,
exit_to,
} => loop_next(rc, env, &wf.loops[lp as usize], body_to, exit_to),
Op::Halt => Flow::Halt,
};
pc = match flow {
Flow::Next => pc + 1,
Flow::Jump(to) => to as usize,
Flow::Halt => break Exit::Halted,
Flow::Stop(e) => break Exit::Limited(e),
Flow::Abort(e) => break Exit::Stopped(e),
Flow::Fail(e) => match wf.on_failure {
WorkflowFailurePolicy::Continue => {
env.failed = true;
fail_to(op) as usize
}
WorkflowFailurePolicy::Fail => break Exit::Stopped(e),
},
};
};
if matches!(selection, Selection::Ran) {
begin(env);
}
(exit, selection)
}
fn begin(env: &mut RunEnv<'_, '_>) {
if !env.watch.observers.workflows() || std::mem::replace(&mut env.frame.begun, true) {
return;
}
if env.watch.observers.timed_workflows() {
env.frame.started = Some(record::now());
}
env.watch.observers.emit(Event::WorkflowStarted {
workflow: env.workflow_ix,
id: env.workflow_id,
});
}
fn skipped_workflow(env: &mut RunEnv<'_, '_>, error: Option<&Error>) {
if env.watch.trace.is_none() {
return;
}
let mut step = env.workflow_step(StepResult::Skipped);
if let Some(e) = error {
step.errors.push(env.workflow_record(e));
}
env.push_step(step);
}
fn attach_cond_site(
env: &RunEnv<'_, '_>,
cond_site: &CondSite,
kind: CondKind,
error: Error,
) -> Error {
let site_kind = match kind {
CondKind::Workflow => SiteKind::WorkflowCondition,
CondKind::Group => SiteKind::GroupCondition,
CondKind::Task => SiteKind::TaskCondition,
};
let step_arc = cond_site.step.as_deref().map(Arc::<str>::from);
let (task_id, group_id) = match kind {
CondKind::Workflow => (None, None),
CondKind::Group => (None, step_arc),
CondKind::Task => (step_arc, None),
};
error.with_site(ErrorSite {
task_id,
group_id,
..env.site(site_kind, cond_site.path.clone())
})
}
fn skipped_calls(
env: &mut RunEnv<'_, '_>,
wf: &CompiledWorkflow,
kind: CondKind,
pc: usize,
else_to: u32,
error: Option<&Error>,
) {
let span = match kind {
CondKind::Group => wf.ops.get(pc + 1..else_to as usize),
CondKind::Task => wf.ops.get(pc + 1..pc + 2),
CondKind::Workflow => None,
};
for op in span.unwrap_or_default() {
if let Op::Call { site, .. } | Op::FanOut { site, .. } = *op {
let task = wf.calls[site as usize].task;
let mut step = env.task_step(task, StepResult::Skipped);
if let Some(e) = error {
step.errors.push(env.task_error_record(task, e));
}
env.push_step(step);
}
}
}
fn loop_init<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
site: &'p LoopSite,
exit_to: u32,
) -> Flow {
if let Some(over) = &site.over {
let over_site = ErrorSite {
loop_counter: None,
..env.site(
SiteKind::LoopOver,
AuthoredPath::root().join("loop").join("over"),
)
};
match elements(
rc,
env.exprs,
env.budget,
over,
Over::Loop(env.workflow_id),
over_site,
) {
Ok(items) => env.frame.items = Some(items.into_owned()),
Err(e) => {
record::workflow_failure(env, &e);
return if *e.kind() == ErrorKind::Budget && env.budget.is_run_limited() {
Flow::Abort(e)
} else {
Flow::Fail(e)
};
}
}
}
env.frame.counter = site.init;
enter(rc, env, site, Flow::Next, exit_to)
}
fn loop_next<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
site: &'p LoopSite,
body_to: u32,
exit_to: u32,
) -> Flow {
env.frame.counter = env.frame.counter.saturating_add(site.increment);
enter(rc, env, site, Flow::Jump(body_to), exit_to)
}
fn enter<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
site: &'p LoopSite,
sweep: Flow,
exit_to: u32,
) -> Flow {
let traced = env.traces_changes();
let mut changes = Vec::new();
let counter = env.frame.counter;
if let Some(path) = &site.counter {
engine_write(
rc,
path,
traced,
&mut changes,
|| OwnedDataValue::from_i64(counter),
|_| DataValue::from_i64(counter),
);
}
let element = match &env.frame.items {
None => None,
Some(items) => usize::try_from(counter).ok().filter(|&i| i < items.len()),
};
let ends = if counter >= site.max || (env.frame.items.is_some() && element.is_none()) {
Some(Flow::Jump(exit_to))
} else {
env.limits.check().err().map(Flow::Stop)
};
if let Some(flow) = ends {
if site.counter.is_some() {
sweep_step(env, counter, changes);
}
return flow;
}
let _guard = match env.budget.charge_dispatch(DispatchKind::LoopStep {
workflow_id: env.workflow_id,
iteration: u64::from(env.frame.sweeps),
}) {
Ok(guard) => guard,
Err(e) => {
if site.counter.is_some() {
sweep_step(env, counter, changes);
}
let e = e.with_site(ErrorSite {
loop_counter: Some(counter),
..env.site(SiteKind::LoopOver, AuthoredPath::root().join("loop"))
});
record::workflow_failure(env, &e);
return Flow::Abort(e);
}
};
if let Some(item) = element.and_then(|i| env.frame.items.as_mut()?.get_mut(i)) {
let item = std::mem::take(item);
if let Some(path) = &site.r#as {
rc.set_recorded(path, item, None, traced.then_some(&mut changes));
}
}
if let Some(path) = &site.scratch {
engine_write(
rc,
path,
traced,
&mut changes,
|| OwnedDataValue::Object(Vec::new()),
|_| DataValue::Object(&[]),
);
}
env.frame.loop_counter = Some(counter);
env.frame.sweeps = env.frame.sweeps.saturating_add(1);
sweep_step(env, counter, changes);
sweep
}
fn engine_write<'p>(
rc: &mut RunContext<'_, 'p>,
path: &Path,
traced: bool,
changes: &mut Vec<Change>,
owned: impl FnOnce() -> OwnedDataValue,
view: impl for<'q> FnOnce(&'q Owner<'p>) -> DataValue<'q>,
) {
let old = traced.then(|| rc.get_or_null(path));
if let Some(old) = old {
let new = owned();
let new_clone = new.clone();
rc.engine_set(path, || new_clone, view);
changes.push(Change::write(Arc::clone(path.dotted()), old, new));
} else {
rc.engine_set(path, owned, view);
}
}
fn sweep_step(env: &mut RunEnv<'_, '_>, counter: i64, changes: Vec<Change>) {
if env.watch.trace.is_none() {
return;
}
let mut step = env.workflow_step(StepResult::Executed);
step.changes = changes;
step.loop_counter = Some(counter);
env.push_step(step);
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Over<'a> {
Loop(&'a str),
FanOut(&'a str),
}
impl fmt::Display for Over<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Loop(id) => write!(f, "Workflow {id}: loop.over"),
Self::FanOut(id) => write!(f, "Task {id}: for_each.over"),
}
}
}
pub(super) fn elements<'e>(
rc: &mut RunContext<'_, '_>,
exprs: &ExprEngine,
budget: &TaskBudget<'_>,
over: &'e Expr,
owner: Over<'_>,
site: ErrorSite,
) -> Result<Cow<'e, [OwnedDataValue]>, Error> {
let value = eval(rc, exprs, budget, over).map_err(|e| {
let msg = format!("{owner} failed to evaluate: {}", e.record_text());
e.with_message(msg).with_site(site.clone())
})?;
match value {
Cow::Owned(OwnedDataValue::Array(items)) => Ok(Cow::Owned(items)),
Cow::Borrowed(OwnedDataValue::Array(items)) => Ok(Cow::Borrowed(items)),
other => Err(Error::new(
ErrorKind::Eval,
format!("{owner} must evaluate to an array, got {}", kind(&other)),
)
.with_expr_cause(ExprCause::type_mismatch(None))
.with_site(site)),
}
}
fn call<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
wf: &'p CompiledWorkflow,
site: &'p CallSite,
builtin: &'p Builtin,
) -> Flow {
let errors_before = env.errors.len();
let meta = &wf.tasks[site.task as usize];
let guard = match env.budget.charge_dispatch(DispatchKind::InlineBuiltin {
workflow_id: &wf.id,
task_id: &meta.id,
function: &meta.function,
}) {
Ok(guard) => guard,
Err(e) => {
let e = e.with_site(env.task_site(SiteKind::Task, site.task, meta.path.clone()));
let decision = policy::forced_abort(env.ids(site.task).task, e);
let done = Completed::new(env, site.task, None, None, errors_before, false);
return record::record(rc, env, wf, done, decision);
}
};
let (traced, started) = if env.capture.is_watched() {
(env.traces_changes(), env.watch.timed.then(record::now))
} else {
(false, None)
};
let mapping_ends = if traced {
env.watch.trace.as_deref_mut().map(Capture::mapping_ends)
} else {
None
};
let mut cx = SyncCtx {
rc: &mut *rc,
exprs: env.exprs,
budget: env.budget,
condition_error_policy: env.condition_error_policy,
payload: env.payload,
workflow_id: env.workflow_id,
task_id: &env.task_ids[site.task as usize],
task_path: &meta.path,
loop_counter: env.frame.loop_counter,
capture: env.capture.captures(),
traced,
changes: &mut env.changes,
marked: false,
mapping_ends,
errors: &mut *env.errors,
now: env.now,
now_text: &mut *env.now_text,
};
let result = builtin.call(&mut cx);
let mut marked = cx.marked;
drop(guard);
let elapsed = started.map(record::since);
if result.is_err() {
if traced {
for change in &mut env.changes {
change.trace_only = true;
}
marked = true;
} else {
env.changes.clear();
}
}
let decision = match result {
Err(mut e) => {
if e.site().is_none() {
e = e.with_site(env.task_site(SiteKind::Task, site.task, meta.path.clone()));
}
let force_stop = (env.condition_error_policy == ConditionErrorPolicy::Stop
&& e.site()
.is_some_and(|s| s.kind == SiteKind::FilterCondition))
|| (*e.kind() == ErrorKind::Budget && env.budget.is_run_limited());
if force_stop {
policy::forced_abort(env.ids(site.task).task, e)
} else {
policy::decide(site.policy, env.ids(site.task).task, Err(e))
}
}
Ok(outcome) => policy::decide(site.policy, env.ids(site.task).task, Ok(outcome)),
};
let done = Completed::new(env, site.task, None, elapsed, errors_before, marked);
record::record(rc, env, wf, done, decision)
}