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