pub mod authoring;
pub mod compiler;
pub mod error;
pub mod executor;
pub mod functions;
pub mod message;
pub mod observer;
#[cfg(not(target_arch = "wasm32"))]
pub mod retry;
pub mod rollout;
pub mod secrets;
pub mod steps;
pub mod task;
pub mod task_context;
pub mod task_executor;
pub mod task_outcome;
pub mod trace;
pub mod utils;
pub mod workflow;
pub mod workflow_executor;
pub use authoring::{IssueCode, Severity, WorkflowIssue};
use error::{DEFAULT_ERROR_CONTEXT_LIMIT, ErrorContextConfig};
pub use error::{DataflowError, ErrorInfo, Result, ServiceErrorBuilder};
pub use functions::{
AsyncFunctionHandler, BoxedFunctionHandler, CompiledCustomInput, DynAsyncFunctionHandler,
FunctionConfig, Template, TemplateCompiler,
};
pub use message::Message;
pub use observer::{
ExecutionObserver, MessageFinished, MessageStarted, TaskEvent, WorkflowFinished,
WorkflowStarted,
};
#[cfg(not(target_arch = "wasm32"))]
pub use retry::{RetryPolicy, retry_with_attempts, retry_with_policy};
pub use rollout::{Rollout, RolloutError};
pub use secrets::Secrets;
pub use steps::{
AuthoredStep, AuthoredSteps, MAX_GROUP_DEPTH, StepKind, is_group, walk_authored_steps,
};
pub use task::{HaltOn, Task, TaskGroup};
pub use task_context::TaskContext;
pub use task_outcome::{HALT_STATUS_CODE, TaskOutcome};
pub use trace::{AuditTrailScope, ExecutionStep, ExecutionTrace, StepResult, TraceOptions};
pub use workflow::{ConnectorRef, Workflow, WorkflowStatus};
use chrono::Utc;
use datalogic_rs::Engine as DatalogicEngine;
use datavalue::OwnedDataValue;
use std::collections::HashMap;
use std::sync::Arc;
use crate::engine::functions::config::{
DispatchableFunction, can_dispatch_in, dispatchable_functions_in,
};
use compiler::LogicCompiler;
use task_executor::TaskExecutor;
use workflow_executor::WorkflowExecutor;
pub struct Engine {
workflows: Arc<Vec<Workflow>>,
channel_index: Arc<HashMap<String, Vec<usize>>>,
workflow_executor: Arc<WorkflowExecutor>,
datalogic: Arc<DatalogicEngine>,
datalogic_operators: DatalogicOperators,
engine_version: Arc<OwnedDataValue>,
secrets: Arc<Secrets>,
}
pub type DatalogicOperators = Arc<HashMap<String, Arc<dyn datalogic_rs::CustomOperator>>>;
fn build_channel_index(workflows: &[Workflow]) -> HashMap<String, Vec<usize>> {
let mut index: HashMap<String, Vec<usize>> = HashMap::new();
for (i, workflow) in workflows.iter().enumerate() {
if workflow.status == WorkflowStatus::Active {
index.entry(workflow.channel.clone()).or_default().push(i);
}
}
index
}
impl Engine {
pub fn new(
workflows: Vec<Workflow>,
task_functions: HashMap<String, BoxedFunctionHandler>,
) -> Result<Self> {
Self::new_with_operators(workflows, task_functions, Arc::new(HashMap::new()))
}
pub fn new_with_operators(
workflows: Vec<Workflow>,
task_functions: HashMap<String, BoxedFunctionHandler>,
datalogic_operators: DatalogicOperators,
) -> Result<Self> {
Self::new_inner(
workflows,
task_functions,
datalogic_operators,
Arc::new(Secrets::empty()),
)
}
fn new_inner(
workflows: Vec<Workflow>,
task_functions: HashMap<String, BoxedFunctionHandler>,
datalogic_operators: DatalogicOperators,
secrets: Arc<Secrets>,
) -> Result<Self> {
if datalogic_operators.contains_key(secrets::SECRET_OPERATOR) {
return Err(DataflowError::Validation(format!(
"'{}' is a reserved operator name — it reads the engine's secret store \
(see EngineBuilder::with_secrets) and cannot be registered by a host",
secrets::SECRET_OPERATOR
)));
}
let compiler = LogicCompiler::with_operators_and_secrets(&datalogic_operators, &secrets);
refuse_authoring_issues(&workflows, &secrets)?;
let mut sorted_workflows = compiler.compile_workflows(workflows)?;
let datalogic = compiler.into_engine();
precompile_custom_inputs(&mut sorted_workflows, &task_functions, &datalogic)?;
let task_executor = Arc::new(TaskExecutor::with_secrets(
Arc::new(task_functions),
Arc::clone(&datalogic),
Arc::clone(&secrets),
));
let workflow_executor =
Arc::new(WorkflowExecutor::new(task_executor, Arc::clone(&datalogic)));
let channel_index = build_channel_index(&sorted_workflows);
Ok(Self {
workflows: Arc::new(sorted_workflows),
channel_index: Arc::new(channel_index),
workflow_executor,
datalogic,
datalogic_operators,
engine_version: Arc::new(OwnedDataValue::String(
env!("CARGO_PKG_VERSION").to_string(),
)),
secrets,
})
}
pub fn builder() -> EngineBuilder {
EngineBuilder::new()
}
pub fn engine_version_value(&self) -> &OwnedDataValue {
&self.engine_version
}
pub fn declared_secrets(&self) -> impl Iterator<Item = &str> {
self.secrets.names()
}
pub fn with_new_workflows(&self, workflows: Vec<Workflow>) -> Result<Self> {
let task_functions = self.workflow_executor.task_functions();
let compiler =
LogicCompiler::with_operators_and_secrets(&self.datalogic_operators, &self.secrets);
refuse_authoring_issues(&workflows, &self.secrets)?;
let mut sorted_workflows = compiler.compile_workflows(workflows)?;
let datalogic = compiler.into_engine();
precompile_custom_inputs(&mut sorted_workflows, &task_functions, &datalogic)?;
let task_executor = Arc::new(TaskExecutor::with_secrets(
task_functions,
Arc::clone(&datalogic),
Arc::clone(&self.secrets),
));
let mut executor = WorkflowExecutor::new(task_executor, Arc::clone(&datalogic));
if let Some(observer) = self.workflow_executor.observer() {
executor = executor.with_observer(Arc::clone(observer));
}
if let Some(cfg) = self.workflow_executor.error_context() {
executor = executor.with_error_context(Arc::clone(cfg));
}
let workflow_executor = Arc::new(executor);
let channel_index = build_channel_index(&sorted_workflows);
Ok(Self {
workflows: Arc::new(sorted_workflows),
channel_index: Arc::new(channel_index),
workflow_executor,
datalogic,
datalogic_operators: Arc::clone(&self.datalogic_operators),
engine_version: Arc::clone(&self.engine_version),
secrets: Arc::clone(&self.secrets),
})
}
pub fn with_observer(self, observer: Arc<dyn ExecutionObserver>) -> Self {
self.rebuild_executor(|executor| executor.with_observer(observer))
}
pub(crate) fn with_error_context(self, cfg: Arc<ErrorContextConfig>) -> Self {
self.rebuild_executor(|executor| executor.with_error_context(cfg))
}
fn rebuild_executor(
self,
configure: impl FnOnce(WorkflowExecutor) -> WorkflowExecutor,
) -> Self {
let task_executor = Arc::new(TaskExecutor::with_secrets(
self.workflow_executor.task_functions(),
Arc::clone(&self.datalogic),
Arc::clone(&self.secrets),
));
let mut executor = WorkflowExecutor::new(task_executor, Arc::clone(&self.datalogic));
if let Some(observer) = self.workflow_executor.observer() {
executor = executor.with_observer(Arc::clone(observer));
}
if let Some(cfg) = self.workflow_executor.error_context() {
executor = executor.with_error_context(Arc::clone(cfg));
}
Self {
workflows: self.workflows,
channel_index: self.channel_index,
workflow_executor: Arc::new(configure(executor)),
datalogic: self.datalogic,
datalogic_operators: self.datalogic_operators,
engine_version: self.engine_version,
secrets: self.secrets,
}
}
pub async fn process_message(&self, message: &mut Message) -> Result<()> {
self.process_all(message, None, Utc::now()).await
}
pub async fn process_message_tracing(
&self,
message: &mut Message,
trace: &mut ExecutionTrace,
) -> Result<()> {
self.process_all(message, Some(trace), Utc::now()).await
}
async fn process_all(
&self,
message: &mut Message,
trace: Option<&mut ExecutionTrace>,
now: chrono::DateTime<Utc>,
) -> Result<()> {
set_processing_metadata(&mut message.context, &self.engine_version, now, None);
self.workflow_executor
.run_all_borrowed(&self.workflows[..], message, trace, now)
.await
}
pub async fn process_message_with_trace(
&self,
message: &mut Message,
) -> Result<ExecutionTrace> {
self.process_message_with_trace_options(message, TraceOptions::default())
.await
}
pub async fn process_message_with_trace_options(
&self,
message: &mut Message,
options: TraceOptions,
) -> Result<ExecutionTrace> {
let mut trace = ExecutionTrace::with_options(options);
self.process_message_tracing(message, &mut trace).await?;
Ok(trace)
}
pub async fn process_message_for_channel(
&self,
channel: &str,
message: &mut Message,
) -> Result<()> {
self.process_channel(channel, message, None, Utc::now())
.await
}
pub async fn process_message_for_channel_tracing(
&self,
channel: &str,
message: &mut Message,
trace: &mut ExecutionTrace,
) -> Result<()> {
self.process_channel(channel, message, Some(trace), Utc::now())
.await
}
async fn process_channel(
&self,
channel: &str,
message: &mut Message,
trace: Option<&mut ExecutionTrace>,
now: chrono::DateTime<Utc>,
) -> Result<()> {
set_processing_metadata(
&mut message.context,
&self.engine_version,
now,
Some(channel),
);
if let Some(indices) = self.channel_index.get(channel) {
let workflows: Vec<&Workflow> =
indices.iter().map(|&idx| &self.workflows[idx]).collect();
self.workflow_executor
.run_all_borrowed(&workflows, message, trace, now)
.await?;
}
Ok(())
}
pub async fn process_message_for_channel_with_trace(
&self,
channel: &str,
message: &mut Message,
) -> Result<ExecutionTrace> {
self.process_message_for_channel_with_trace_options(
channel,
message,
TraceOptions::default(),
)
.await
}
pub async fn process_message_for_channel_with_trace_options(
&self,
channel: &str,
message: &mut Message,
options: TraceOptions,
) -> Result<ExecutionTrace> {
let mut trace = ExecutionTrace::with_options(options);
self.process_message_for_channel_tracing(channel, message, &mut trace)
.await?;
Ok(trace)
}
pub fn workflows(&self) -> &Arc<Vec<Workflow>> {
&self.workflows
}
pub fn workflow_by_id(&self, id: &str) -> Option<&Workflow> {
self.workflows.iter().find(|w| w.id == id)
}
pub fn dispatchable_functions(&self) -> impl Iterator<Item = DispatchableFunction<'_>> {
dispatchable_functions_in(self.workflow_executor.registry())
}
pub fn can_dispatch(&self, name: &str) -> bool {
can_dispatch_in(self.workflow_executor.registry(), name)
}
pub fn check_workflow(&self, workflow: &Workflow) -> Vec<WorkflowIssue> {
let compiler = TemplateCompiler::new(Arc::clone(&self.datalogic));
authoring::check_against_registry(
workflow,
self.workflow_executor.registry(),
&compiler,
&self.secrets,
)
}
pub fn operator_names(&self) -> impl Iterator<Item = &str> + '_ {
let builtins = || {
self.datalogic
.builtin_operator_names()
.map(|name| -> &str { name })
};
let customs = self
.datalogic_operators
.keys()
.map(String::as_str)
.filter(move |name| !builtins().any(|b| b == *name));
builtins()
.chain(customs)
.chain(std::iter::once(secrets::SECRET_OPERATOR))
}
pub fn template_key_escape(&self) -> char {
compiler::TEMPLATE_KEY_ESCAPE
}
pub fn datalogic(&self) -> &Arc<DatalogicEngine> {
&self.datalogic
}
}
#[must_use = "EngineBuilder must be `.build()` to produce an Engine"]
#[derive(Default)]
pub struct EngineBuilder {
workflows: Vec<Workflow>,
handlers: HashMap<String, BoxedFunctionHandler>,
observer: Option<Arc<dyn ExecutionObserver>>,
datalogic_operators: HashMap<String, Arc<dyn datalogic_rs::CustomOperator>>,
error_context_path: Option<String>,
error_context_limit: Option<usize>,
secrets: Option<Result<Secrets>>,
}
impl EngineBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn register<F>(mut self, name: impl Into<String>, handler: F) -> Self
where
F: AsyncFunctionHandler,
{
self.handlers.insert(name.into(), Box::new(handler));
self
}
pub fn register_boxed(
mut self,
name: impl Into<String>,
handler: BoxedFunctionHandler,
) -> Self {
self.handlers.insert(name.into(), handler);
self
}
pub fn dispatchable_functions(&self) -> impl Iterator<Item = DispatchableFunction<'_>> {
dispatchable_functions_in(&self.handlers)
}
pub fn can_dispatch(&self, name: &str) -> bool {
can_dispatch_in(&self.handlers, name)
}
pub fn check_workflow(&self, workflow: &Workflow) -> Vec<WorkflowIssue> {
let compiler = LogicCompiler::with_operators(&self.datalogic_operators);
let template_compiler = TemplateCompiler::new(compiler.into_engine());
let store = match &self.secrets {
Some(Ok(secrets)) => secrets,
None | Some(Err(_)) => &secrets::EMPTY,
};
let mut issues =
authoring::check_against_registry(workflow, &self.handlers, &template_compiler, store);
if let Some(Err(err)) = &self.secrets {
issues.retain(|issue| issue.code != IssueCode::UnknownSecret);
issues.insert(
0,
WorkflowIssue {
code: IssueCode::InvalidSecretStore,
message: format!("the configured secret store is unusable: {err}"),
path: None,
task_id: None,
},
);
}
issues
}
pub fn with_workflow(mut self, workflow: Workflow) -> Self {
self.workflows.push(workflow);
self
}
pub fn with_workflows<I>(mut self, workflows: I) -> Self
where
I: IntoIterator<Item = Workflow>,
{
self.workflows.extend(workflows);
self
}
pub fn with_handlers(mut self, handlers: HashMap<String, BoxedFunctionHandler>) -> Self {
self.handlers.extend(handlers);
self
}
pub fn with_observer(mut self, observer: Arc<dyn ExecutionObserver>) -> Self {
self.observer = Some(observer);
self
}
pub fn with_error_context_path(mut self, path: impl Into<String>) -> Self {
self.error_context_path = Some(path.into());
self
}
pub fn with_error_context_limit(mut self, limit: usize) -> Self {
self.error_context_limit = Some(limit);
self
}
pub fn with_secrets(mut self, secrets: OwnedDataValue) -> Self {
self.secrets = Some(Secrets::new(secrets));
self
}
pub fn with_secrets_json(self, secrets: &serde_json::Value) -> Self {
self.with_secrets(OwnedDataValue::from(secrets))
}
pub fn with_datalogic_operator<T>(mut self, name: impl Into<String>, operator: T) -> Self
where
T: datalogic_rs::CustomOperator + 'static,
{
self.datalogic_operators
.insert(name.into(), Arc::new(operator));
self
}
pub fn build(self) -> Result<Engine> {
let error_context = match self.error_context_path {
Some(path) => Some(Arc::new(ErrorContextConfig::new(
path,
self.error_context_limit
.unwrap_or(DEFAULT_ERROR_CONTEXT_LIMIT),
)?)),
None => None,
};
let secrets = Arc::new(match self.secrets {
Some(secrets) => secrets?,
None => Secrets::empty(),
});
let engine = Engine::new_inner(
self.workflows,
self.handlers,
Arc::new(self.datalogic_operators),
secrets,
)?;
let engine = match error_context {
Some(cfg) => engine.with_error_context(cfg),
None => engine,
};
Ok(match self.observer {
Some(observer) => engine.with_observer(observer),
None => engine,
})
}
}
fn refuse_authoring_issues(workflows: &[Workflow], secrets: &Secrets) -> Result<()> {
for workflow in workflows {
let mut issues = authoring::check_secrets(workflow, secrets);
issues.extend(authoring::refusing_template_key_issues(workflow));
if !issues.is_empty() {
let listed: Vec<String> = issues.iter().map(ToString::to_string).collect();
return Err(DataflowError::Validation(format!(
"workflow '{}': {}",
workflow.id,
listed.join("; ")
)));
}
}
Ok(())
}
fn precompile_custom_inputs(
workflows: &mut [Workflow],
handlers: &HashMap<String, BoxedFunctionHandler>,
datalogic: &Arc<DatalogicEngine>,
) -> Result<()> {
let template_compiler = TemplateCompiler::new(Arc::clone(datalogic));
for workflow in workflows {
for task in &mut workflow.tasks {
if let FunctionConfig::Custom {
name,
input,
compiled_input,
} = &mut task.function
{
let handler = handlers
.get(name)
.ok_or_else(|| function_not_found_error(name, handlers))?;
let mut parsed = handler.parse_input_box(input)?;
handler.compile_input_box(&mut *parsed, &template_compiler)?;
*compiled_input = Some(CompiledCustomInput(Arc::from(parsed)));
}
}
}
Ok(())
}
fn function_not_found_error(
name: &str,
handlers: &HashMap<String, BoxedFunctionHandler>,
) -> DataflowError {
use crate::engine::functions::config::BUILTIN_FUNCTION_NAMES;
let mut registered: Vec<&str> = handlers.keys().map(String::as_str).collect();
registered.sort_unstable();
let registered_part = if registered.is_empty() {
String::from("none")
} else {
registered.join(", ")
};
DataflowError::FunctionNotFound(format!(
"{name} (registered handlers: {registered_part}; built-ins: {})",
BUILTIN_FUNCTION_NAMES.join(", ")
))
}
fn set_processing_metadata(
context: &mut OwnedDataValue,
engine_version: &Arc<OwnedDataValue>,
now: chrono::DateTime<Utc>,
channel: Option<&str>,
) {
let OwnedDataValue::Object(top) = context else {
return;
};
let metadata = match top.iter().position(|(k, _)| k == "metadata") {
Some(i) => &mut top[i].1,
None => {
top.push(("metadata".to_string(), OwnedDataValue::Object(Vec::new())));
&mut top.last_mut().expect("just pushed").1
}
};
let OwnedDataValue::Object(meta) = metadata else {
return;
};
let mut set_key = |key: &str, value: OwnedDataValue| {
if let Some(slot) = meta.iter_mut().find(|(k, _)| k == key) {
slot.1 = value;
} else {
meta.push((key.to_string(), value));
}
};
set_key("processed_at", OwnedDataValue::String(now.to_rfc3339()));
set_key("engine_version", (**engine_version).clone());
if let Some(channel) = channel {
set_key("channel", OwnedDataValue::String(channel.to_string()));
}
}