use super::RunStatus;
use super::builder::EngineBuilder;
use super::cancel::{Limits, timers_available};
use super::error_context::ErrorContext;
use super::interp::{CaptureMode, Exit, Frame, RunEnv, Watch, run_workflow};
use super::message::{Message, finish_audit, keeps_entries};
use super::observers::Observers;
use super::options::RunOptions;
use super::record;
use super::report::{Record, Run};
use super::trace::Capture;
use crate::def::Rollout;
use crate::expr::Path;
use crate::observe::{
Event, Observer, Selection, StepResult, TraceStep, WorkflowExit, WorkflowRecord,
};
use crate::program::{Compiler, Program};
use crate::state::{RunContext, Tuning};
use chrono::Utc;
use datavalue::OwnedDataValue;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Default)]
#[non_exhaustive]
pub struct EngineConfig {
pub observer: Option<Arc<dyn Observer>>,
pub error_context: Option<ErrorContext>,
}
impl EngineConfig {
#[must_use]
pub fn observer(mut self, o: Arc<dyn Observer>) -> Self {
self.observer = Some(o);
self
}
#[must_use]
pub fn error_context(mut self, ec: ErrorContext) -> Self {
self.error_context = Some(ec);
self
}
}
impl fmt::Debug for EngineConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let observer = if self.observer.is_some() {
"present"
} else {
"absent"
};
f.debug_struct("EngineConfig")
.field("observer", &format_args!("{observer}"))
.field("error_context", &self.error_context)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct Engine {
inner: Arc<Inner>,
}
struct Inner {
program: Program,
config: EngineConfig,
}
pub(crate) mod slots {
pub(crate) const PROGRESS: &str = "progress";
pub(crate) const PROCESSED_AT: &str = "processed_at";
pub(crate) const ENGINE_VERSION: &str = "engine_version";
pub(crate) const CHANNEL: &str = "channel";
pub(crate) const ALL: [&str; 4] = [PROGRESS, PROCESSED_AT, ENGINE_VERSION, CHANNEL];
}
pub(crate) fn metadata(key: &str) -> Path {
format!("metadata.{key}")
.parse()
.expect("a metadata key is a path")
}
impl Engine {
pub fn new(program: Program, cfg: EngineConfig) -> Self {
Self {
inner: Arc::new(Inner {
program,
config: cfg,
}),
}
}
pub fn builder() -> EngineBuilder {
EngineBuilder::new()
}
pub async fn run(&self, msg: &mut Message) -> Run {
static DEFAULT: RunOptions<'static> = RunOptions::DEFAULT;
self.run_with(msg, &DEFAULT).await
}
pub async fn run_with(&self, msg: &mut Message, opts: &RunOptions<'_>) -> Run {
let program = &self.inner.program;
let mut watch = {
let observers = Observers::attach(self.inner.config.observer.as_deref(), opts.observer);
Watch {
timed: observers.timed_tasks(),
observers,
trace: None,
error_context: self.inner.config.error_context.as_ref(),
}
};
let started = watch.observers.timed().then(record::now);
let now = Utc::now();
let now_str = now.to_rfc3339();
let mut now_text: Option<Arc<str>> = None;
let parts = msg.parts();
stamp(parts.context, now_str, opts.channel);
if let Some(ec) = &self.inner.config.error_context {
ec.reset(parts.context);
}
if let Some(options) = &opts.trace {
let trace = Capture::new(options.clone(), parts.context);
watch.timed |= trace.timings();
watch.trace = Some(Box::new(trace));
}
let mut rc = RunContext::new(parts.context, Tuning::default());
let active = program.active(opts.channel);
watch.observers.emit(Event::RunStarted {
program: program.id(),
message_id: parts.id,
channel: opts.channel,
considered: active.len(),
});
let capture_audit = opts.capture_changes && keeps_entries(opts.audit);
let mut records = Vec::with_capacity(active.len());
let mut ops = 0;
let timers = opts.deadline.is_some() && timers_available();
let bucket = opts.routing_bucket.map(|b| b.min(Rollout::MAX_BUCKET));
let stopped = 'run: {
for &ix in active {
let wf = program.compiled(ix);
if let (Some(rollout), Some(bucket)) = (wf.rollout, bucket)
&& !rollout.accepts(bucket)
{
if let Some(trace) = watch.trace.as_deref_mut() {
let (workflow_id, _) = program.ids(ix);
trace.push(TraceStep::new(ix, workflow_id, StepResult::Skipped));
}
records.push(Record {
ix,
selection: Selection::SkippedByRollout,
sweeps: 0,
exit: None,
});
continue;
}
let (workflow_id, task_ids) = program.ids(ix);
let mut env = RunEnv {
program: program.id(),
workflow_ix: ix,
task_base: program.task_base(ix),
channel: opts.channel,
routing_bucket: bucket,
layers: opts.layers,
limits: Limits {
deadline: opts.deadline,
cancel: opts.cancel,
timers,
},
ops,
exprs: program.registry().exprs(),
payload: parts.payload,
errors: &mut *parts.errors,
audit: &mut *parts.audit,
mode: opts.audit,
capture: CaptureMode::new(capture_audit, &watch),
watch: &mut watch,
now,
now_text: &mut now_text,
changes: Vec::new(),
workflow_id,
task_ids,
failed: false,
frame: Frame::default(),
};
let (exit, selection) = run_workflow(&mut rc, wf, &mut env).await;
ops = env.ops;
let ran = matches!(selection, Selection::Ran);
let (exit, stop) = match exit {
Exit::Completed => (WorkflowExit::Completed, None),
Exit::Halted => (WorkflowExit::Halted, None),
Exit::Failed => (WorkflowExit::Failed, None),
Exit::Stopped(e) => (WorkflowExit::Stopped, Some(RunStatus::Stopped(e))),
Exit::Limited(stop) => (WorkflowExit::Stopped, Some(stop.status())),
};
let considered = Record {
ix,
selection,
sweeps: env.frame.sweeps,
exit: ran.then_some(exit),
};
let workflow_started = env.frame.started;
if ran && watch.observers.workflows() {
let view = WorkflowRecord {
id: workflow_id,
index: ix,
selection: &considered.selection,
sweeps: considered.sweeps,
exit: considered.exit,
};
watch.observers.emit(Event::WorkflowFinished {
record: &view,
elapsed: workflow_started.map(record::since),
});
}
records.push(considered);
if let Some(status) = stop {
break 'run Some(status);
}
}
None
};
let stats = rc.finish();
finish_audit(parts.audit, opts.audit);
let status = match stopped {
Some(status) => status,
None if !parts.errors.is_empty() => RunStatus::CompletedWithErrors,
None => RunStatus::Completed,
};
watch.observers.emit(Event::RunFinished {
message_id: parts.id,
status: &status,
errors: parts.errors.len(),
elapsed: started.map(record::since),
});
Run::new(
program.clone(),
status,
records,
stats,
watch.trace.map(|t| Box::new(t.finish())),
)
}
pub fn compiler(&self) -> Compiler {
let program = &self.inner.program;
program.registry().compiler(program.options().clone())
}
#[must_use]
pub fn with_program(&self, program: Program) -> Self {
Self::new(program, self.inner.config.clone())
}
pub fn program(&self) -> &Program {
&self.inner.program
}
#[doc(hidden)]
pub fn config(&self) -> &EngineConfig {
&self.inner.config
}
}
fn stamp(context: &mut OwnedDataValue, now: String, channel: Option<&str>) {
let OwnedDataValue::Object(roots) = context else {
return;
};
let Some((_, OwnedDataValue::Object(meta))) = roots.iter_mut().find(|(k, _)| k == "metadata")
else {
return;
};
let need = 2 + usize::from(channel.is_some());
if meta.capacity() < meta.len() + need {
meta.reserve(need);
}
match meta.iter_mut().find(|(k, _)| k == slots::PROCESSED_AT) {
Some((_, slot)) => *slot = OwnedDataValue::String(now),
None => meta.push((slots::PROCESSED_AT.to_owned(), OwnedDataValue::String(now))),
}
put_meta_str(meta, slots::ENGINE_VERSION, crate::VERSION);
if let Some(channel) = channel {
put_meta_str(meta, slots::CHANNEL, channel);
}
}
fn put_meta_str(meta: &mut Vec<(String, OwnedDataValue)>, key: &str, text: &str) {
match meta.iter_mut().find(|(k, _)| k == key) {
Some((_, OwnedDataValue::String(s))) => {
if s != text {
s.clear();
s.push_str(text);
}
}
Some((_, slot)) => *slot = OwnedDataValue::String(text.to_owned()),
None => meta.push((key.to_owned(), OwnedDataValue::String(text.to_owned()))),
}
}
impl fmt::Debug for Engine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Engine")
.field("program", &self.inner.program)
.field("config", &self.inner.config)
.finish()
}
}