fix(executor): cap per-execution durable fan-out width
`trigger_depth` bounds how DEEP a trigger/invoke chain runs, but nothing bounded
how WIDE one execution could fan out. A single anonymous request running
`for i in 0..1_000_000 { invoke_async("w", #{}) }` costs a few Rhai ops plus one
cheap outbox INSERT per iteration, so within the op / wall-clock budget it could
write ~10^5 durable rows — each dispatched as its own execution — flooding the
outbox and dispatcher (a durable-amplification DoS).
Add a per-execution ceiling on DURABLE emissions (`invoke_async`,
`pubsub::publish_durable`, `queue::enqueue`, and the shared-topic/-queue
variants), env `PICLOUD_MAX_EMISSIONS_PER_EXECUTION` (default 1000). It's a
thread-local counter wrapped by a re-entrancy-aware `EmissionBudgetScope` around
every `execute_ast`: the OUTERMOST scope resets it, so a fresh dispatched
handler (a new pooled-thread task) gets a full budget while a synchronous
`invoke()` / interceptor re-entry nested in the same call stack SHARES it (fan-
out counted across the whole synchronous chain). The outermost Drop re-zeroes
the counter so a pooled thread never leaks a count into the next task.
Pinned by an `emit_budget` unit test (outermost resets, nested shares, ceiling
trips); the invoke/queue/pubsub/workflow journeys (small counts) stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
module.set_native_fn(
|
||||
"enqueue",
|
||||
move |name: &str, message: Dynamic| -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::enqueue")?;
|
||||
let json = message_to_json(&message)?;
|
||||
enqueue_blocking(&svc, &cx, name, json, EnqueueOpts::default())
|
||||
},
|
||||
@@ -54,6 +55,7 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
module.set_native_fn(
|
||||
"enqueue",
|
||||
move |name: &str, message: Dynamic, opts: Map| -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::enqueue")?;
|
||||
let json = message_to_json(&message)?;
|
||||
let opts = parse_opts(&opts)?;
|
||||
enqueue_blocking(&svc, &cx, name, json, opts)
|
||||
@@ -131,6 +133,7 @@ fn group_enqueue(
|
||||
message: Dynamic,
|
||||
opts: EnqueueOpts,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::shared_collection")?;
|
||||
let json = message_to_json(&message)?;
|
||||
let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(
|
||||
|
||||
Reference in New Issue
Block a user