1+ //! Executor for flow node execution.
2+ //!
3+ //! Execution model overview:
4+ //! - The executor walks a linear "next" chain starting from `starting_node_id`.
5+ //! - Each node can call into other nodes through lazy arguments.
6+ //! - A node marked as remote is executed via `RemoteRuntime`.
7+ //! - The executor is synchronous; remote calls are awaited via `block_on`.
8+ //!
9+ //! Remote execution:
10+ //! - A node is considered remote based on `is_remote(&node)`.
11+ //! - Remote args are fully resolved to concrete `Value`s before sending.
12+ //! - The request parameters are mapped by `runtime_parameter_id`.
13+ //! - Remote responses are mapped into `Signal::Success` or `Signal::Failure`.
14+ //!
15+ //! Tracing:
16+ //! - Each node execution produces a trace frame with arguments and outcome.
17+ //! - Child executions are linked with `EdgeKind` to reflect eager or runtime calls.
18+ //!
19+ //! Error behavior:
20+ //! - Missing nodes/functions yield `Signal::Failure`.
21+ //! - Remote failures are mapped to `RuntimeError`.
22+ //! - The executor commits all final outcomes into the `Context`.
23+
124use crate :: context:: argument:: { Argument , ParameterNode } ;
225use crate :: context:: context:: { Context , ContextResult } ;
326use crate :: context:: registry:: { FunctionStore , HandlerFunctionEntry } ;
427use crate :: context:: signal:: Signal ;
528use crate :: debug:: trace:: { ArgKind , ArgTrace , EdgeKind , Outcome , ReferenceKind } ;
629use crate :: debug:: tracer:: { ExecutionTracer , Tracer } ;
730use crate :: runtime:: error:: RuntimeError ;
31+ use crate :: runtime:: remote:: RemoteRuntime ;
832
33+ use futures_lite:: future:: block_on;
934use std:: collections:: HashMap ;
35+ use tucana:: aquila:: execution_result:: Result as ExecutionOutcome ;
36+ use tucana:: aquila:: { ActionRuntimeError , ExecutionRequest , ExecutionResult } ;
1037use tucana:: shared:: reference_value:: Target ;
1138use tucana:: shared:: value:: Kind ;
12- use tucana:: shared:: { NodeFunction , Value } ;
39+ use tucana:: shared:: { NodeFunction , Struct , Value } ;
1340
41+ /// Executes a flow graph by repeatedly evaluating nodes.
42+ ///
43+ /// The executor is intentionally stateless with respect to the runtime:
44+ /// it borrows the function registry and graph, and mutates only the `Context`.
1445pub struct Executor < ' a > {
46+ // Registered Runtime Functions
1547 functions : & ' a FunctionStore ,
48+ // Nodes to execute
1649 nodes : HashMap < i64 , NodeFunction > ,
50+ // Connection for Remote Function Execution => Actions
51+ remote : Option < & ' a dyn RemoteRuntime > ,
52+ }
53+
54+ /// Determines whether a node should be executed remotely.
55+ ///
56+ /// The current policy treats any node whose `definition_source` is not `"taurus"`
57+ /// as a remote node.
58+ fn is_remote ( node : & NodeFunction ) -> bool {
59+ node. definition_source != "taurus"
1760}
1861
1962impl < ' a > Executor < ' a > {
63+ /// Create a new executor for the given function store and node map.
64+ ///
65+ /// This does not attach a remote runtime. Remote nodes will error unless
66+ /// a runtime is provided via `with_remote_runtime`.
2067 pub fn new ( functions : & ' a FunctionStore , nodes : HashMap < i64 , NodeFunction > ) -> Self {
21- Self { functions, nodes }
68+ Self {
69+ functions,
70+ nodes,
71+ remote : None ,
72+ }
73+ }
74+
75+ /// Attach a remote runtime for executing nodes marked as remote.
76+ ///
77+ /// This is a builder-style method for ergonomic setup:
78+ /// `Executor::new(...).with_remote_runtime(&runtime)`.
79+ pub fn with_remote_runtime ( mut self , remote : & ' a dyn RemoteRuntime ) -> Self {
80+ self . remote = Some ( remote) ;
81+ self
2282 }
2383
2484 /// This is now the ONLY execution entry point.
85+ ///
86+ /// - `start_node_id` is the first node in the flow.
87+ /// - `ctx` is mutated in-place with results and errors.
88+ /// - `with_trace` controls whether the trace is printed on completion.
2589 pub fn execute ( & self , start_node_id : i64 , ctx : & mut Context , with_trace : bool ) -> Signal {
2690 let mut tracer = Tracer :: new ( ) ;
2791
@@ -33,7 +97,10 @@ impl<'a> Executor<'a> {
3397 signal
3498 }
3599
36- // Main execution loop
100+ /// Main execution loop.
101+ ///
102+ /// Executes nodes one-by-one along the `next_node_id` chain until a
103+ /// non-success signal is produced or the chain ends.
37104 fn execute_call (
38105 & self ,
39106 start_node_id : i64 ,
@@ -75,7 +142,13 @@ impl<'a> Executor<'a> {
75142 }
76143 }
77144
78- // executes a single node
145+ /// Execute a single node and return its signal and trace frame id.
146+ ///
147+ /// This handles:
148+ /// - Node lookup
149+ /// - Remote vs local dispatch
150+ /// - Argument building and eager evaluation
151+ /// - Handler invocation and result commit
79152 fn execute_single_node (
80153 & self ,
81154 node_id : i64 ,
@@ -92,6 +165,66 @@ impl<'a> Executor<'a> {
92165 }
93166 } ;
94167
168+ if is_remote ( & node) {
169+ let remote = match self . remote {
170+ Some ( r) => r,
171+ None => {
172+ let err = RuntimeError :: simple (
173+ "RemoteRuntimeNotConfigured" ,
174+ "Remote runtime not configured" . to_string ( ) ,
175+ ) ;
176+ return ( Signal :: Failure ( err) , 0 ) ;
177+ }
178+ } ;
179+
180+ let frame_id = tracer. enter_node ( node. database_id , node. runtime_function_id . as_str ( ) ) ;
181+
182+ let mut args = match self . build_args ( & node, ctx, tracer, frame_id) {
183+ Ok ( a) => a,
184+ Err ( e) => {
185+ ctx. insert_error ( node. database_id , e. clone ( ) ) ;
186+ tracer. exit_node (
187+ frame_id,
188+ Outcome :: Failure {
189+ error_preview : format ! ( "{:#?}" , e) ,
190+ } ,
191+ ) ;
192+ return ( Signal :: Failure ( e) , frame_id) ;
193+ }
194+ } ;
195+
196+ let values = match self . resolve_remote_args ( & mut args, ctx, tracer, frame_id) {
197+ Ok ( v) => v,
198+ Err ( ( sig, outcome) ) => {
199+ tracer. exit_node ( frame_id, outcome) ;
200+ return ( sig, frame_id) ;
201+ }
202+ } ;
203+
204+ let request = match self . build_remote_request ( & node, values) {
205+ Ok ( r) => r,
206+ Err ( e) => {
207+ ctx. insert_error ( node. database_id , e. clone ( ) ) ;
208+ tracer. exit_node (
209+ frame_id,
210+ Outcome :: Failure {
211+ error_preview : format ! ( "{:#?}" , e) ,
212+ } ,
213+ ) ;
214+ return ( Signal :: Failure ( e) , frame_id) ;
215+ }
216+ } ;
217+
218+ let remote_result = block_on ( remote. execute_remote ( request) ) ;
219+ let signal = match remote_result {
220+ Ok ( result) => self . map_remote_result ( result) ,
221+ Err ( err) => Signal :: Failure ( self . map_remote_error ( err) ) ,
222+ } ;
223+
224+ let final_signal = self . commit_result ( & node, signal, ctx, tracer, frame_id) ;
225+ return ( final_signal, frame_id) ;
226+ }
227+
95228 let entry = match self . functions . get ( node. runtime_function_id . as_str ( ) ) {
96229 Some ( e) => e,
97230 None => {
@@ -137,6 +270,10 @@ impl<'a> Executor<'a> {
137270 ( final_signal, frame_id)
138271 }
139272
273+ /// Build arguments for a node from literals, references, or thunks.
274+ ///
275+ /// Arguments are recorded to the tracer for debugging and inspection.
276+ /// Thunks are represented by the referenced node id.
140277 fn build_args (
141278 & self ,
142279 node : & NodeFunction ,
@@ -236,6 +373,10 @@ impl<'a> Executor<'a> {
236373 Ok ( args)
237374 }
238375
376+ /// Eagerly execute any argument marked as `Eager`.
377+ ///
378+ /// Lazy arguments are preserved as thunks until needed by a handler.
379+ /// If an eager child fails, the failure bubbles up immediately.
239380 fn force_eager_args (
240381 & self ,
241382 _node : & NodeFunction ,
@@ -283,6 +424,10 @@ impl<'a> Executor<'a> {
283424 Ok ( ( ) )
284425 }
285426
427+ /// Invoke a local handler with a closure for lazy execution.
428+ ///
429+ /// The closure will evaluate a thunk node and link its trace to the
430+ /// current execution frame.
286431 fn invoke_handler (
287432 & self ,
288433 entry : & HandlerFunctionEntry ,
@@ -302,6 +447,7 @@ impl<'a> Executor<'a> {
302447 ( entry. handler ) ( args, ctx, & mut run)
303448 }
304449
450+ /// Persist the final signal into the context and close the trace frame.
305451 fn commit_result (
306452 & self ,
307453 node : & NodeFunction ,
@@ -363,6 +509,102 @@ impl<'a> Executor<'a> {
363509 }
364510 }
365511 }
512+
513+ /// Resolve all arguments for a remote call.
514+ ///
515+ /// Remote execution requires concrete values, so any thunks are executed
516+ /// eagerly and replaced with their resulting `Value`.
517+ fn resolve_remote_args (
518+ & self ,
519+ args : & mut [ Argument ] ,
520+ ctx : & mut Context ,
521+ tracer : & mut dyn ExecutionTracer ,
522+ parent_frame : u64 ,
523+ ) -> Result < Vec < Value > , ( Signal , Outcome ) > {
524+ let mut values = Vec :: with_capacity ( args. len ( ) ) ;
525+
526+ for ( i, arg) in args. iter_mut ( ) . enumerate ( ) {
527+ match arg {
528+ Argument :: Eval ( v) => values. push ( v. clone ( ) ) ,
529+ Argument :: Thunk ( id) => {
530+ tracer. mark_thunk ( parent_frame, i, true , true ) ;
531+ let ( child_sig, child_root) = self . execute_call ( * id, ctx, tracer) ;
532+ tracer. link_child (
533+ parent_frame,
534+ child_root,
535+ EdgeKind :: EagerCall { arg_index : i } ,
536+ ) ;
537+
538+ match child_sig {
539+ Signal :: Success ( v) => {
540+ * arg = Argument :: Eval ( v. clone ( ) ) ;
541+ values. push ( v) ;
542+ }
543+ s => {
544+ return Err ( (
545+ s,
546+ Outcome :: Failure {
547+ error_preview : "Eager child failed" . into ( ) ,
548+ } ,
549+ ) ) ;
550+ }
551+ }
552+ }
553+ }
554+ }
555+
556+ Ok ( values)
557+ }
558+
559+ /// Build a remote execution request from a node and its resolved values.
560+ ///
561+ /// Values are mapped to their parameter ids for the remote runtime.
562+ fn build_remote_request (
563+ & self ,
564+ node : & NodeFunction ,
565+ values : Vec < Value > ,
566+ ) -> Result < ExecutionRequest , RuntimeError > {
567+ if node. parameters . len ( ) != values. len ( ) {
568+ return Err ( RuntimeError :: simple_str (
569+ "RemoteParameterMismatch" ,
570+ "Remote parameter count mismatch" ,
571+ ) ) ;
572+ }
573+
574+ let mut fields = HashMap :: new ( ) ;
575+ for ( param, value) in node. parameters . iter ( ) . zip ( values. into_iter ( ) ) {
576+ fields. insert ( param. runtime_parameter_id . clone ( ) , value) ;
577+ }
578+
579+ Ok ( ExecutionRequest {
580+ execution_identifier : format ! ( "node-{}" , node. database_id) ,
581+ function_identifier : node. runtime_function_id . clone ( ) ,
582+ parameters : Some ( Struct { fields } ) ,
583+ project_id : 0 ,
584+ } )
585+ }
586+
587+ /// Map a remote execution result into a local `Signal`.
588+ fn map_remote_result ( & self , result : ExecutionResult ) -> Signal {
589+ match result. result {
590+ Some ( ExecutionOutcome :: Success ( v) ) => Signal :: Success ( v) ,
591+ Some ( ExecutionOutcome :: Error ( err) ) => Signal :: Failure ( self . map_remote_error ( err) ) ,
592+ None => Signal :: Failure ( RuntimeError :: simple_str (
593+ "RemoteExecutionError" ,
594+ "Remote execution returned no result" ,
595+ ) ) ,
596+ }
597+ }
598+
599+ /// Map a remote runtime error into a local `RuntimeError`.
600+ fn map_remote_error ( & self , err : ActionRuntimeError ) -> RuntimeError {
601+ RuntimeError :: new (
602+ err. code ,
603+ err. description
604+ . unwrap_or_else ( || "Remote runtime error" . to_string ( ) ) ,
605+ None ,
606+ )
607+ }
366608}
367609
368610fn preview_value ( value : & Value ) -> String {
@@ -371,16 +613,12 @@ fn preview_value(value: &Value) -> String {
371613
372614fn format_value_json ( value : & Value ) -> String {
373615 match value. kind . as_ref ( ) {
374- Some ( Kind :: NumberValue ( v) ) => {
375- match v. number {
376- Some ( kind) => {
377- match kind {
378- tucana:: shared:: number_value:: Number :: Integer ( i) => i. to_string ( ) ,
379- tucana:: shared:: number_value:: Number :: Float ( f) => f. to_string ( ) ,
380- }
381- }
382- _ => "null" . to_string ( ) ,
383- }
616+ Some ( Kind :: NumberValue ( v) ) => match v. number {
617+ Some ( kind) => match kind {
618+ tucana:: shared:: number_value:: Number :: Integer ( i) => i. to_string ( ) ,
619+ tucana:: shared:: number_value:: Number :: Float ( f) => f. to_string ( ) ,
620+ } ,
621+ _ => "null" . to_string ( ) ,
384622 } ,
385623 Some ( Kind :: BoolValue ( v) ) => v. to_string ( ) ,
386624 Some ( Kind :: StringValue ( v) ) => format ! ( "{:?}" , v) ,
0 commit comments