Skip to content

Commit c712739

Browse files
authored
Merge pull request #2012 from Automattic/fix/2010-worker-rejection-exit
Terminate poisoned Playground PHP-WASM workers
2 parents 6552a47 + 72029ea commit c712739

4 files changed

Lines changed: 107 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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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..65f76be 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,8 @@ async function C(r, e) {
6+
}
7+
process.on("unhandledRejection", (r) => {
8+
S.error("Unhandled rejection:", r);
9+
+ if (r instanceof WebAssembly.RuntimeError && String(r.stack).includes("php.wasm"))
10+
+ throw r;
11+
});
12+
const i = new _(), [c, p] = y(
13+
new F(new f()),
14+
diff --git a/node_modules/@wp-playground/cli/worker-thread-v2.js b/node_modules/@wp-playground/cli/worker-thread-v2.js
15+
index fb79a59..77ddb67 100644
16+
--- a/node_modules/@wp-playground/cli/worker-thread-v2.js
17+
+++ b/node_modules/@wp-playground/cli/worker-thread-v2.js
18+
@@ -168,6 +168,8 @@ async function j(r, e) {
19+
}
20+
process.on("unhandledRejection", (r) => {
21+
E.error("Unhandled rejection:", r);
22+
+ if (r instanceof WebAssembly.RuntimeError && String(r.stack).includes("php.wasm"))
23+
+ throw r;
24+
});
25+
const i = new m(), [h, u] = k(
26+
new L(new b()),

patches/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ 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 fatal
13+
`WebAssembly.RuntimeError` rejections originating from `php.wasm` after logging
14+
them. Other unhandled rejections retain the released CLI's log-only behavior.
15+
Without this narrow terminalization, a poisoned PHP-WASM worker remains alive
16+
and leaves the parent request pending until the recipe timeout. Remove the patch
17+
after the Playground CLI ships the same worker terminalization behavior.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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-non-wasm-rejection") {
15+
Promise.reject(new Error("ordinary worker rejection"))
16+
setTimeout(() => parentPort?.postMessage("wp-codebox-worker-survived"), 25)
17+
}
18+
if (message === "wp-codebox-trigger-php-wasm-rejection") {
19+
const error = new WebAssembly.RuntimeError("null function or function signature mismatch")
20+
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)"
21+
Promise.reject(error)
22+
}
23+
})
24+
`, "utf8")
25+
26+
try {
27+
for (const version of ["v1", "v2"]) {
28+
await assertWorkerTerminates(version)
29+
}
30+
} finally {
31+
await rm(root, { recursive: true, force: true })
32+
}
33+
34+
async function assertWorkerTerminates(version: string): Promise<void> {
35+
const workerPath = resolve(`node_modules/@wp-playground/cli/worker-thread-${version}.js`)
36+
const worker = new Worker(pathToFileURL(workerPath), {
37+
execArgv: ["--import", pathToFileURL(preloadPath).href],
38+
})
39+
40+
let workerError: Error | undefined
41+
let initialized = false
42+
let survivedOrdinaryRejection = false
43+
const exitCode = await Promise.race([
44+
new Promise<number>((resolveExit) => {
45+
worker.once("message", (message: { command?: unknown; phpPort?: { close?: () => void } }) => {
46+
if (message?.command !== "worker-script-initialized") return
47+
initialized = true
48+
message.phpPort?.close?.()
49+
worker.postMessage("wp-codebox-trigger-non-wasm-rejection")
50+
})
51+
worker.on("message", (message: unknown) => {
52+
if (message !== "wp-codebox-worker-survived") return
53+
survivedOrdinaryRejection = true
54+
worker.postMessage("wp-codebox-trigger-php-wasm-rejection")
55+
})
56+
worker.once("error", (error) => {
57+
workerError = error
58+
})
59+
worker.once("exit", resolveExit)
60+
}),
61+
new Promise<never>((_resolve, reject) => {
62+
setTimeout(() => reject(new Error(`Playground ${version} worker remained alive after a fatal PHP-WASM rejection`)), 3_000).unref()
63+
}),
64+
]).finally(() => worker.terminate())
65+
66+
assert.ok(initialized, `Playground ${version} worker must initialize before the rejection checks`)
67+
assert.ok(survivedOrdinaryRejection, `Playground ${version} worker must preserve ordinary unhandled rejection behavior`)
68+
assert.notEqual(exitCode, 0, `Playground ${version} worker must exit nonzero after a fatal PHP-WASM rejection`)
69+
assert.ok(workerError, `Playground ${version} worker must propagate its fatal PHP-WASM rejection`)
70+
assert.match(workerError.message, /null function or function signature mismatch/)
71+
}
72+
73+
console.log("playground PHP-WASM worker rejection terminalization ok")

0 commit comments

Comments
 (0)