pub mod compiler;
pub mod error;
pub mod executor;
pub mod functions;
pub mod message;
pub mod observer;
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;
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, TaskEvent};
pub use task::Task;
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, Rollout, Workflow, WorkflowStatus};
use chrono::Utc;
use datalogic_rs::Engine as DatalogicEngine;
use datavalue::OwnedDataValue;
use std::collections::HashMap;
use std::sync::Arc;
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>,
}
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> {
let compiler = LogicCompiler::with_operators(&datalogic_operators);
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::new(
Arc::new(task_functions),
Arc::clone(&datalogic),
));
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(),
)),
})
}
pub fn builder() -> EngineBuilder {
EngineBuilder::new()
}
pub fn engine_version_value(&self) -> &OwnedDataValue {
&self.engine_version
}
pub fn with_new_workflows(&self, workflows: Vec<Workflow>) -> Result<Self> {
let task_functions = self.workflow_executor.task_functions();
let compiler = LogicCompiler::with_operators(&self.datalogic_operators);
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::new(task_functions, Arc::clone(&datalogic)));
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),
})
}
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::new(
self.workflow_executor.task_functions(),
Arc::clone(&self.datalogic),
));
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,
}
}
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 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>,
}
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 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_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 engine = Engine::new_with_operators(
self.workflows,
self.handlers,
Arc::new(self.datalogic_operators),
)?;
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 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()));
}
}