use crate::engine::error::{DataflowError, ErrorInfo, Result, service_error_code};
use crate::engine::executor::{
ArenaContext, evaluate_condition, evaluate_condition_in_arena, with_arena,
};
use crate::engine::functions::BoxedFunctionHandler;
use crate::engine::message::{AuditTrail, Change, Message};
use crate::engine::observer::{ExecutionObserver, TaskEvent};
use crate::engine::task::Task;
use crate::engine::task_executor::TaskExecutor;
use crate::engine::task_outcome::TaskOutcome;
use crate::engine::trace::{ExecutionStep, ExecutionTrace, duration_us_between};
use crate::engine::utils::set_nested_value;
use crate::engine::workflow::Workflow;
use chrono::{DateTime, Utc};
use core::time::Duration;
use datalogic_rs::Engine;
use datavalue::OwnedDataValue;
use log::{debug, error, info, warn};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
enum TaskControlFlow {
Continue,
HaltWorkflow,
}
fn next_async_boundary(tasks: &[Task], start: usize) -> usize {
let mut i = start;
while i < tasks.len() && tasks[i].function.is_sync_builtin() {
i += 1;
}
i
}
fn note_workflow_skip(trace: Option<&mut ExecutionTrace>, workflow_id: &str, reason: &str) {
debug!("Skipping workflow {} - {}", workflow_id, reason);
if let Some(t) = trace {
t.add_step(ExecutionStep::workflow_skipped(workflow_id));
}
}
fn rollout_admits(workflow: &Workflow, message: &Message) -> bool {
match workflow.rollout {
None => true,
Some(r) => match message.routing_bucket() {
None => true,
Some(b) => r.accepts(b),
},
}
}
fn new_progress_object(workflow_id: &str, task_id: &str, status: u16) -> OwnedDataValue {
OwnedDataValue::Object(vec![
(
"workflow_id".to_string(),
OwnedDataValue::String(workflow_id.to_string()),
),
(
"task_id".to_string(),
OwnedDataValue::String(task_id.to_string()),
),
(
"status_code".to_string(),
OwnedDataValue::from(u64::from(status)),
),
])
}
fn overwrite_progress_in_place(
fields: &mut [(String, OwnedDataValue)],
workflow_id: &str,
task_id: &str,
status: u16,
) -> bool {
if fields.len() != 3 {
return false;
}
let mut matched = 0;
for (k, v) in fields.iter_mut() {
match k.as_str() {
"workflow_id" => {
*v = OwnedDataValue::String(workflow_id.to_string());
matched += 1;
}
"task_id" => {
*v = OwnedDataValue::String(task_id.to_string());
matched += 1;
}
"status_code" => {
*v = OwnedDataValue::from(u64::from(status));
matched += 1;
}
_ => {}
}
}
matched == 3
}
fn write_progress_metadata(
context: &mut OwnedDataValue,
workflow_id: &str,
task_id: &str,
status: u16,
) {
if let OwnedDataValue::Object(top) = context {
if let Some((_, OwnedDataValue::Object(meta))) =
top.iter_mut().find(|(k, _)| k == "metadata")
{
match meta.iter_mut().find(|(k, _)| k == "progress") {
Some((_, slot)) => {
if let OwnedDataValue::Object(fields) = slot {
if overwrite_progress_in_place(fields, workflow_id, task_id, status) {
return;
}
}
*slot = new_progress_object(workflow_id, task_id, status);
}
None => {
meta.push((
"progress".to_string(),
new_progress_object(workflow_id, task_id, status),
));
}
}
return;
}
}
set_nested_value(
context,
"metadata.progress",
new_progress_object(workflow_id, task_id, status),
);
}
pub struct WorkflowExecutor {
task_executor: Arc<TaskExecutor>,
engine: Arc<Engine>,
observer: Option<Arc<dyn ExecutionObserver>>,
}
impl WorkflowExecutor {
pub fn new(task_executor: Arc<TaskExecutor>, engine: Arc<Engine>) -> Self {
Self {
task_executor,
engine,
observer: None,
}
}
pub fn with_observer(mut self, observer: Arc<dyn ExecutionObserver>) -> Self {
self.observer = Some(observer);
self
}
pub fn observer(&self) -> Option<&Arc<dyn ExecutionObserver>> {
self.observer.as_ref()
}
#[inline]
fn emit_task_event(
&self,
workflow: &Workflow,
task: &Task,
result: &Result<(TaskOutcome, Vec<Change>)>,
started_at: Option<DateTime<Utc>>,
) {
if let Some(observer) = self.observer.as_ref() {
let status = match result {
Ok((outcome, _)) => outcome.audit_status(),
Err(_) => Some(500),
};
let duration = started_at
.map(|s| Duration::from_micros(duration_us_between(s, Utc::now())))
.unwrap_or_default();
observer.task_finished(&TaskEvent {
workflow_id: &workflow.id,
task_id: &task.id,
function: task.function.function_name(),
status,
duration,
});
}
}
#[inline]
fn observer_clock(&self) -> Option<DateTime<Utc>> {
self.observer.as_ref().map(|_| Utc::now())
}
pub fn task_functions(&self) -> Arc<HashMap<String, BoxedFunctionHandler>> {
self.task_executor.task_functions()
}
pub async fn execute(
&self,
workflow: &Workflow,
message: &mut Message,
now: DateTime<Utc>,
) -> Result<bool> {
self.execute_inner(workflow, message, None, now).await
}
pub async fn execute_with_trace(
&self,
workflow: &Workflow,
message: &mut Message,
trace: &mut ExecutionTrace,
now: DateTime<Utc>,
) -> Result<bool> {
self.execute_inner(workflow, message, Some(trace), now)
.await
}
async fn execute_inner(
&self,
workflow: &Workflow,
message: &mut Message,
mut trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<bool> {
enum FirstStretch {
Skipped,
Halted,
Continue,
}
if !rollout_admits(workflow, message) {
note_workflow_skip(trace.as_deref_mut(), &workflow.id, "outside rollout bucket");
return Ok(false);
}
let tasks = &workflow.tasks;
let first_boundary = next_async_boundary(tasks, 0);
let first: Result<FirstStretch> =
if workflow.compiled_condition.is_none() && first_boundary == 0 {
Ok(FirstStretch::Continue)
} else {
with_arena(|arena| -> Result<FirstStretch> {
let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
let should_execute = match workflow.compiled_condition.as_ref() {
None => true,
Some(compiled) => evaluate_condition_in_arena(
&self.engine,
Some(compiled),
arena_ctx.as_data_value(),
arena,
)?,
};
if !should_execute {
return Ok(FirstStretch::Skipped);
}
if first_boundary == 0 {
return Ok(FirstStretch::Continue);
}
let halted = self.run_tasks_slice_in_arena(
&tasks[..first_boundary],
workflow,
message,
&mut arena_ctx,
trace.as_deref_mut(),
now,
)?;
Ok(if halted {
FirstStretch::Halted
} else {
FirstStretch::Continue
})
})
};
let run_result: Result<()> = match first {
Ok(FirstStretch::Skipped) => {
note_workflow_skip(trace.as_deref_mut(), &workflow.id, "condition not met");
return Ok(false);
}
Ok(FirstStretch::Halted) => Ok(()),
Ok(FirstStretch::Continue) => {
self.execute_tasks(workflow, message, trace, now, first_boundary)
.await
}
Err(e) => Err(e),
};
match run_result {
Ok(_) => {
info!("Successfully completed workflow: {}", workflow.id);
Ok(true)
}
Err(e) => {
if self.record_workflow_error(workflow, message, &e) {
Err(e)
} else {
Ok(true)
}
}
}
}
fn record_workflow_error(
&self,
workflow: &Workflow,
message: &mut Message,
e: &DataflowError,
) -> bool {
message.errors.push(
ErrorInfo::builder(
"WORKFLOW_ERROR",
format!("Workflow {} error: {}", workflow.id, e),
)
.workflow_id(&workflow.id)
.build(),
);
if workflow.continue_on_error {
warn!(
"Workflow {} encountered error but continuing: {:?}",
workflow.id, e
);
false
} else {
error!("Workflow {} failed: {:?}", workflow.id, e);
true
}
}
async fn execute_tasks(
&self,
workflow: &Workflow,
message: &mut Message,
mut trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
start: usize,
) -> Result<()> {
let tasks = &workflow.tasks;
let mut idx = start;
while idx < tasks.len() {
let stretch_end = next_async_boundary(tasks, idx);
if stretch_end > idx {
let halt = self.run_sync_stretch(
&tasks[idx..stretch_end],
workflow,
message,
trace.as_deref_mut(),
now,
)?;
if halt {
return Ok(());
}
idx = stretch_end;
}
if idx < tasks.len() {
let task = &tasks[idx];
let should_execute = evaluate_condition(
&self.engine,
task.compiled_condition.as_ref(),
&message.context,
)?;
if !should_execute {
debug!("Skipping task {} - condition not met", task.id);
if let Some(t) = trace.as_deref_mut() {
t.add_step(ExecutionStep::task_skipped(&workflow.id, &task.id));
}
idx += 1;
continue;
}
let trace_start = if trace.is_some() {
Some(Utc::now())
} else {
None
};
let obs_start = trace_start.or_else(|| self.observer_clock());
let result = self.task_executor.execute(task, message).await;
self.emit_task_event(workflow, task, &result, obs_start);
let control_flow = self.handle_task_result(
result,
&workflow.id_arc,
&task.id_arc,
task.continue_on_error,
message,
now,
)?;
if let Some(t) = trace.as_deref_mut() {
let started_at = trace_start.unwrap_or(now);
t.add_executed_step(
&workflow.id,
&task.id,
message,
started_at,
duration_us_between(started_at, Utc::now()),
None,
);
}
if matches!(control_flow, TaskControlFlow::HaltWorkflow) {
return Ok(());
}
idx += 1;
}
}
Ok(())
}
fn run_sync_stretch(
&self,
tasks: &[Task],
workflow: &Workflow,
message: &mut Message,
trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<bool> {
with_arena(|arena| -> Result<bool> {
let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
self.run_tasks_slice_in_arena(tasks, workflow, message, &mut arena_ctx, trace, now)
})
}
fn run_tasks_slice_in_arena<'arena>(
&self,
tasks: &'arena [Task],
workflow: &Workflow,
message: &mut Message,
arena_ctx: &mut ArenaContext<'arena>,
mut trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<bool> {
let arena = arena_ctx.arena();
for task in tasks {
let should_execute = match task.compiled_condition.as_ref() {
None => true,
Some(compiled) => evaluate_condition_in_arena(
&self.engine,
Some(compiled),
arena_ctx.as_data_value(),
arena,
)?,
};
if !should_execute {
debug!("Skipping task {} - condition not met", task.id);
if let Some(t) = trace.as_deref_mut() {
t.add_step(ExecutionStep::task_skipped(&workflow.id, &task.id));
}
continue;
}
let mut mapping_snapshots: Vec<Value> = Vec::new();
let want_mapping_contexts = trace
.as_deref()
.is_some_and(|t| t.options().mapping_contexts);
let mapping_snapshots_buf = if want_mapping_contexts {
Some(&mut mapping_snapshots)
} else {
None
};
let trace_start = if trace.is_some() {
Some(Utc::now())
} else {
None
};
let obs_start = trace_start.or_else(|| self.observer_clock());
let result =
self.execute_sync_task_in_arena(task, message, arena_ctx, mapping_snapshots_buf);
self.emit_task_event(workflow, task, &result, obs_start);
let control_flow = self.handle_task_result(
result,
&workflow.id_arc,
&task.id_arc,
task.continue_on_error,
message,
now,
)?;
arena_ctx.refresh_for_path(&message.context, "metadata.progress");
if let Some(t) = trace.as_deref_mut() {
let started_at = trace_start.unwrap_or(now);
let mapping_contexts = if mapping_snapshots.is_empty() {
None
} else {
Some(mapping_snapshots)
};
t.add_executed_step(
&workflow.id,
&task.id,
message,
started_at,
duration_us_between(started_at, Utc::now()),
mapping_contexts,
);
}
if matches!(control_flow, TaskControlFlow::HaltWorkflow) {
return Ok(true);
}
}
Ok(false)
}
pub async fn run_all(
&self,
workflows: &[&Workflow],
message: &mut Message,
trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<()> {
self.run_all_borrowed(workflows, message, trace, now).await
}
pub(crate) async fn run_all_borrowed<W: std::borrow::Borrow<Workflow>>(
&self,
workflows: &[W],
message: &mut Message,
mut trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<()> {
let mut i = 0;
while i < workflows.len() {
if workflows[i].borrow().fully_sync {
let mut j = i + 1;
while j < workflows.len() && workflows[j].borrow().fully_sync {
j += 1;
}
self.execute_sync_workflow_run(
&workflows[i..j],
message,
trace.as_deref_mut(),
now,
)?;
i = j;
} else {
self.execute_inner(workflows[i].borrow(), message, trace.as_deref_mut(), now)
.await?;
i += 1;
}
}
Ok(())
}
fn execute_sync_workflow_run<W: std::borrow::Borrow<Workflow>>(
&self,
workflows: &[W],
message: &mut Message,
mut trace: Option<&mut ExecutionTrace>,
now: DateTime<Utc>,
) -> Result<()> {
with_arena(|arena| -> Result<()> {
let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
for workflow in workflows {
let workflow: &Workflow = workflow.borrow();
if !rollout_admits(workflow, message) {
note_workflow_skip(
trace.as_deref_mut(),
&workflow.id,
"outside rollout bucket",
);
continue;
}
let should_execute = match workflow.compiled_condition.as_ref() {
None => true,
Some(compiled) => evaluate_condition_in_arena(
&self.engine,
Some(compiled),
arena_ctx.as_data_value(),
arena,
)?,
};
if !should_execute {
note_workflow_skip(trace.as_deref_mut(), &workflow.id, "condition not met");
continue;
}
match self.run_tasks_slice_in_arena(
&workflow.tasks,
workflow,
message,
&mut arena_ctx,
trace.as_deref_mut(),
now,
) {
Ok(_halted) => {
info!("Successfully completed workflow: {}", workflow.id);
}
Err(e) => {
if self.record_workflow_error(workflow, message, &e) {
return Err(e);
}
}
}
}
Ok(())
})
}
fn execute_sync_task_in_arena<'arena>(
&self,
task: &'arena Task,
message: &mut Message,
arena_ctx: &mut ArenaContext<'arena>,
mapping_snapshots: Option<&mut Vec<Value>>,
) -> Result<(TaskOutcome, Vec<Change>)> {
debug!(
"Executing sync task in arena: {} ({})",
task.id,
task.function.function_name()
);
debug_assert!(
task.function.is_sync_builtin(),
"execute_sync_task_in_arena called with non-sync-builtin task: {}",
task.function.function_name()
);
task.function
.try_execute_in_arena(message, arena_ctx, &self.engine, mapping_snapshots)
.ok_or_else(|| {
DataflowError::Task(format!(
"execute_sync_task_in_arena dispatched to non-sync-builtin task '{}' \
(engine bug — sync-stretch should only contain sync-builtin tasks)",
task.function.function_name()
))
})?
}
fn handle_task_result(
&self,
result: Result<(TaskOutcome, Vec<Change>)>,
workflow_id_arc: &Arc<str>,
task_id_arc: &Arc<str>,
continue_on_error: bool,
message: &mut Message,
now: DateTime<Utc>,
) -> Result<TaskControlFlow> {
let workflow_id: &str = workflow_id_arc;
let task_id: &str = task_id_arc;
match result {
Ok((TaskOutcome::Skip, _)) => {
debug!("Task {} signaled skip", task_id);
Ok(TaskControlFlow::Continue)
}
Ok((outcome, changes)) => {
let status = outcome
.audit_status()
.expect("Skip handled above; remaining variants emit audit status");
let halt = outcome.halts_workflow();
message.audit_trail.push(AuditTrail {
timestamp: now,
workflow_id: Arc::clone(workflow_id_arc),
task_id: Arc::clone(task_id_arc),
status: status as usize,
changes,
});
write_progress_metadata(&mut message.context, workflow_id, task_id, status);
if halt {
info!("Task {} halted workflow {}", task_id, workflow_id);
return Ok(TaskControlFlow::HaltWorkflow);
}
if (400..500).contains(&status) {
warn!("Task {} returned client error status: {}", task_id, status);
} else if status >= 500 {
error!("Task {} returned server error status: {}", task_id, status);
message.errors.push(
ErrorInfo::builder(
"TASK_STATUS_ERROR",
format!("Task {} returned status {}", task_id, status),
)
.workflow_id(workflow_id)
.task_id(task_id)
.build(),
);
if !continue_on_error {
return Err(DataflowError::Task(format!(
"Task {} failed with status {}",
task_id, status
)));
}
}
Ok(TaskControlFlow::Continue)
}
Err(e) => {
error!("Task {} failed: {:?}", task_id, e);
message.audit_trail.push(AuditTrail {
timestamp: now,
workflow_id: Arc::clone(workflow_id_arc),
task_id: Arc::clone(task_id_arc),
status: 500,
changes: vec![],
});
write_progress_metadata(&mut message.context, workflow_id, task_id, 500);
let mut info = ErrorInfo::builder(
service_error_code(&e),
format!("Task {} error: {}", task_id, e),
)
.workflow_id(workflow_id)
.task_id(task_id);
if let Some(detail) = e.detail() {
info = info.detail(detail);
}
message.errors.push(info.build());
if !continue_on_error {
Err(e)
} else {
Ok(TaskControlFlow::Continue)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::compiler::LogicCompiler;
use serde_json::json;
use std::collections::HashMap;
#[tokio::test]
async fn test_workflow_executor_skip_condition() {
let workflow_json = r#"{
"id": "test_workflow",
"name": "Test Workflow",
"condition": false,
"tasks": [{
"id": "dummy_task",
"name": "Dummy Task",
"function": {
"name": "map",
"input": {"mappings": []}
}
}]
}"#;
let compiler = LogicCompiler::new();
let mut workflow = Workflow::from_json(workflow_json).unwrap();
let workflows = compiler.compile_workflows(vec![workflow.clone()]).unwrap();
if let Some(compiled_workflow) = workflows.iter().find(|w| w.id == "test_workflow") {
workflow = compiled_workflow.clone();
}
let engine = compiler.into_engine();
let task_executor = Arc::new(TaskExecutor::new(
Arc::new(HashMap::new()),
Arc::clone(&engine),
));
let workflow_executor = WorkflowExecutor::new(task_executor, engine);
let mut message = Message::from_value(&json!({}));
let executed = workflow_executor
.execute(&workflow, &mut message, Utc::now())
.await
.unwrap();
assert!(!executed);
assert_eq!(message.audit_trail.len(), 0);
}
#[tokio::test]
async fn test_workflow_executor_execute_success() {
let workflow_json = r#"{
"id": "test_workflow",
"name": "Test Workflow",
"condition": true,
"tasks": [{
"id": "dummy_task",
"name": "Dummy Task",
"function": {
"name": "map",
"input": {"mappings": []}
}
}]
}"#;
let compiler = LogicCompiler::new();
let mut workflow = Workflow::from_json(workflow_json).unwrap();
let workflows = compiler.compile_workflows(vec![workflow.clone()]).unwrap();
if let Some(compiled_workflow) = workflows.iter().find(|w| w.id == "test_workflow") {
workflow = compiled_workflow.clone();
}
let engine = compiler.into_engine();
let task_executor = Arc::new(TaskExecutor::new(
Arc::new(HashMap::new()),
Arc::clone(&engine),
));
let workflow_executor = WorkflowExecutor::new(task_executor, engine);
let mut message = Message::from_value(&json!({}));
let executed = workflow_executor
.execute(&workflow, &mut message, Utc::now())
.await
.unwrap();
assert!(executed);
}
}