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::{Change, Error, ErrorKind, ErrorRecord, Instant, ProgramId, 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, 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) 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 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 passed = test(rc, env.exprs, &wf.conds[cond as usize].expr);
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);
}
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::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 skipped_calls(
env: &mut RunEnv<'_, '_>,
wf: &CompiledWorkflow,
kind: CondKind,
pc: usize,
else_to: u32,
) {
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 step = env.task_step(wf.calls[site as usize].task, StepResult::Skipped);
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 {
match elements(rc, env.exprs, over, Over::Loop(env.workflow_id)) {
Ok(items) => env.frame.items = Some(items.into_owned()),
Err(e) => {
record::workflow_failure(env, &e);
return 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;
}
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,
over: &'e Expr,
owner: Over<'_>,
) -> Result<Cow<'e, [OwnedDataValue]>, Error> {
let value = eval(rc, exprs, over).map_err(|e| {
Error::new(
e.kind().clone(),
format!("{owner} failed to evaluate: {}", e.record_text()),
)
})?;
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)),
)),
}
}
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 workflow_id = env.workflow_id;
let task_id = &env.task_ids[site.task as usize];
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,
payload: env.payload,
workflow_id,
task_id,
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;
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 = policy::decide(site.policy, env.ids(site.task).task, result);
let done = Completed::new(env, site.task, None, elapsed, errors_before, marked);
record::record(rc, env, wf, done, decision)
}