use std::sync::Arc;
use std::time::{Duration, Instant};
use serde_json::Value;
use super::runner::{TraceCapture, run_for_channel};
#[derive(Debug)]
pub enum RunOutcome {
Ok,
WorkflowErrors(String),
Timeout(u64),
EngineError(dataflow_rs::DataflowError),
}
impl RunOutcome {
pub fn status_label(&self) -> &'static str {
match self {
Self::Ok => "ok",
Self::WorkflowErrors(_) | Self::EngineError(_) => "error",
Self::Timeout(_) => "timeout",
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok)
}
}
#[derive(Default)]
pub struct ExecOpts<'a> {
pub timeout_ms: Option<u64>,
pub capture: Option<TraceCapture>,
pub routing_bucket: Option<u8>,
pub profile: Option<&'a Arc<super::profile::ProfileCollector>>,
}
pub struct Execution {
pub message: dataflow_rs::Message,
pub task_trace: Option<dataflow_rs::ExecutionTrace>,
pub outcome: RunOutcome,
pub duration: Duration,
}
pub async fn execute_admitted(
engine: &Arc<dataflow_rs::Engine>,
channel: &str,
data: &Value,
metadata: &Value,
opts: ExecOpts<'_>,
) -> Execution {
let mut builder = dataflow_rs::Message::builder()
.payload_json(data)
.metadata_json(metadata)
.capture_changes(opts.capture.is_some());
if let Some(bucket) = opts.routing_bucket {
builder = builder.routing_bucket(bucket);
}
let mut message = builder.build();
let started = Instant::now();
let call = run_for_channel(
engine,
channel,
&mut message,
opts.timeout_ms,
opts.profile,
opts.capture,
)
.await;
let duration = started.elapsed();
let (outcome, task_trace) = match call {
Err(ms) => (RunOutcome::Timeout(ms), None),
Ok((Err(e), trace)) => (RunOutcome::EngineError(e), trace),
Ok((Ok(()), trace)) => {
if message.has_errors() {
(RunOutcome::WorkflowErrors(error_summary(&message)), trace)
} else {
(RunOutcome::Ok, trace)
}
}
};
Execution {
message,
task_trace,
outcome,
duration,
}
}
fn error_summary(message: &dataflow_rs::Message) -> String {
message
.errors()
.iter()
.map(|e| format!("{}: {}", e.code, e.message))
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_status_label_is_one_derivation() {
assert_eq!(RunOutcome::Ok.status_label(), "ok");
assert_eq!(RunOutcome::Timeout(50).status_label(), "timeout");
assert_eq!(
RunOutcome::WorkflowErrors("boom".into()).status_label(),
"error"
);
assert_eq!(
RunOutcome::EngineError(dataflow_rs::DataflowError::Unknown("x".into())).status_label(),
"error"
);
}
#[tokio::test]
async fn only_a_traced_run_captures_per_write_changes() {
let workflow = dataflow_rs::Workflow::from_json(
r#"{"id":"w","name":"w","channel":"c","priority":0,"condition":true,
"loop":{"counter":"i","max":3},
"tasks":[{"id":"m","name":"m","function":{"name":"map","input":
{"mappings":[{"path":"temp_data.seen","logic":{"var":"temp_data.i"}}]}}}]}"#,
)
.expect("workflow parses");
let engine = Arc::new(
dataflow_rs::Engine::new(vec![workflow], std::collections::HashMap::new())
.expect("engine builds"),
);
let data = serde_json::json!({"xs": [1, 2, 3]});
let metadata = serde_json::json!({});
let untraced = execute_admitted(&engine, "c", &data, &metadata, ExecOpts::default()).await;
assert!(untraced.outcome.is_ok());
let trail = untraced.message.audit_trail();
assert_eq!(trail.len(), 3, "one entry per sweep is still recorded");
assert!(
trail.iter().all(|entry| entry.changes.is_empty()),
"an untraced run holds no value copies"
);
let traced = execute_admitted(
&engine,
"c",
&data,
&metadata,
ExecOpts {
capture: Some(TraceCapture {
max_snapshot_bytes: 0,
}),
..Default::default()
},
)
.await;
assert!(traced.outcome.is_ok());
assert!(
traced
.message
.audit_trail()
.iter()
.all(|entry| entry.changes.len() == 1),
"a traced run captures each write"
);
let trace = traced.task_trace.expect("a traced run returns its trace");
assert!(
trace
.steps
.iter()
.all(|step| step.changes.as_ref().is_some_and(|c| c.len() == 1)),
"the trace's per-step diff is still populated"
);
}
}