Skip to content

Commit 42e2ece

Browse files
[CLI] Fix run-cli leak which was revealed by repeated runCLI() calls during test (#2888)
## Motivation for the change, related issues Prior to this PR, the initial Playground CLI worker was not being cleaned up when Playground CLI was disposed. ## Implementation details This PR: - Adds the initial worker and its Playground handle to the list of items to be disposed when Playground CLI is disposed. - Prevents multiple workers from being requested when not running the `server` command. ## Testing Instructions (or ideally a Blueprint) - CI
1 parent 4f98f16 commit 42e2ece

File tree

4 files changed

+88
-42
lines changed

4 files changed

+88
-42
lines changed

packages/playground/cli/src/run-cli.ts

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
333333
}
334334

335335
if (args['experimental-multi-worker'] !== undefined) {
336+
const cliCommand = args._[0] as string;
337+
if (cliCommand !== 'server') {
338+
throw new Error(
339+
'The --experimental-multi-worker flag is only supported when running the server command.'
340+
);
341+
}
336342
if (args['experimental-multi-worker'] <= 1) {
337343
throw new Error(
338344
'The --experimental-multi-worker flag must be a positive integer greater than 1.'
@@ -549,10 +555,10 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
549555
let loadBalancer: LoadBalancer;
550556
let playground: RemoteAPI<PlaygroundCliWorker>;
551557

552-
const playgroundsToCleanUp: {
553-
playground: RemoteAPI<PlaygroundCliWorker>;
554-
worker: Worker;
555-
}[] = [];
558+
const playgroundsToCleanUp: Map<
559+
Worker,
560+
RemoteAPI<PlaygroundCliWorker>
561+
> = new Map();
556562

557563
/**
558564
* Expand auto-mounts to include the necessary mounts and steps
@@ -624,12 +630,19 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
624630
const serverUrl = `http://${host}:${port}`;
625631
const siteUrl = args['site-url'] || serverUrl;
626632

627-
// Create the blueprints handler
628-
const targetWorkerCount = args.experimentalMultiWorker ?? 1;
629-
// Account for the initial worker which is discarded after setup.
630-
const totalWorkerCountIncludingSetupWorker = targetWorkerCount + 1;
633+
const targetWorkerCount =
634+
args.command === 'server'
635+
? args.experimentalMultiWorker ?? 1
636+
: 1;
637+
const totalWorkersToSpawn =
638+
args.command === 'server'
639+
? // Account for the initial worker
640+
// which is discarded by the server after setup.
641+
targetWorkerCount + 1
642+
: targetWorkerCount;
643+
631644
const processIdSpaceLength = Math.floor(
632-
Number.MAX_SAFE_INTEGER / totalWorkerCountIncludingSetupWorker
645+
Number.MAX_SAFE_INTEGER / totalWorkersToSpawn
633646
);
634647

635648
/*
@@ -875,10 +888,12 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
875888

876889
disposing = true;
877890
await Promise.all(
878-
playgroundsToCleanUp.map(async ({ playground, worker }) => {
879-
await playground.dispose();
880-
await worker.terminate();
881-
})
891+
[...playgroundsToCleanUp].map(
892+
async ([worker, playground]) => {
893+
await playground.dispose();
894+
await worker.terminate();
895+
}
896+
)
882897
);
883898
if (server) {
884899
await new Promise((resolve) => server.close(resolve));
@@ -889,7 +904,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
889904
// Kick off worker threads now to save time later.
890905
// There is no need to wait for other async processes to complete.
891906
const promisedWorkers = spawnWorkerThreads(
892-
totalWorkerCountIncludingSetupWorker,
907+
totalWorkersToSpawn,
893908
handler.getWorkerType(),
894909
({ exitCode, workerIndex }) => {
895910
// We are already disposing, so worker exit is expected
@@ -929,6 +944,10 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
929944
fileLockManagerPort,
930945
nativeInternalDirPath
931946
);
947+
playgroundsToCleanUp.set(
948+
initialWorker.worker,
949+
initialPlayground
950+
);
932951

933952
await initialPlayground.isReady();
934953
wordPressReady = true;
@@ -968,16 +987,16 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
968987
// be configured differently than post-boot workers.
969988
// For example, we do not enable Xdebug by default for the initial worker.
970989
await loadBalancer.removeWorker(initialPlayground);
971-
// TODO: Wrap in a cleanup function and reuse for all worker cleanup.
972990
await initialPlayground.dispose();
973991
await initialWorker.worker.terminate();
992+
playgroundsToCleanUp.delete(initialWorker.worker);
974993
}
975994

976995
logger.log(`Preparing workers...`);
977996

978997
// Boot additional workers using the handler
979998
const initialWorkerProcessIdSpace = processIdSpaceLength;
980-
// Just take the first Playground instance to be relayed to others.
999+
// Just take the first Playground instance to be returned to the caller.
9811000
[playground] = await Promise.all(
9821001
workers.map(async (worker, index) => {
9831002
const firstProcessId =
@@ -996,11 +1015,10 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
9961015
nativeInternalDirPath,
9971016
});
9981017

999-
playgroundsToCleanUp.push({
1000-
playground: additionalPlayground,
1001-
worker: worker.worker,
1002-
});
1003-
1018+
playgroundsToCleanUp.set(
1019+
worker.worker,
1020+
additionalPlayground
1021+
);
10041022
loadBalancer.addWorker(additionalPlayground);
10051023

10061024
return additionalPlayground;
@@ -1035,6 +1053,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
10351053
if (await playground?.fileExists(errorLogPath)) {
10361054
phpLogs = await playground.readFileAsText(errorLogPath);
10371055
}
1056+
await disposeCLI();
10381057
throw new Error(phpLogs, { cause: error });
10391058
}
10401059
},

packages/playground/cli/tests/test-running-unbuilt-cli.sh

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,13 @@ function test_playground_cli() {
4747
fi
4848
}
4949

50-
function test_playground_cli_multi_worker() {
51-
MULTIWORKER_WP_PATH="$HOME/playground-cli-multi-worker-wp"
52-
mkdir -p "$MULTIWORKER_WP_PATH"
53-
54-
# TODO: Also test with asyncify once we multiple workers there.
55-
test_playground_cli unbuilt-jspi \
56-
--mountBeforeInstall="$MULTIWORKER_WP_PATH:/wordpress" \
57-
--experimentalMultiWorker
58-
}
59-
6050
echo
6151
test_playground_cli unbuilt-asyncify
6252
echo
6353
test_playground_cli unbuilt-jspi
6454
echo
6555

66-
67-
test_playground_cli_multi_worker
56+
test_playground_cli unbuilt-asyncify --experimental-multi-worker
57+
echo
58+
test_playground_cli unbuilt-jspi --experimental-multi-worker
6859
echo
69-
echo 'Retesting multi-worker to test with a pre-existing WordPress installation where we have seen bugs.'
70-
test_playground_cli_multi_worker

packages/playground/test-built-npm-packages/es-modules-and-vitest/run-tests.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function red(text: string) {
2121
type Result = {
2222
phpVersion: string;
2323
code: number | null;
24+
timeout?: boolean;
2425
};
2526

2627
const results: Result[] = [];
@@ -47,7 +48,7 @@ for (const phpVersion of SupportedPHPVersions.filter(
4748
}
4849
);
4950

50-
await new Promise<void>((resolve) => {
51+
const promiseToClose = new Promise<void>((resolve) => {
5152
child.on('close', (code) => {
5253
results.push({
5354
phpVersion,
@@ -56,25 +57,49 @@ for (const phpVersion of SupportedPHPVersions.filter(
5657
resolve();
5758
});
5859
});
60+
const promiseToTimeout = new Promise<void>((resolve, reject) => {
61+
setTimeout(() => {
62+
console.error(`PHP ${phpVersion}: timed out.`);
63+
reject(new Error('Test timed out'));
64+
}, 30000);
65+
});
66+
try {
67+
await Promise.race([promiseToClose, promiseToTimeout]);
68+
} catch (e) {
69+
results.push({
70+
phpVersion,
71+
code: null,
72+
timeout: true,
73+
});
74+
child.kill('SIGKILL');
75+
}
5976
}
6077

6178
console.log('Results:');
6279
for (const result of results) {
63-
console.log(
64-
`PHP ${result.phpVersion}: ${
65-
result.code === 0 ? green('PASS') : red('FAIL')
66-
} with exit code ${result.code}`
67-
);
80+
if (result.timeout) {
81+
console.log(red(`PHP ${result.phpVersion}: ${red('timed out')}.`));
82+
} else {
83+
console.log(
84+
`PHP ${result.phpVersion}: ${
85+
result.code === 0 ? green('PASS') : red('FAIL')
86+
} with exit code ${result.code}`
87+
);
88+
}
6889
}
6990

7091
const numPassed = results.filter((r) => r.code === 0).length;
71-
const numFailed = results.filter((r) => r.code !== 0).length;
92+
const numFailed = results.filter((r) => r.code !== 0 && !r.timeout).length;
93+
const numTimedOut = results.filter((r) => r.timeout).length;
7294
if (numPassed > 0) {
7395
console.log(green(`${numPassed} / ${results.length} tests passed`));
7496
}
7597
if (numFailed > 0) {
7698
console.log(red(`${numFailed} / ${results.length} tests failed`));
7799
}
100+
if (numTimedOut > 0) {
101+
console.log(red(`${numTimedOut} / ${results.length} tests timed out`));
102+
}
78103

79104
if (numFailed > 0) {
80105
process.exit(1);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2019",
4+
"module": "ES2022",
5+
"moduleResolution": "node",
6+
"esModuleInterop": true,
7+
"forceConsistentCasingInFileNames": true,
8+
"strict": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["**/*.ts"],
12+
"exclude": ["node_modules"]
13+
}

0 commit comments

Comments
 (0)