-
Notifications
You must be signed in to change notification settings - Fork 54
Re-enable WP-CLI execution in the child process #2261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Re-enable WP-CLI execution in the child process #2261
Conversation
fredrikekelund
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing your solution in this PR, I realized that we already have a sequential helper function in common/lib/sequential.ts that can accomplish this a little more succinctly. If we apply the following diff, implement my inline suggestion and restore the function body of runWpCliCommand, I believe we should be good.
diff --git a/common/lib/sequential.ts b/common/lib/sequential.ts
index 593ee246..d7698147 100644
--- a/common/lib/sequential.ts
+++ b/common/lib/sequential.ts
@@ -2,6 +2,7 @@ const sequentialLocks = new Map< () => Promise< unknown >, Set< Promise< unknown
// Ensures that calls to the provided function are executed sequentially
export function sequential< Args extends unknown[], Return >(
+ concurrentCount: number,
fn: ( ...args: Args ) => Promise< Return >
) {
return async ( ...args: Args ) => {
@@ -10,7 +11,7 @@ export function sequential< Args extends unknown[], Return >(
sequentialLocks.set( fn, locks );
}
- while ( locks.size ) {
+ while ( locks.size >= concurrentCount ) {
await Promise.allSettled( [ ...locks ] );
}
Let me know your thoughts, @bcotrim
| async function runWpCliCommand( | ||
| args: string[] | ||
| ): Promise< { stdout: string; stderr: string; exitCode: number } > { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const runWpCliCommand = sequential( 3, async ( args: string[] ): Promise< WpCliResult > => {e3fe64e to
b536fec
Compare
2881990 to
b536fec
Compare
40c541c to
bf9c008
Compare
fredrikekelund
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Queue rejects commands when full" test case didn't work as expected for me (with or without my proposed changes to common/lib/sequential.ts). This definitely fixes the core issue, though. I've confirmed that I can run WP-CLI commands from the Assistant without any issues.
Great work identifying the messageId issue, too 👍
cli/lib/wordpress-server-manager.ts
Outdated
| /** | ||
| * Generate a unique message ID that won't collide across concurrent CLI processes. | ||
| */ | ||
| function generateMessageId(): number { | ||
| return Date.now() * 1000 + Math.floor( Math.random() * 1000 ); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh man, good catch 👍 This is definitely part of the reason I was seeing jumbled output when running multiple WP-CLI commands in quick succession.
The current implementation is fine, but I guess we could also use crypto.randomUUID(). Any thoughts, @bcotrim?
cli/wordpress-server-child.ts
Outdated
| /** | ||
| * Result type for WP-CLI command execution | ||
| */ | ||
| type WpCliResult = { stdout: string; stderr: string; exitCode: number }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no big deal, but we could easily keep this inlined in the return type for runWpCliCommand, IMO. Since we're not reusing it, I mean.
common/lib/sequential.ts
Outdated
| if ( locks.size >= concurrentCount ) { | ||
| if ( maxQueueSize !== undefined && queueCount >= maxQueueSize ) { | ||
| throw new Error( | ||
| `Queue is full (${ maxQueueSize } pending commands). Please try again later.` | ||
| ); | ||
| } | ||
|
|
||
| while ( locks.size ) { | ||
| await Promise.allSettled( [ ...locks ] ); | ||
| queueCount++; | ||
| try { | ||
| while ( locks.size >= concurrentCount ) { | ||
| await Promise.allSettled( [ ...locks ] ); | ||
| } | ||
| } finally { | ||
| queueCount--; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if ( maxQueueSize !== undefined && queueCount >= maxQueueSize ) {
throw new Error(
`Queue is full (${ maxQueueSize } pending commands). Please try again later.`
);
}
while ( locks.size >= concurrentCount ) {
queueCount++;
await Promise.allSettled( [ ...locks ] );
queueCount--;
}This is my suggestion ⬆️
A couple of comments:
- The initial
if ( locks.size >= concurrentCount )doesn't really make sense to me. We should check the queue count before checking the lock count. - There's no need to wrap
Promise.allSettledin atry..catch, since it never rejects (unlikePromise.all). - By moving the
queueCountincrement and decrement operations inside thewhileloop, we ensure that thequeueCountvariable isn't touched if the lock count doesn't exceedconcurrentCount. This doesn't really matter, though, since we don't run any asynchronous operations if this is case. Consider this an optional nit that maybe just looks cleaner 🙂
common/lib/sequential.ts
Outdated
| locks.add( fnPromise ); | ||
|
|
||
| try { | ||
| locks.add( fnPromise ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably fine, but I guess the locks.add call was previously part of the try clause to clarify its relationship to the finally clause. I don't believe this change makes any functional difference, though, so feel free to implement it whichever way you prefer, @bcotrim
common/lib/sequential.ts
Outdated
| ) { | ||
| const concurrentCount = options?.concurrent ?? 1; | ||
| const maxQueueSize = options?.max; | ||
| const locks = new Set< Promise< unknown > >(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const locks = new Set< Promise< unknown > >(); | |
| const locks = new Set< Promise< Return > >(); |
Good idea moving locks inside the closure 👍 It definitely simplifies things. We might as well use the correct return type now, though.
❯ for i in {1..15}; do
node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1 &
done
wait
[2] 41596
[3] 41597
[4] 41598
[5] 41599
[6] 41600
[7] 41601
[8] 41602
[9] 41603
[10] 41604
[11] 41605
[12] 41606
[13] 41607
[14] 41608
[15] 41609
[16] 41610
✖ Failed to run WP-CLI command: Queue is full (10 pending commands). Please try again later.
✖ Failed to run WP-CLI command: Queue is full (10 pending commands). Please try again later.
[14] exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[2] exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[5] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[9] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[12] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[4] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[16] + done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[8] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[6] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[15] + done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[7] done node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
✖ Failed to run WP-CLI command: Timeout waiting for response to message 2d2c6a3a-9e83-4114-8bcb-21adbdc8cef9: No activity for 120s
✖ Failed to run WP-CLI command: Timeout waiting for response to message ab1da4ca-bb4e-4971-8f6e-87b0a85c9564: No activity for 120s
✖ Failed to run WP-CLI command: Timeout waiting for response to message 6b933194-c256-4118-bc08-8f4ec6708500: No activity for 120s
✖ Failed to run WP-CLI command: Timeout waiting for response to message 0c5c871d-dff6-4150-9ee9-fda246e3d71c: No activity for 120s
[11] - exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[3] exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[10] - exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1
[13] + exit 1 node dist/cli/main.js wp --path /tmp/test-site eval "sleep(30);" 2>&1Were you referencing the timeouts? I feel like that is expected, we could not timeout something while it is in queue, but it might be overthinking at this point. |
|
No, I meant that I didn't get any timeout errors. I didn't get any Playground errors about exceeding the maximum number of request handlers either, though. I say let's land this PR and revisit this later if needed 👍 |
7c11d5c to
52c93b1
Compare
|
Let's change the base branch to |
Related issues
Proposed Changes
Re-enable WP-CLI execution in child process (
cli/commands/wp.ts)IS_WP_CLI_CHILD_PROCESS_EXECUTION_ENABLED = trueAdd WP-CLI command queue (
cli/wordpress-server-child.ts)WpCliCommandQueueclass serializes concurrent WP-CLI commandsMAX_CONCURRENT_WP_CLI_COMMANDS = 3- limits concurrentplayground.cli()calls to preventMaxPhpInstancesErrorMAX_WP_CLI_QUEUE_SIZE = 10- prevents unbounded queue growth, rejects new commands when fullFix messageId collision in IPC (
cli/lib/wordpress-server-manager.ts)nextMessageId++withgenerateMessageId()using timestamp + randomTesting Instructions
Setup
npm run cli:build node dist/cli/main.js site create --path /tmp/test-site --name "Test" --skip-browserTest 1: Concurrent commands return correct results
Expected: Each returns different, correct values (e.g., "My WordPress Website", "http://localhost:XXXX", "[email protected]", "3")
Test 2: Site doesn't crash with concurrent commands
Expected: Site returns HTTP 200 both before and after (no crash)
Test 3: Failed commands don't crash the server
Expected: Command shows error message, site still returns HTTP 200
Test 4: Queue rejects commands when full
Expected: 13 commands succeed, 2 commands fail with "WP-CLI command queue is full (10 pending commands). Please try again later."
Cleanup
Pre-merge Checklist