From 1ebf94b135256e70490a0e00f1e3670df48ed39a Mon Sep 17 00:00:00 2001 From: udo Date: Fri, 17 Jul 2026 23:53:46 +0000 Subject: [PATCH] Prove pooled workspace request isolation --- scripts/run_cli_tests.sh | 2 +- site/tests/cli_runner.uce | 33 +++++++++++++++++++++++++++++++-- site/tests/pool-isolation.uce | 34 ++++++++++++++++++++++++++++++++++ src/wasm/worker.cpp | 4 ++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 site/tests/pool-isolation.uce diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 367c696..79d9750 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -58,7 +58,7 @@ if [[ "$action" == "list" ]]; then fi if [[ "$action" == "run" ]]; then - groups=(demo http site doc-gate security task-lifetime starter tcp) + groups=(demo http site doc-gate security task-lifetime pool-isolation starter tcp) if [[ "$include_kill" == "1" ]]; then groups+=(wasm-kill) fi diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce index 2c03fe6..c266c09 100644 --- a/site/tests/cli_runner.uce +++ b/site/tests/cli_runner.uce @@ -384,6 +384,33 @@ void cli_run_task_lifetime_smoke() cli_test_case("uce_task_lifetime:delayed callback after request return", res.status == 200 && cli_contains(res.body, "late task scheduled") && cli_contains(observed, "late callback ran"), "status=" + std::to_string(res.status) + " observed=" + cli_truncate(observed)); } +void cli_run_pool_isolation() +{ + DValue worker_counts; + bool clean = true; + String detail; + for(u64 i = 0; i < 64; i++) + { + String token = "request-" + std::to_string(i); + CliHttpResponse res = cli_frontend("/tests/pool-isolation.uce?token=" + token); + StringList parts = split(trim(res.body), "|"); + bool current = res.status == 200 && parts.size() == 3 && + parts[0] == "fresh" && parts[1] == token && int_val(parts[2]) > 0; + if(!current && detail == "") + detail = "request=" + std::to_string(i) + " status=" + std::to_string(res.status) + " body=" + cli_truncate(res.body); + clean = clean && current; + if(parts.size() == 3) + worker_counts[parts[2]] = worker_counts[parts[2]].to_u64() + 1; + } + bool reused = false; + worker_counts.each([&](DValue count, String) { + if(count.to_u64() > 1) + reused = true; + }); + cli_test_case("uce_pool_isolation:recycled slots reset request state", clean && reused, + clean && reused ? "64 requests retained fresh globals, constructors, ONCE, context, and worker identity" : first(detail, "no worker served a repeated request")); +} + void cli_run_tcp_smoke() { u64 fd80 = socket_connect("127.0.0.1", 80); @@ -413,7 +440,7 @@ void cli_run_wasm_kill(bool include_kill) void cli_print_list(bool include_kill) { print("UCE CLI test groups:\n"); - print(" demo, http, site, doc-gate, security, task-lifetime, starter, tcp"); + print(" demo, http, site, doc-gate, security, task-lifetime, pool-isolation, starter, tcp"); if(include_kill) print(", wasm-kill"); print("\n"); @@ -434,6 +461,8 @@ bool cli_run_group(String group, bool skip_local_service_pages, bool include_kil cli_run_security_smoke(); else if(group == "task-lifetime") cli_run_task_lifetime_smoke(); + else if(group == "pool-isolation") + cli_run_pool_isolation(); else if(group == "starter") cli_run_starter_parity(); else if(group == "tcp") @@ -472,7 +501,7 @@ CLI(Request& context) cli_run_group(group, input["skip_local_service_pages"].to_bool(), include_kill); else { - for(String item : { "demo", "http", "site", "doc-gate", "security", "task-lifetime", "starter", "tcp" }) + for(String item : { "demo", "http", "site", "doc-gate", "security", "task-lifetime", "pool-isolation", "starter", "tcp" }) cli_run_group(item, input["skip_local_service_pages"].to_bool(), include_kill); cli_run_group("wasm-kill", input["skip_local_service_pages"].to_bool(), include_kill); } diff --git a/site/tests/pool-isolation.uce b/site/tests/pool-isolation.uce new file mode 100644 index 0000000..6008434 --- /dev/null +++ b/site/tests/pool-isolation.uce @@ -0,0 +1,34 @@ +u64 pool_request_global = 0; +u64 pool_constructor_value = 0; + +struct PoolIsolationConstructor +{ + PoolIsolationConstructor() + { + pool_constructor_value = 17; + } +}; + +PoolIsolationConstructor pool_isolation_constructor; + +ONCE(Request& context) +{ + pool_request_global++; +} + +RENDER(Request& context) +{ + String token = context.get["token"]; + bool clean = pool_request_global == 1 && + pool_constructor_value == 17 && + context.call["pool-leak"].to_string() == "" && + context.props["pool-leak"].to_string() == ""; + DValue perf = request_perf(); + print(clean ? "fresh|" : "leaked|", token, "|", (u64)perf["worker_pid"].to_u64()); + + pool_request_global = 999; + pool_constructor_value = 999; + context.call["pool-leak"] = token; + context.props["pool-leak"] = token; + context.get["token"] = "dirty"; +} diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index c857448..d60a4a8 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1201,6 +1201,10 @@ private: { wasmtime::Config config; wasmtime::PoolAllocationConfig pool; + // A workspace owns one core plus at most table_headroom side modules; + // side modules import its single memory/table. Keep allocator capacity at + // the existing handler-table boundary instead of imposing its 1000-slot default. + pool.total_core_instances(4097); config.pooling_allocation_strategy(pool); config.epoch_interruption(true); // CRITICAL: the host (linux_fastcgi.cpp) installs its own SIGSEGV/SIGILL/