From b69d3683a9fce926fd9564b6ef7928591b5bfb9a Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 12 Nov 2025 13:10:40 -0500 Subject: [PATCH 1/7] Stop leaking initial worker --- packages/playground/cli/src/run-cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 67af7faa01..62861d5916 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -925,6 +925,11 @@ export async function runCLI(args: RunCLIArgs): Promise { nativeInternalDirPath ); + playgroundsToCleanUp.push({ + playground: initialPlayground, + worker: initialWorker.worker, + }); + await initialPlayground.isReady(); wordPressReady = true; logger.log(`Booted!`); From 6eceebb34180e7fd8b85892892a22106be475ba4 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 12 Nov 2025 13:11:20 -0500 Subject: [PATCH 2/7] Only consider multiple workers for server mode --- packages/playground/cli/src/run-cli.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 62861d5916..30e9bdd293 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -619,12 +619,19 @@ export async function runCLI(args: RunCLIArgs): Promise { const serverUrl = `http://${host}:${port}`; const siteUrl = args['site-url'] || serverUrl; - // Create the blueprints handler - const targetWorkerCount = args.experimentalMultiWorker ?? 1; - // Account for the initial worker which is discarded after setup. - const totalWorkerCountIncludingSetupWorker = targetWorkerCount + 1; + const targetWorkerCount = + args.command === 'server' + ? args.experimentalMultiWorker ?? 1 + : 1; + const totalWorkersToSpawn = + args.command === 'server' + ? // Account for the initial worker + // which is discarded by the server after setup. + targetWorkerCount + 1 + : targetWorkerCount; + const processIdSpaceLength = Math.floor( - Number.MAX_SAFE_INTEGER / totalWorkerCountIncludingSetupWorker + Number.MAX_SAFE_INTEGER / totalWorkersToSpawn ); /* @@ -884,7 +891,7 @@ export async function runCLI(args: RunCLIArgs): Promise { // Kick off worker threads now to save time later. // There is no need to wait for other async processes to complete. const promisedWorkers = spawnWorkerThreads( - totalWorkerCountIncludingSetupWorker, + totalWorkersToSpawn, handler.getWorkerType(), ({ exitCode, workerIndex }) => { // We are already disposing, so worker exit is expected From 81a78631550b588fcf8220872802af5d3137e2fb Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 12 Nov 2025 13:16:56 -0500 Subject: [PATCH 3/7] Prevent users from enabling multi-worker outside of server mode --- packages/playground/cli/src/run-cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 30e9bdd293..32fb4e66f7 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -328,6 +328,11 @@ export async function parseOptionsAndRunCLI() { } if (args['experimental-multi-worker'] !== undefined) { + if (args.command !== 'server') { + throw new Error( + 'The --experimental-multi-worker flag is only supported when running the server command.' + ); + } if (args['experimental-multi-worker'] <= 1) { throw new Error( 'The --experimental-multi-worker flag must be a positive integer greater than 1.' From bc6b65ed1c961dce58c3f1019c7680d62d82f5d5 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 12 Nov 2025 19:28:47 -0500 Subject: [PATCH 4/7] Fix multi-worker command check --- packages/playground/cli/src/run-cli.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 32fb4e66f7..3139f8262e 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -328,7 +328,8 @@ export async function parseOptionsAndRunCLI() { } if (args['experimental-multi-worker'] !== undefined) { - if (args.command !== 'server') { + const cliCommand = args._[0] as string; + if (cliCommand !== 'server') { throw new Error( 'The --experimental-multi-worker flag is only supported when running the server command.' ); From 447d8de012326528a76f94800d7b9e04d9ee0ca3 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 12 Nov 2025 19:29:07 -0500 Subject: [PATCH 5/7] Expand unbuilt Playground CLI tests to test asyncify --- .../cli/tests/test-running-unbuilt-cli.sh | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/playground/cli/tests/test-running-unbuilt-cli.sh b/packages/playground/cli/tests/test-running-unbuilt-cli.sh index 19037ced2d..774b5c1826 100755 --- a/packages/playground/cli/tests/test-running-unbuilt-cli.sh +++ b/packages/playground/cli/tests/test-running-unbuilt-cli.sh @@ -47,24 +47,13 @@ function test_playground_cli() { fi } -function test_playground_cli_multi_worker() { - MULTIWORKER_WP_PATH="$HOME/playground-cli-multi-worker-wp" - mkdir -p "$MULTIWORKER_WP_PATH" - - # TODO: Also test with asyncify once we multiple workers there. - test_playground_cli unbuilt-jspi \ - --mountBeforeInstall="$MULTIWORKER_WP_PATH:/wordpress" \ - --experimentalMultiWorker -} - echo test_playground_cli unbuilt-asyncify echo test_playground_cli unbuilt-jspi echo - -test_playground_cli_multi_worker +test_playground_cli unbuilt-asyncify --experimental-multi-worker +echo +test_playground_cli unbuilt-jspi --experimental-multi-worker echo -echo 'Retesting multi-worker to test with a pre-existing WordPress installation where we have seen bugs.' -test_playground_cli_multi_worker From 56d63c9a516ea1980b241081227de3168d0861b7 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 15:13:29 -0500 Subject: [PATCH 6/7] Attempt to fix failing tests that run indefinitely --- .../es-modules-and-vitest/run-tests.ts | 39 +++++++++++++++---- .../es-modules-and-vitest/tsconfig.json | 13 +++++++ 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 packages/playground/test-built-npm-packages/es-modules-and-vitest/tsconfig.json diff --git a/packages/playground/test-built-npm-packages/es-modules-and-vitest/run-tests.ts b/packages/playground/test-built-npm-packages/es-modules-and-vitest/run-tests.ts index 32427a7ce4..b72bbdf1ee 100644 --- a/packages/playground/test-built-npm-packages/es-modules-and-vitest/run-tests.ts +++ b/packages/playground/test-built-npm-packages/es-modules-and-vitest/run-tests.ts @@ -21,6 +21,7 @@ function red(text: string) { type Result = { phpVersion: string; code: number | null; + timeout?: boolean; }; const results: Result[] = []; @@ -47,7 +48,7 @@ for (const phpVersion of SupportedPHPVersions.filter( } ); - await new Promise((resolve) => { + const promiseToClose = new Promise((resolve) => { child.on('close', (code) => { results.push({ phpVersion, @@ -56,25 +57,49 @@ for (const phpVersion of SupportedPHPVersions.filter( resolve(); }); }); + const promiseToTimeout = new Promise((resolve, reject) => { + setTimeout(() => { + console.error(`PHP ${phpVersion}: timed out.`); + reject(new Error('Test timed out')); + }, 30000); + }); + try { + await Promise.race([promiseToClose, promiseToTimeout]); + } catch (e) { + results.push({ + phpVersion, + code: null, + timeout: true, + }); + child.kill('SIGKILL'); + } } console.log('Results:'); for (const result of results) { - console.log( - `PHP ${result.phpVersion}: ${ - result.code === 0 ? green('PASS') : red('FAIL') - } with exit code ${result.code}` - ); + if (result.timeout) { + console.log(red(`PHP ${result.phpVersion}: ${red('timed out')}.`)); + } else { + console.log( + `PHP ${result.phpVersion}: ${ + result.code === 0 ? green('PASS') : red('FAIL') + } with exit code ${result.code}` + ); + } } const numPassed = results.filter((r) => r.code === 0).length; -const numFailed = results.filter((r) => r.code !== 0).length; +const numFailed = results.filter((r) => r.code !== 0 && !r.timeout).length; +const numTimedOut = results.filter((r) => r.timeout).length; if (numPassed > 0) { console.log(green(`${numPassed} / ${results.length} tests passed`)); } if (numFailed > 0) { console.log(red(`${numFailed} / ${results.length} tests failed`)); } +if (numTimedOut > 0) { + console.log(red(`${numTimedOut} / ${results.length} tests timed out`)); +} if (numFailed > 0) { process.exit(1); diff --git a/packages/playground/test-built-npm-packages/es-modules-and-vitest/tsconfig.json b/packages/playground/test-built-npm-packages/es-modules-and-vitest/tsconfig.json new file mode 100644 index 0000000000..aca8683f5b --- /dev/null +++ b/packages/playground/test-built-npm-packages/es-modules-and-vitest/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} From 8221c53a34bea59f68c05dea0ecd352b64622cbb Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 13 Nov 2025 15:52:50 -0500 Subject: [PATCH 7/7] Further fixes --- packages/playground/cli/src/run-cli.ts | 41 +++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 3139f8262e..1cebf46066 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -550,10 +550,10 @@ export async function runCLI(args: RunCLIArgs): Promise { let loadBalancer: LoadBalancer; let playground: RemoteAPI; - const playgroundsToCleanUp: { - playground: RemoteAPI; - worker: Worker; - }[] = []; + const playgroundsToCleanUp: Map< + Worker, + RemoteAPI + > = new Map(); /** * Expand auto-mounts to include the necessary mounts and steps @@ -883,10 +883,12 @@ export async function runCLI(args: RunCLIArgs): Promise { disposing = true; await Promise.all( - playgroundsToCleanUp.map(async ({ playground, worker }) => { - await playground.dispose(); - await worker.terminate(); - }) + [...playgroundsToCleanUp].map( + async ([worker, playground]) => { + await playground.dispose(); + await worker.terminate(); + } + ) ); if (server) { await new Promise((resolve) => server.close(resolve)); @@ -937,11 +939,10 @@ export async function runCLI(args: RunCLIArgs): Promise { fileLockManagerPort, nativeInternalDirPath ); - - playgroundsToCleanUp.push({ - playground: initialPlayground, - worker: initialWorker.worker, - }); + playgroundsToCleanUp.set( + initialWorker.worker, + initialPlayground + ); await initialPlayground.isReady(); wordPressReady = true; @@ -981,16 +982,16 @@ export async function runCLI(args: RunCLIArgs): Promise { // be configured differently than post-boot workers. // For example, we do not enable Xdebug by default for the initial worker. await loadBalancer.removeWorker(initialPlayground); - // TODO: Wrap in a cleanup function and reuse for all worker cleanup. await initialPlayground.dispose(); await initialWorker.worker.terminate(); + playgroundsToCleanUp.delete(initialWorker.worker); } logger.log(`Preparing workers...`); // Boot additional workers using the handler const initialWorkerProcessIdSpace = processIdSpaceLength; - // Just take the first Playground instance to be relayed to others. + // Just take the first Playground instance to be returned to the caller. [playground] = await Promise.all( workers.map(async (worker, index) => { const firstProcessId = @@ -1009,11 +1010,10 @@ export async function runCLI(args: RunCLIArgs): Promise { nativeInternalDirPath, }); - playgroundsToCleanUp.push({ - playground: additionalPlayground, - worker: worker.worker, - }); - + playgroundsToCleanUp.set( + worker.worker, + additionalPlayground + ); loadBalancer.addWorker(additionalPlayground); return additionalPlayground; @@ -1048,6 +1048,7 @@ export async function runCLI(args: RunCLIArgs): Promise { if (await playground?.fileExists(errorLogPath)) { phpLogs = await playground.readFileAsText(errorLogPath); } + await disposeCLI(); throw new Error(phpLogs, { cause: error }); } },