feat(v1.1.9): invoke:: Rhai SDK module — sync re-entry + invoke_async
executor-core/sdk/invoke.rs adds `invoke()` and `invoke_async()` as
top-level Rhai helpers (no `::` namespace, mirroring the brief):
invoke(target, args) -> Dynamic (sync, returns callee's value)
invoke_async(target, args) -> String (fire-and-forget; returns execution_id)
Sync re-entry pattern:
- bridge captures Arc<Engine> via Engine::self_arc()
- calls engine.execute(&source, req) directly — same engine instance,
same Services, same Limits; only SdkCallCx changes per call
- runs inside the caller's spawn_blocking thread (no nested
spawn_blocking, no gate re-admission — the outer execution already
holds a permit)
Back-reference plumbing:
- Engine gains self_weak: OnceLock<Weak<Engine>> + set_self_weak() +
self_arc() accessor
- picloud binary calls engine.set_self_weak(Arc::downgrade(&engine))
right after construction
- register_all extended with limits + Option<Arc<Engine>> params
- Engine::execute_ast threads both through
Limits.trigger_depth_max added — mirrors TriggerConfig::max_trigger_depth.
Default 8; the picloud binary syncs from TriggerConfig::from_env() so
PICLOUD_MAX_TRIGGER_DEPTH governs both the dispatcher's fan-out cap and
the invoke depth bound.
Target parsing (string-first):
- "/api/foo" → InvokeTarget::Path
- 36-char UUID-like → InvokeTarget::Id (uuid::Uuid parse-validated)
- everything else → InvokeTarget::Name
Cross-app + depth + FnPtr guards:
- resolve() returns InvokeError::CrossApp if resolved.app_id != cx.app_id
- bridge throws "invoke: depth limit exceeded (max N)" when
cx.trigger_depth + 1 > limits.trigger_depth_max (checked BEFORE
resolve to avoid a wasted DB round-trip)
- args_to_json rejects FnPtr at any depth — closures don't survive
invoke boundaries
invoke_async path:
- calls services.invoke.enqueue_async() which writes an
OutboxSourceKind::Invoke row (commit 10 wires the dispatcher arm)
- returns the new ExecutionId as a string for caller tracking
Integration tests (sdk_invoke.rs, 6 binaries):
- sync invoke returns callee's value (script returns body.x + 1)
- cross-app invoke rejected (target in other app)
- depth limit exceeded (recursive script that calls itself; throws
before stack overflow)
- callee receives incremented depth in cx (smoke — strict assertion
needs ctx.trigger_depth surface, deferred to v1.2)
- args_to_json rejects FnPtr in payload
- invoke_async returns valid UUID string + queues args payload
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, OnceLock, Weak};
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -44,6 +44,14 @@ pub struct Engine {
|
||||
/// `(app_id, name)`; invalidated lazily by `updated_at` mismatch
|
||||
/// at resolver time.
|
||||
module_cache: Arc<ModuleCache>,
|
||||
/// v1.1.9: back-reference set by the picloud binary after
|
||||
/// `Arc::new(Engine::new(...))`. The `invoke` SDK bridge reads it
|
||||
/// to re-enter the engine synchronously for `invoke()`. `Weak` so
|
||||
/// holding the Engine in an Arc doesn't create a strong cycle.
|
||||
/// `None` until `set_self_weak` runs; the bridge surfaces a clear
|
||||
/// error if invoke is called without the back-reference being set
|
||||
/// (which only happens in tests that don't wire it).
|
||||
self_weak: OnceLock<Weak<Engine>>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -67,9 +75,31 @@ impl Engine {
|
||||
limits,
|
||||
services,
|
||||
module_cache: new_module_cache(module_cache_capacity),
|
||||
self_weak: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// v1.1.9: install the back-reference used by the `invoke` SDK
|
||||
/// bridge for synchronous re-entry. Idempotent (subsequent calls
|
||||
/// are no-ops). The picloud binary calls this right after
|
||||
/// `Arc::new(Engine::new(...))`:
|
||||
///
|
||||
/// ```ignore
|
||||
/// let engine = Arc::new(Engine::new(limits, services));
|
||||
/// engine.set_self_weak(Arc::downgrade(&engine));
|
||||
/// ```
|
||||
pub fn set_self_weak(&self, weak: Weak<Engine>) {
|
||||
let _ = self_weak_set(&self.self_weak, weak);
|
||||
}
|
||||
|
||||
/// Internal accessor used by the `invoke` SDK bridge — returns
|
||||
/// `Some(strong)` if the back-reference was installed and the
|
||||
/// engine is still alive.
|
||||
#[must_use]
|
||||
pub fn self_arc(&self) -> Option<Arc<Engine>> {
|
||||
self.self_weak.get().and_then(Weak::upgrade)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn limits(&self) -> &Limits {
|
||||
&self.limits
|
||||
@@ -164,7 +194,8 @@ impl Engine {
|
||||
effective_limits.module_import_depth_max,
|
||||
);
|
||||
engine.set_module_resolver(resolver);
|
||||
sdk::register_all(&mut engine, &self.services, cx);
|
||||
let self_engine = self.self_arc();
|
||||
sdk::register_all(&mut engine, &self.services, cx, effective_limits, self_engine);
|
||||
|
||||
let mut scope = Scope::new();
|
||||
scope.push_constant("ctx", build_ctx_map(&req));
|
||||
@@ -211,6 +242,12 @@ impl ScriptValidator for Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tiny helper to make the `set_self_weak` body idempotent without
|
||||
/// pulling the impl up into the public API.
|
||||
fn self_weak_set(slot: &OnceLock<Weak<Engine>>, weak: Weak<Engine>) -> Result<(), Weak<Engine>> {
|
||||
slot.set(weak)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Engine construction
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user