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::{BuiltinKind, builtin_function_kind};
11use crate::engine::functions::{BoxedFunctionHandler, FunctionConfig};
12use crate::engine::message::{Change, Message};
13use crate::engine::task::Task;
14use crate::engine::task_context::TaskContext;
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        debug!(
60            "Executing task: {} with function: {:?}",
61            task.id,
62            task.function.function_name()
63        );
64
65        match &task.function {
66            // Sync built-ins — only hit here when called outside the workflow
67            // sync stretch (test harness, direct `TaskExecutor::execute`).
68            FunctionConfig::Map { input, .. } => input.execute(message, &self.engine),
69            FunctionConfig::Validation { input, .. } => input.execute(message, &self.engine),
70            FunctionConfig::ParseJson { input, .. } => {
71                crate::engine::functions::parse::execute_parse_json(message, input)
72            }
73            FunctionConfig::ParseXml { input, .. } => {
74                crate::engine::functions::parse::execute_parse_xml(message, input)
75            }
76            FunctionConfig::PublishJson { input, .. } => {
77                crate::engine::functions::publish::execute_publish_json(message, input)
78            }
79            FunctionConfig::PublishXml { input, .. } => {
80                crate::engine::functions::publish::execute_publish_xml(message, input)
81            }
82            FunctionConfig::Filter { input, .. } => input.execute(message, &self.engine),
83            FunctionConfig::Log { input, .. } => input.execute(message, &self.engine),
84            // Async / user-registered handlers. Named via `function_name()`
85            // rather than a repeated string literal, so the registry lookup
86            // can never drift from the canonical name `FunctionConfig` itself
87            // reports for the variant.
88            FunctionConfig::HttpCall { input, .. } => {
89                self.dispatch_handler(task.function.function_name(), message, input)
90                    .await
91            }
92            FunctionConfig::Enrich { input, .. } => {
93                self.dispatch_handler(task.function.function_name(), message, input)
94                    .await
95            }
96            FunctionConfig::PublishKafka { input, .. } => {
97                self.dispatch_handler(task.function.function_name(), message, input)
98                    .await
99            }
100            FunctionConfig::Custom {
101                name,
102                compiled_input,
103                ..
104            } => {
105                let any_input = compiled_input.as_ref().ok_or_else(|| {
106                    DataflowError::Validation(format!(
107                        "Custom function '{}' has no precompiled input — \
108                         was the workflow built outside Engine::new?",
109                        name
110                    ))
111                })?;
112                self.dispatch_handler_any(name, message, any_input.as_any())
113                    .await
114            }
115        }
116    }
117
118    /// Generic-Input flavour: takes any `T: Any + Send + Sync`, hands it to
119    /// the registered handler as `&dyn Any`. Used by the built-in async
120    /// dispatch (`HttpCallConfig`, `EnrichConfig`, `PublishKafkaConfig`)
121    /// where the typed config is already on the `FunctionConfig` enum.
122    async fn dispatch_handler<T>(
123        &self,
124        name: &str,
125        message: &mut Message,
126        input: &T,
127    ) -> Result<(TaskOutcome, Vec<Change>)>
128    where
129        T: Any + Send + Sync,
130    {
131        let any_input: &(dyn Any + Send + Sync) = input;
132        self.dispatch_handler_any(name, message, any_input).await
133    }
134
135    /// Inner dispatch: build a `TaskContext`, invoke the handler, drain the
136    /// accumulated `Change` buffer.
137    async fn dispatch_handler_any(
138        &self,
139        name: &str,
140        message: &mut Message,
141        any_input: &(dyn Any + Send + Sync),
142    ) -> Result<(TaskOutcome, Vec<Change>)> {
143        let handler = self.task_functions.get(name).ok_or_else(|| {
144            error!("Function handler not found: {}", name);
145            DataflowError::FunctionNotFound(name.to_string())
146        })?;
147        let mut ctx = TaskContext::new(message, &self.engine);
148        let outcome = handler.dyn_execute(&mut ctx, any_input).await?;
149        let changes = ctx.into_changes();
150        Ok((outcome, changes))
151    }
152
153    /// Whether this executor can actually run a task named `name`.
154    ///
155    /// `true` for a [`BuiltinKind::SelfContained`] built-in, which this crate
156    /// executes itself, and for any name with a registered handler.
157    ///
158    /// `false` for `http_call` / `enrich` / `publish_kafka` with no handler
159    /// registered — those deserialize into a typed built-in variant and so pass
160    /// `Engine::new`, but fail at dispatch with
161    /// [`DataflowError::FunctionNotFound`] on the first message. This is
162    /// deliberately narrower than "is this a known name"; use
163    /// [`crate::is_builtin_function`] for that.
164    pub fn has_function(&self, name: &str) -> bool {
165        match builtin_function_kind(name) {
166            Some(BuiltinKind::SelfContained) => true,
167            // RequiresHandler and Custom alike: only if a handler was registered.
168            _ => self.task_functions.contains_key(name),
169        }
170    }
171
172    /// Get a clone of the task_functions Arc for reuse in new engines
173    pub fn task_functions(&self) -> Arc<HashMap<String, BoxedFunctionHandler>> {
174        Arc::clone(&self.task_functions)
175    }
176
177    /// Get the count of registered custom functions
178    pub fn custom_function_count(&self) -> usize {
179        self.task_functions.len()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::engine::AsyncFunctionHandler;
187    use crate::engine::compiler::LogicCompiler;
188    use crate::engine::functions::config::is_builtin_function;
189
190    /// A `TaskExecutor` with an empty handler registry.
191    fn executor_with_no_handlers() -> TaskExecutor {
192        TaskExecutor::new(Arc::new(HashMap::new()), LogicCompiler::new().into_engine())
193    }
194
195    /// A `TaskExecutor` with `MockAsyncFunction` registered under `name`.
196    fn executor_with_handler(name: &str) -> TaskExecutor {
197        let mut handlers: HashMap<String, BoxedFunctionHandler> = HashMap::new();
198        handlers.insert(name.to_string(), Box::new(MockAsyncFunction));
199        TaskExecutor::new(Arc::new(handlers), LogicCompiler::new().into_engine())
200    }
201
202    #[test]
203    fn test_has_function() {
204        let task_executor = executor_with_no_handlers();
205
206        // Built-in functions
207        assert!(task_executor.has_function("map"));
208        assert!(task_executor.has_function("validation"));
209        assert!(task_executor.has_function("validate"));
210
211        // Non-existent function
212        assert!(!task_executor.has_function("nonexistent"));
213        assert!(!task_executor.has_function(""));
214    }
215
216    #[test]
217    fn has_function_is_false_for_config_only_integrations_without_a_handler() {
218        // These three deserialize into typed built-in variants, so `Engine::new`
219        // accepts them — but they dispatch to a registered handler and fail with
220        // FunctionNotFound on the first message. `has_function` answers "can this
221        // executor run it", so it must say no.
222        let task_executor = executor_with_no_handlers();
223
224        for name in ["http_call", "enrich", "publish_kafka"] {
225            assert!(
226                !task_executor.has_function(name),
227                "'{name}' has no registered handler, so it cannot be run"
228            );
229            // It is still a known built-in name — the two questions differ.
230            assert!(is_builtin_function(name));
231        }
232    }
233
234    #[test]
235    fn has_function_is_true_for_config_only_integrations_once_registered() {
236        for name in ["http_call", "enrich", "publish_kafka"] {
237            assert!(
238                executor_with_handler(name).has_function(name),
239                "'{name}' has a registered handler, so it can be run"
240            );
241        }
242    }
243
244    #[test]
245    fn has_function_is_true_for_every_self_contained_builtin_with_an_empty_registry() {
246        let task_executor = executor_with_no_handlers();
247
248        // Both accepted spellings of validation are covered: the deserializer
249        // takes either, while `function_name()` only ever returns "validate".
250        for name in [
251            "map",
252            "validation",
253            "validate",
254            "parse_json",
255            "parse_xml",
256            "publish_json",
257            "publish_xml",
258            "filter",
259            "log",
260        ] {
261            assert!(
262                task_executor.has_function(name),
263                "'{name}' is executed by the crate and needs no registration"
264            );
265        }
266    }
267
268    #[test]
269    fn registering_a_handler_under_a_self_contained_name_does_not_change_the_answer() {
270        // `TaskExecutor::execute` dispatches sync built-ins by variant and never
271        // consults the registry, so a handler registered under "map" is dead
272        // code. `has_function` answers `true` either way; pinning it here stops a
273        // later change from making the two disagree silently.
274        assert!(executor_with_handler("map").has_function("map"));
275        assert!(executor_with_no_handlers().has_function("map"));
276    }
277
278    #[test]
279    fn has_function_is_unchanged_for_custom_names() {
280        assert!(executor_with_handler("custom_test").has_function("custom_test"));
281        assert!(!executor_with_handler("custom_test").has_function("other_custom"));
282    }
283
284    #[test]
285    fn test_custom_function_count() {
286        let mut custom_functions: HashMap<String, BoxedFunctionHandler> = HashMap::new();
287        custom_functions.insert("custom_test".to_string(), Box::new(MockAsyncFunction));
288
289        let engine = LogicCompiler::new().into_engine();
290        let task_executor = TaskExecutor::new(Arc::new(custom_functions), engine);
291
292        assert_eq!(task_executor.custom_function_count(), 1);
293    }
294
295    // Mock async function for testing
296    struct MockAsyncFunction;
297
298    #[async_trait::async_trait]
299    impl AsyncFunctionHandler for MockAsyncFunction {
300        type Input = serde_json::Value;
301
302        async fn execute(
303            &self,
304            _ctx: &mut TaskContext<'_>,
305            _input: &serde_json::Value,
306        ) -> Result<TaskOutcome> {
307            Ok(TaskOutcome::Success)
308        }
309    }
310}