Skip to main content

dataflow_rs/engine/
task_executor.rs

1//! # Task Execution Module
2//!
3//! Dispatches a single `Task` to its function implementation. Built-in sync
4//! variants of `FunctionConfig` are dispatched in `workflow_executor`'s sync
5//! stretch via [`FunctionConfig::try_execute_in_arena`]; this module owns
6//! the async path — `HttpCall`, `Enrich`, `PublishKafka`, and `Custom` —
7//! routed to the matching registered handler.
8
9use crate::engine::error::{DataflowError, Result};
10use crate::engine::functions::config::can_dispatch_in;
11use crate::engine::functions::{BoxedFunctionHandler, FunctionConfig};
12use crate::engine::message::{Change, Message};
13use crate::engine::task::Task;
14use crate::engine::task_context::{TaskContext, TaskIdentity};
15use crate::engine::task_outcome::TaskOutcome;
16use datalogic_rs::Engine;
17use log::{debug, error};
18use std::any::Any;
19use std::collections::HashMap;
20use std::sync::Arc;
21
22/// Handles the execution of tasks with their associated functions.
23///
24/// The `TaskExecutor` is responsible for:
25/// - Routing async functions (http_call, enrich, publish_kafka, custom) to
26///   the matching registered handler via [`crate::engine::functions::DynAsyncFunctionHandler`]
27/// - Owning the function registry
28///
29/// Sync built-ins are *not* routed through `execute` — `workflow_executor`
30/// calls [`FunctionConfig::try_execute_in_arena`] inside its sync stretch
31/// for those, sharing one arena across consecutive sync tasks.
32pub struct TaskExecutor {
33    /// Registry of async function handlers
34    task_functions: Arc<HashMap<String, BoxedFunctionHandler>>,
35    /// Shared datalogic Engine (Send + Sync; Arc-shared across tasks)
36    engine: Arc<Engine>,
37}
38
39impl TaskExecutor {
40    /// Create a new TaskExecutor
41    pub fn new(
42        task_functions: Arc<HashMap<String, BoxedFunctionHandler>>,
43        engine: Arc<Engine>,
44    ) -> Self {
45        Self {
46            task_functions,
47            engine,
48        }
49    }
50
51    /// Execute a single task. Sync built-ins reach here only when called from
52    /// outside the workflow executor's sync-stretch path — they fall back to
53    /// their `execute()` methods (which open a fresh thread-local arena).
54    pub async fn execute(
55        &self,
56        task: &Task,
57        message: &mut Message,
58    ) -> Result<(TaskOutcome, Vec<Change>)> {
59        self.execute_in_workflow(task, message, None, None).await
60    }
61
62    /// As [`Self::execute`], carrying the identity the handler will see through
63    /// [`TaskContext::workflow_id`] / [`TaskContext::task_id`].
64    ///
65    /// Separate from `execute` rather than an added parameter because
66    /// `TaskExecutor` is publicly reachable (`engine::task_executor`), so
67    /// widening the existing signature would break a caller outside the crate
68    /// for a path only the workflow executor uses.
69    pub(crate) async fn execute_in_workflow(
70        &self,
71        task: &Task,
72        message: &mut Message,
73        identity: Option<TaskIdentity<'_>>,
74        loop_counter: Option<i64>,
75    ) -> Result<(TaskOutcome, Vec<Change>)> {
76        debug!(
77            "Executing task: {} with function: {:?}",
78            task.id,
79            task.function.function_name()
80        );
81
82        match &task.function {
83            // Sync built-ins — only hit here when called outside the workflow
84            // sync stretch (test harness, direct `TaskExecutor::execute`).
85            FunctionConfig::Map { input, .. } => input.execute(message, &self.engine),
86            FunctionConfig::Validation { input, .. } => input.execute(message, &self.engine),
87            FunctionConfig::ParseJson { input, .. } => {
88                crate::engine::functions::parse::execute_parse_json(message, input)
89            }
90            FunctionConfig::ParseXml { input, .. } => {
91                crate::engine::functions::parse::execute_parse_xml(message, input)
92            }
93            FunctionConfig::PublishJson { input, .. } => {
94                crate::engine::functions::publish::execute_publish_json(message, input)
95            }
96            FunctionConfig::PublishXml { input, .. } => {
97                crate::engine::functions::publish::execute_publish_xml(message, input)
98            }
99            FunctionConfig::Filter { input, .. } => input.execute(message, &self.engine),
100            FunctionConfig::Log { input, .. } => input.execute(message, &self.engine),
101            // Async / user-registered handlers. Named via `function_name()`
102            // rather than a repeated string literal, so the registry lookup
103            // can never drift from the canonical name `FunctionConfig` itself
104            // reports for the variant.
105            FunctionConfig::HttpCall { input, .. } => {
106                self.dispatch_handler(
107                    task.function.function_name(),
108                    message,
109                    input,
110                    identity,
111                    loop_counter,
112                )
113                .await
114            }
115            FunctionConfig::Enrich { input, .. } => {
116                self.dispatch_handler(
117                    task.function.function_name(),
118                    message,
119                    input,
120                    identity,
121                    loop_counter,
122                )
123                .await
124            }
125            FunctionConfig::PublishKafka { input, .. } => {
126                self.dispatch_handler(
127                    task.function.function_name(),
128                    message,
129                    input,
130                    identity,
131                    loop_counter,
132                )
133                .await
134            }
135            FunctionConfig::Custom {
136                name,
137                compiled_input,
138                ..
139            } => {
140                let any_input = compiled_input.as_ref().ok_or_else(|| {
141                    DataflowError::Validation(format!(
142                        "Custom function '{}' has no precompiled input — \
143                         was the workflow built outside Engine::new?",
144                        name
145                    ))
146                })?;
147                self.dispatch_handler_any(name, message, any_input.as_any(), identity, loop_counter)
148                    .await
149            }
150        }
151    }
152
153    /// Generic-Input flavour: takes any `T: Any + Send + Sync`, hands it to
154    /// the registered handler as `&dyn Any`. Used by the built-in async
155    /// dispatch (`HttpCallConfig`, `EnrichConfig`, `PublishKafkaConfig`)
156    /// where the typed config is already on the `FunctionConfig` enum.
157    async fn dispatch_handler<T>(
158        &self,
159        name: &str,
160        message: &mut Message,
161        input: &T,
162        identity: Option<TaskIdentity<'_>>,
163        loop_counter: Option<i64>,
164    ) -> Result<(TaskOutcome, Vec<Change>)>
165    where
166        T: Any + Send + Sync,
167    {
168        let any_input: &(dyn Any + Send + Sync) = input;
169        self.dispatch_handler_any(name, message, any_input, identity, loop_counter)
170            .await
171    }
172
173    /// Inner dispatch: build a `TaskContext`, invoke the handler, drain the
174    /// accumulated `Change` buffer.
175    async fn dispatch_handler_any(
176        &self,
177        name: &str,
178        message: &mut Message,
179        any_input: &(dyn Any + Send + Sync),
180        identity: Option<TaskIdentity<'_>>,
181        loop_counter: Option<i64>,
182    ) -> Result<(TaskOutcome, Vec<Change>)> {
183        let handler = self.task_functions.get(name).ok_or_else(|| {
184            error!("Function handler not found: {}", name);
185            DataflowError::FunctionNotFound(name.to_string())
186        })?;
187        let mut ctx = TaskContext::with_identity(message, &self.engine, identity, loop_counter);
188        let outcome = handler.dyn_execute(&mut ctx, any_input).await?;
189        let changes = ctx.into_changes();
190        Ok((outcome, changes))
191    }
192
193    /// Whether this executor can actually run a task named `name`.
194    ///
195    /// `true` for a [`BuiltinKind::SelfContained`] built-in, which this crate
196    /// executes itself, and for any name with a registered handler.
197    ///
198    /// `false` for `http_call` / `enrich` / `publish_kafka` with no handler
199    /// registered — those deserialize into a typed built-in variant and so pass
200    /// `Engine::new`, but fail at dispatch with
201    /// [`DataflowError::FunctionNotFound`] on the first message. This is
202    /// deliberately narrower than "is this a known name"; use
203    /// [`crate::is_builtin_function`] for that.
204    pub fn has_function(&self, name: &str) -> bool {
205        can_dispatch_in(&self.task_functions, name)
206    }
207
208    /// Borrow the handler registry.
209    ///
210    /// [`Self::task_functions`] hands back an owned `Arc` clone for rebuilding
211    /// an executor; this borrows in place, so callers can yield `&str` keyed to
212    /// the executor's own lifetime — which
213    /// [`crate::Engine::dispatchable_functions`] needs and an `Arc` temporary
214    /// cannot provide.
215    pub fn registry(&self) -> &HashMap<String, BoxedFunctionHandler> {
216        &self.task_functions
217    }
218
219    /// Get a clone of the task_functions Arc for reuse in new engines
220    pub fn task_functions(&self) -> Arc<HashMap<String, BoxedFunctionHandler>> {
221        Arc::clone(&self.task_functions)
222    }
223
224    /// Get the count of registered custom functions
225    pub fn custom_function_count(&self) -> usize {
226        self.task_functions.len()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::engine::AsyncFunctionHandler;
234    use crate::engine::compiler::LogicCompiler;
235    use crate::engine::functions::config::is_builtin_function;
236
237    /// A `TaskExecutor` with an empty handler registry.
238    fn executor_with_no_handlers() -> TaskExecutor {
239        TaskExecutor::new(Arc::new(HashMap::new()), LogicCompiler::new().into_engine())
240    }
241
242    /// A `TaskExecutor` with `MockAsyncFunction` registered under `name`.
243    fn executor_with_handler(name: &str) -> TaskExecutor {
244        let mut handlers: HashMap<String, BoxedFunctionHandler> = HashMap::new();
245        handlers.insert(name.to_string(), Box::new(MockAsyncFunction));
246        TaskExecutor::new(Arc::new(handlers), LogicCompiler::new().into_engine())
247    }
248
249    #[test]
250    fn test_has_function() {
251        let task_executor = executor_with_no_handlers();
252
253        // Built-in functions
254        assert!(task_executor.has_function("map"));
255        assert!(task_executor.has_function("validation"));
256        assert!(task_executor.has_function("validate"));
257
258        // Non-existent function
259        assert!(!task_executor.has_function("nonexistent"));
260        assert!(!task_executor.has_function(""));
261    }
262
263    #[test]
264    fn has_function_is_false_for_config_only_integrations_without_a_handler() {
265        // These three deserialize into typed built-in variants, so `Engine::new`
266        // accepts them — but they dispatch to a registered handler and fail with
267        // FunctionNotFound on the first message. `has_function` answers "can this
268        // executor run it", so it must say no.
269        let task_executor = executor_with_no_handlers();
270
271        for name in ["http_call", "enrich", "publish_kafka"] {
272            assert!(
273                !task_executor.has_function(name),
274                "'{name}' has no registered handler, so it cannot be run"
275            );
276            // It is still a known built-in name — the two questions differ.
277            assert!(is_builtin_function(name));
278        }
279    }
280
281    #[test]
282    fn has_function_is_true_for_config_only_integrations_once_registered() {
283        for name in ["http_call", "enrich", "publish_kafka"] {
284            assert!(
285                executor_with_handler(name).has_function(name),
286                "'{name}' has a registered handler, so it can be run"
287            );
288        }
289    }
290
291    #[test]
292    fn has_function_is_true_for_every_self_contained_builtin_with_an_empty_registry() {
293        let task_executor = executor_with_no_handlers();
294
295        // Both accepted spellings of validation are covered: the deserializer
296        // takes either, while `function_name()` only ever returns "validate".
297        for name in [
298            "map",
299            "validation",
300            "validate",
301            "parse_json",
302            "parse_xml",
303            "publish_json",
304            "publish_xml",
305            "filter",
306            "log",
307        ] {
308            assert!(
309                task_executor.has_function(name),
310                "'{name}' is executed by the crate and needs no registration"
311            );
312        }
313    }
314
315    #[test]
316    fn registering_a_handler_under_a_self_contained_name_does_not_change_the_answer() {
317        // `TaskExecutor::execute` dispatches sync built-ins by variant and never
318        // consults the registry, so a handler registered under "map" is dead
319        // code. `has_function` answers `true` either way; pinning it here stops a
320        // later change from making the two disagree silently.
321        assert!(executor_with_handler("map").has_function("map"));
322        assert!(executor_with_no_handlers().has_function("map"));
323    }
324
325    #[test]
326    fn has_function_is_unchanged_for_custom_names() {
327        assert!(executor_with_handler("custom_test").has_function("custom_test"));
328        assert!(!executor_with_handler("custom_test").has_function("other_custom"));
329    }
330
331    #[test]
332    fn test_custom_function_count() {
333        let mut custom_functions: HashMap<String, BoxedFunctionHandler> = HashMap::new();
334        custom_functions.insert("custom_test".to_string(), Box::new(MockAsyncFunction));
335
336        let engine = LogicCompiler::new().into_engine();
337        let task_executor = TaskExecutor::new(Arc::new(custom_functions), engine);
338
339        assert_eq!(task_executor.custom_function_count(), 1);
340    }
341
342    // Mock async function for testing
343    struct MockAsyncFunction;
344
345    #[async_trait::async_trait]
346    impl AsyncFunctionHandler for MockAsyncFunction {
347        type Input = serde_json::Value;
348
349        async fn execute(
350            &self,
351            _ctx: &mut TaskContext<'_>,
352            _input: &serde_json::Value,
353        ) -> Result<TaskOutcome> {
354            Ok(TaskOutcome::Success)
355        }
356    }
357}