Skip to content

Commit 19b21c8

Browse files
committed
Terminate poisoned Playground workers
1 parent 6552a47 commit 19b21c8

4 files changed

Lines changed: 89 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"test:playground-custom-archive-cache": "tsx tests/playground-custom-archive-cache.test.ts && tsx tests/playground-custom-archive-cache-process.test.ts && tsx tests/playground-custom-archive-cache.integration.test.ts",
126126
"test:phpunit-runtime-failure-diagnostics": "tsx tests/phpunit-runtime-failure-diagnostics.test.ts",
127127
"test:phpunit-runtime-rejection": "tsx tests/phpunit-runtime-rejection.test.ts",
128+
"test:playground-worker-runtime-rejection": "tsx tests/playground-worker-runtime-rejection.test.ts",
128129
"test:php-wasm-extension-manifests": "tsx tests/php-wasm-extension-manifests.test.ts",
129130
"test:browser-task-builder": "tsx tests/browser-task-builder.test.ts",
130131
"test:browser-runtime-generic-invoker": "tsx tests/browser-runtime-generic-invoker.test.ts",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
diff --git a/node_modules/@wp-playground/cli/worker-thread-v1.js b/node_modules/@wp-playground/cli/worker-thread-v1.js
2+
index 93b7099..63e96bc 100644
3+
--- a/node_modules/@wp-playground/cli/worker-thread-v1.js
4+
+++ b/node_modules/@wp-playground/cli/worker-thread-v1.js
5+
@@ -157,6 +157,7 @@ async function C(r, e) {
6+
}
7+
process.on("unhandledRejection", (r) => {
8+
S.error("Unhandled rejection:", r);
9+
+ throw r;
10+
});
11+
const i = new _(), [c, p] = y(
12+
new F(new f()),
13+
diff --git a/node_modules/@wp-playground/cli/worker-thread-v2.js b/node_modules/@wp-playground/cli/worker-thread-v2.js
14+
index fb79a59..5d09b1f 100644
15+
--- a/node_modules/@wp-playground/cli/worker-thread-v2.js
16+
+++ b/node_modules/@wp-playground/cli/worker-thread-v2.js
17+
@@ -168,6 +168,7 @@ async function j(r, e) {
18+
}
19+
process.on("unhandledRejection", (r) => {
20+
E.error("Unhandled rejection:", r);
21+
+ throw r;
22+
});
23+
const i = new m(), [h, u] = k(
24+
new L(new b()),

patches/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ WP Codebox needs the repaired bounded Range decoder for the Cloudflare dev rig
88
and browser asset path before that upstream change can be merged and released.
99
Remove this patch and the package override after a published Playground package
1010
contains the same change.
11+
12+
`@wp-playground+cli+3.1.46.patch` makes blueprint worker threads rethrow
13+
unhandled runtime rejections after logging them. The released CLI otherwise
14+
keeps a poisoned PHP-WASM worker alive and leaves the parent request pending
15+
until the recipe timeout. Remove the patch after the Playground CLI ships the
16+
same worker terminalization behavior.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from "node:assert/strict"
2+
import { mkdtemp, rm, writeFile } from "node:fs/promises"
3+
import { tmpdir } from "node:os"
4+
import { join, resolve } from "node:path"
5+
import { pathToFileURL } from "node:url"
6+
import { Worker } from "node:worker_threads"
7+
8+
const root = await mkdtemp(join(tmpdir(), "wp-codebox-playground-worker-rejection-"))
9+
const preloadPath = join(root, "reject-on-command.mjs")
10+
11+
await writeFile(preloadPath, `
12+
import { parentPort } from "node:worker_threads"
13+
parentPort?.on("message", (message) => {
14+
if (message !== "wp-codebox-trigger-runtime-rejection") return
15+
const error = new WebAssembly.RuntimeError("null function or function signature mismatch")
16+
error.stack = "RuntimeError: null function or function signature mismatch\\n at php.wasm.zif_mysqli_poll (wasm://wasm/php.wasm-05996276:wasm-function[12986]:0x9949a8)"
17+
Promise.reject(error)
18+
})
19+
`, "utf8")
20+
21+
try {
22+
for (const version of ["v1", "v2"]) {
23+
await assertWorkerTerminates(version)
24+
}
25+
} finally {
26+
await rm(root, { recursive: true, force: true })
27+
}
28+
29+
async function assertWorkerTerminates(version: string): Promise<void> {
30+
const workerPath = resolve(`node_modules/@wp-playground/cli/worker-thread-${version}.js`)
31+
const worker = new Worker(pathToFileURL(workerPath), {
32+
execArgv: ["--import", pathToFileURL(preloadPath).href],
33+
})
34+
35+
let workerError: Error | undefined
36+
const exitCode = await Promise.race([
37+
new Promise<number>((resolveExit) => {
38+
worker.once("message", (message: { command?: unknown; phpPort?: { close?: () => void } }) => {
39+
if (message?.command !== "worker-script-initialized") return
40+
message.phpPort?.close?.()
41+
worker.postMessage("wp-codebox-trigger-runtime-rejection")
42+
})
43+
worker.once("error", (error) => {
44+
workerError = error
45+
})
46+
worker.once("exit", resolveExit)
47+
}),
48+
new Promise<never>((_resolve, reject) => {
49+
setTimeout(() => reject(new Error(`Playground ${version} worker remained alive after an unhandled runtime rejection`)), 3_000).unref()
50+
}),
51+
]).finally(() => worker.terminate())
52+
53+
assert.notEqual(exitCode, 0, `Playground ${version} worker must exit nonzero after an unhandled runtime rejection`)
54+
assert.ok(workerError, `Playground ${version} worker must propagate its unhandled runtime rejection`)
55+
assert.match(workerError.message, /null function or function signature mismatch/)
56+
}
57+
58+
console.log("playground worker runtime rejection terminalization ok")

0 commit comments

Comments
 (0)