perf(interceptors): per-execution resolve cache (§9.4 M6)
Memoizes the interceptor chain resolution per execution tree, keyed by (app_id, service, op). The FIRST hooked-eligible op pays the one chain query; every later op reuses it — so N kv::sets in a script with no interceptors now issue ONE resolve, not N (the dominant, zero-marker case caches an empty chain). InterceptorCacheScope is an RAII scope entered in execute_ast next to the emission budget, same re-entrancy model: a synchronous invoke/interceptor re-entry shares the cache, and it is cleared at the outermost boundary so a pooled thread never serves a foreign app's cache. Pinned by an executor-core test: 25 sets → 1 resolve, and a fresh execution → a new resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -329,3 +329,81 @@ async fn kv_bridge_preserves_cross_app_isolation() {
|
||||
by app_id, not execution_id/script_id"
|
||||
);
|
||||
}
|
||||
|
||||
// --- §9.4 M6: per-execution interceptor resolve cache ---------------------
|
||||
|
||||
/// An `InterceptorService` that resolves to an EMPTY chain (nothing hooked) but
|
||||
/// counts every `resolve` call, so a test can prove the per-execution cache
|
||||
/// collapses N same-`(service, op)` resolves into one.
|
||||
#[derive(Default)]
|
||||
struct CountingInterceptors {
|
||||
calls: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl picloud_shared::InterceptorService for CountingInterceptors {
|
||||
async fn resolve(
|
||||
&self,
|
||||
_cx: &SdkCallCx,
|
||||
_service: &str,
|
||||
_op: &str,
|
||||
) -> Result<picloud_shared::InterceptorChain, String> {
|
||||
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(picloud_shared::InterceptorChain::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_engine_with_interceptors(ic: Arc<CountingInterceptors>) -> Arc<Engine> {
|
||||
let services = Services::new(
|
||||
Arc::new(InMemoryKv::default()),
|
||||
Arc::new(NoopDocsService),
|
||||
Arc::new(NoopDeadLetterService),
|
||||
Arc::new(NoopEventEmitter),
|
||||
Arc::new(NoopModuleSource),
|
||||
Arc::new(NoopHttpService),
|
||||
Arc::new(picloud_shared::NoopFilesService),
|
||||
Arc::new(picloud_shared::NoopPubsubService),
|
||||
Arc::new(picloud_shared::NoopSecretsService),
|
||||
Arc::new(picloud_shared::NoopEmailService),
|
||||
Arc::new(picloud_shared::NoopUsersService),
|
||||
Arc::new(picloud_shared::NoopQueueService),
|
||||
Arc::new(picloud_shared::NoopInvokeService),
|
||||
Arc::new(picloud_shared::NoopVarsService),
|
||||
)
|
||||
.with_interceptors(ic);
|
||||
Arc::new(Engine::new(Limits::default(), services))
|
||||
}
|
||||
|
||||
/// M6: within ONE execution, N `kv::set`s of the same `(service, op)` resolve
|
||||
/// the interceptor chain at most once (the empty chain is cached), and a SECOND
|
||||
/// execution starts fresh (the cache is cleared at the outermost boundary).
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn resolve_is_cached_per_execution() {
|
||||
let ic = Arc::new(CountingInterceptors::default());
|
||||
let engine = make_engine_with_interceptors(ic.clone());
|
||||
let app = AppId::new();
|
||||
|
||||
// 25 sets in one execution — all the same (kv, set).
|
||||
let src = r#"
|
||||
let c = kv::collection("c");
|
||||
let n = 0;
|
||||
while n < 25 { c.set("k" + n, n); n += 1; }
|
||||
"done"
|
||||
"#;
|
||||
let body = run_script(engine.clone(), src, baseline_request(app)).await;
|
||||
assert_eq!(body, Value::from("done"));
|
||||
assert_eq!(
|
||||
ic.calls.load(std::sync::atomic::Ordering::SeqCst),
|
||||
1,
|
||||
"25 kv::sets in one execution must resolve the (kv, set) chain exactly once"
|
||||
);
|
||||
|
||||
// A second execution resolves again (the cache cleared at the outermost exit).
|
||||
let body = run_script(engine, src, baseline_request(app)).await;
|
||||
assert_eq!(body, Value::from("done"));
|
||||
assert_eq!(
|
||||
ic.calls.load(std::sync::atomic::Ordering::SeqCst),
|
||||
2,
|
||||
"a fresh execution must not reuse the previous execution's resolve cache"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user