use super::engine::{metadata, slots};
use super::interp::RunEnv;
use super::message::{AuditEntry, keeps_entries, record_audit};
use super::policy::{Decision, Flow};
use crate::base::{Change, Error, Instant};
use crate::expr::Path;
use crate::function::task_record;
use crate::observe::{Event, StepResult};
use crate::program::CompiledWorkflow;
use crate::state::RunContext;
use datavalue::{DataValue, NumberValue, OwnedDataValue};
use std::borrow::Cow;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
pub(super) static PROGRESS: LazyLock<Path> = LazyLock::new(|| metadata(slots::PROGRESS));
#[derive(Clone, Copy, Debug)]
pub(crate) struct TaskIds<'p> {
pub(crate) workflow: &'p Arc<str>,
pub(crate) task: &'p Arc<str>,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Stamp {
pub(crate) loop_counter: Option<i64>,
pub(crate) element_index: Option<usize>,
}
impl Stamp {
pub(crate) fn sweep(env: &RunEnv<'_, '_>) -> Self {
Self {
loop_counter: env.frame.loop_counter,
element_index: None,
}
}
}
pub(crate) fn now() -> Instant {
#[cfg(test)]
super::tests::on_clock_read();
#[cfg(not(target_arch = "wasm32"))]
{
tokio::time::Instant::now().into_std()
}
#[cfg(target_arch = "wasm32")]
{
Instant::now()
}
}
pub(crate) fn since(start: Instant) -> Duration {
now().saturating_duration_since(start)
}
pub(crate) struct Completed {
pub(crate) task: u32,
pub(crate) stamp: Stamp,
pub(crate) elapsed: Option<Duration>,
pub(crate) errors_before: usize,
pub(crate) marked: bool,
}
impl Completed {
pub(crate) fn new(
env: &RunEnv<'_, '_>,
task: u32,
element_index: Option<usize>,
elapsed: Option<Duration>,
errors_before: usize,
marked: bool,
) -> Self {
Self {
task,
stamp: Stamp {
element_index,
..Stamp::sweep(env)
},
elapsed,
errors_before,
marked,
}
}
}
#[inline(always)]
pub(crate) fn record<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
wf: &'p CompiledWorkflow,
done: Completed,
decision: Decision,
) -> Flow {
let Decision {
status,
record,
flow,
} = decision;
#[cfg(test)]
super::tests::completed(env, &done, status, &flow);
if env.capture.is_watched() || done.marked {
let failed = matches!(flow, Flow::Fail(_) | Flow::Stop(_));
watched(rc, env, wf, done, status, record, failed);
} else {
plain(rc, env, done.task, done.stamp, status, record);
}
flow
}
#[inline(always)]
fn record_core<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
ids: TaskIds<'p>,
stamp: Stamp,
status: u16,
record: Option<(Cow<'static, str>, String)>,
audit_changes: impl FnOnce(&mut RunEnv<'_, 'p>) -> Vec<Change>,
) -> Option<Change> {
if keeps_entries(env.mode) {
let changes = audit_changes(env);
audit(env, ids, stamp, status, changes);
}
let old = env.traces_changes().then(|| rc.get_or_null(&PROGRESS));
write_progress(rc, ids.workflow, ids.task, status);
let progress_change = old.map(|old| {
Change::write(
Arc::clone(PROGRESS.dotted()),
old,
rc.get_or_null(&PROGRESS),
)
});
if let Some(record) = record {
push_record(env, ids, stamp, record);
}
progress_change
}
#[inline]
fn plain<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
task: u32,
stamp: Stamp,
status: Option<u16>,
record: Option<(Cow<'static, str>, String)>,
) {
if let Some(status) = status {
let ids = env.ids(task);
record_core(rc, env, ids, stamp, status, record, |env| {
std::mem::take(&mut env.changes)
});
}
env.changes.clear();
}
#[inline(always)]
fn audit(
env: &mut RunEnv<'_, '_>,
ids: TaskIds<'_>,
stamp: Stamp,
status: u16,
changes: Vec<Change>,
) {
record_audit(
env.audit,
env.mode,
AuditEntry {
workflow_id: Arc::clone(ids.workflow),
task_id: Arc::clone(ids.task),
timestamp: env.now,
changes,
status: usize::from(status),
loop_counter: stamp.loop_counter,
element_index: stamp.element_index,
},
);
}
#[inline(always)]
fn push_record(
env: &mut RunEnv<'_, '_>,
ids: TaskIds<'_>,
stamp: Stamp,
(code, message): (Cow<'static, str>, String),
) {
let now = env.now_text();
let mut record = task_record(code, message, ids.workflow, ids.task, now);
record.element_index = stamp.element_index;
env.errors.push(record);
}
#[cold]
#[inline(never)]
fn watched<'p>(
rc: &mut RunContext<'_, 'p>,
env: &mut RunEnv<'_, 'p>,
wf: &'p CompiledWorkflow,
done: Completed,
status: Option<u16>,
record: Option<(Cow<'static, str>, String)>,
failed: bool,
) {
let Completed {
task,
stamp,
elapsed,
errors_before,
marked,
} = done;
let ids = env.ids(task);
let traced = env.traces_changes();
let mut changes = match env.watch.trace.as_deref_mut() {
Some(trace) if traced => trace.take_prelude(),
_ => Vec::new(),
};
let prelude = changes.len();
if let Some(status) = status {
let progress = record_core(rc, env, ids, stamp, status, record, |env| {
if env.capture.audit() {
let mut changes = if traced {
env.changes.clone()
} else {
std::mem::take(&mut env.changes)
};
if marked {
changes.retain_mut(audited);
}
changes
} else {
Vec::new()
}
});
if let Some(progress) = progress {
changes.append(&mut env.changes);
changes.push(progress);
}
if let Some(ec) = env.watch.error_context
&& let Some(change) = ec.mirror(
rc,
&env.errors[errors_before..],
ids.workflow,
ids.task,
status,
traced,
)
{
changes.push(change);
}
} else if traced {
changes.append(&mut env.changes);
}
env.changes.clear();
if let Some(trace) = env.watch.trace.as_deref_mut() {
let timings = trace.timings();
let mapping_ends = trace.take_mapping_ends(prelude);
let mut step = env.task_step(
task,
if failed {
StepResult::Failed
} else {
StepResult::Executed
},
);
step.status = status;
step.changes = changes;
step.mapping_ends = mapping_ends;
#[cfg(test)]
super::tests::on_trace_work();
step.errors = env.errors[errors_before..].to_vec();
step.elapsed = elapsed.filter(|_| timings);
step.element_index = stamp.element_index;
env.push_step(step);
}
if env.watch.observers.tasks() {
let info = env.call_info(wf, task, stamp);
env.watch.observers.emit(Event::TaskFinished {
info: &info,
status,
elapsed,
});
}
}
fn audited(change: &mut Change) -> bool {
change.replaced = false;
!change.trace_only
}
pub(crate) fn workflow_failure(env: &mut RunEnv<'_, '_>, error: &Error) {
let mut record = env.workflow_record(error);
let workflow_id = env.workflow_id;
let now = env.now_text();
record.stamp(workflow_id, None, now, None);
if env.watch.trace.is_some() {
let mut step = env.workflow_step(StepResult::Failed);
#[cfg(test)]
super::tests::on_trace_work();
step.errors = vec![record.clone()];
env.push_step(step);
}
env.errors.push(record);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Key {
Workflow,
Task,
Status,
}
impl Key {
fn of(key: &str) -> Option<Self> {
match key {
"workflow_id" => Some(Self::Workflow),
"task_id" => Some(Self::Task),
"status_code" => Some(Self::Status),
_ => None,
}
}
fn name(self) -> &'static str {
match self {
Self::Workflow => "workflow_id",
Self::Task => "task_id",
Self::Status => "status_code",
}
}
fn leaf(self) -> &'static Path {
fn parse(key: Key) -> Path {
format!("metadata.progress.{}", key.name())
.parse()
.expect("a progress key is a path")
}
static WORKFLOW: LazyLock<Path> = LazyLock::new(|| parse(Key::Workflow));
static TASK: LazyLock<Path> = LazyLock::new(|| parse(Key::Task));
static STATUS: LazyLock<Path> = LazyLock::new(|| parse(Key::Status));
match self {
Self::Workflow => &WORKFLOW,
Self::Task => &TASK,
Self::Status => &STATUS,
}
}
fn value<'p>(self, workflow: &'p str, task: &'p str, status: u16) -> DataValue<'p> {
match self {
Self::Workflow => DataValue::String(workflow),
Self::Task => DataValue::String(task),
Self::Status => DataValue::Number(NumberValue::Integer(i64::from(status))),
}
}
}
const FRESH: [Key; 3] = [Key::Workflow, Key::Task, Key::Status];
fn in_place<'k>(keys: impl ExactSizeIterator<Item = &'k str>) -> Option<[Key; 3]> {
if keys.len() != 3 {
return None;
}
let mut order = FRESH;
for (slot, key) in order.iter_mut().zip(keys) {
*slot = Key::of(key)?;
}
Some(order)
}
fn overwrite_in_place(slot: &mut OwnedDataValue, workflow: &str, task: &str, status: u16) -> bool {
let OwnedDataValue::Object(pairs) = slot else {
return false;
};
let Some(order) = in_place(pairs.iter().map(|(k, _)| k.as_str())) else {
return false;
};
for ((_, value), key) in pairs.iter_mut().zip(order) {
match key {
Key::Workflow => overwrite_str(value, workflow),
Key::Task => overwrite_str(value, task),
Key::Status => *value = OwnedDataValue::from(status),
}
}
true
}
fn overwrite_str(slot: &mut OwnedDataValue, text: &str) {
match slot {
OwnedDataValue::String(s) => {
if s != text {
s.clear();
s.push_str(text);
}
}
_ => *slot = OwnedDataValue::String(text.to_owned()),
}
}
fn unchanged(old: DataValue<'_>, new: DataValue<'_>) -> bool {
match (old, new) {
(DataValue::String(a), DataValue::String(b)) => a == b,
(
DataValue::Number(NumberValue::Integer(a)),
DataValue::Number(NumberValue::Integer(b)),
) => a == b,
_ => false,
}
}
fn overwrite_in_view<'p>(
rc: &mut RunContext<'_, 'p>,
workflow: &'p str,
task: &'p str,
status: u16,
) -> bool {
rc.edit(|e| {
e.overwrite(&PROGRESS, |pairs| {
let order = in_place(pairs.iter().map(|(k, _)| *k))?;
Some(std::array::from_fn::<_, 3, _>(|i| {
let new = order[i].value(workflow, task, status);
(
order[i].leaf(),
(!unchanged(pairs[i].1, new)).then_some(new),
)
}))
})
})
}
pub(super) fn write_progress<'p>(
rc: &mut RunContext<'_, 'p>,
workflow: &'p str,
task: &'p str,
status: u16,
) {
if rc.engine_update(&PROGRESS, |slot| {
overwrite_in_place(slot, workflow, task, status)
}) {
return;
}
if rc.has_view() && overwrite_in_view(rc, workflow, task, status) {
return;
}
rc.engine_set(
&PROGRESS,
|| {
OwnedDataValue::Object(
FRESH
.iter()
.map(|key| {
let value = key.value(workflow, task, status).to_owned();
(key.name().to_owned(), value)
})
.collect(),
)
},
|owner| {
DataValue::Object(owner.arena.alloc_slice_copy(
&FRESH.map(|key| (key.name(), key.value(workflow, task, status))),
))
},
);
}