Skip to content

Commit 02b3615

Browse files
committed
feat: integrated remote exeutor inside of execution loop
1 parent cf40061 commit 02b3615

3 files changed

Lines changed: 265 additions & 14 deletions

File tree

crates/core/src/context/executor.rs

Lines changed: 252 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,91 @@
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+
124
use crate::context::argument::{Argument, ParameterNode};
225
use crate::context::context::{Context, ContextResult};
326
use crate::context::registry::{FunctionStore, HandlerFunctionEntry};
427
use crate::context::signal::Signal;
528
use crate::debug::trace::{ArgKind, ArgTrace, EdgeKind, Outcome, ReferenceKind};
629
use crate::debug::tracer::{ExecutionTracer, Tracer};
730
use crate::runtime::error::RuntimeError;
31+
use crate::runtime::remote::RemoteRuntime;
832

33+
use futures_lite::future::block_on;
934
use std::collections::HashMap;
35+
use tucana::aquila::execution_result::Result as ExecutionOutcome;
36+
use tucana::aquila::{ActionRuntimeError, ExecutionRequest, ExecutionResult};
1037
use tucana::shared::reference_value::Target;
1138
use 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`.
1445
pub 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

1962
impl<'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

368610
fn preview_value(value: &Value) -> String {
@@ -371,16 +613,12 @@ fn preview_value(value: &Value) -> String {
371613

372614
fn 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),

crates/core/src/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod error;
2+
pub mod remote;
23
pub mod functions;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use std::future::Future;
2+
use std::pin::Pin;
3+
use tucana::aquila::{ActionRuntimeError, ExecutionRequest, ExecutionResult};
4+
5+
pub trait RemoteRuntime {
6+
fn supports(&self, function_identifier: &str) -> bool;
7+
8+
fn execute_remote(
9+
&self,
10+
request: ExecutionRequest,
11+
) -> Pin<Box<dyn Future<Output = Result<ExecutionResult, ActionRuntimeError>> + Send + '_>>;
12+
}

0 commit comments

Comments
 (0)