-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
feat(webserver): customize shutdown with new kill
option
#33379
base: main
Are you sure you want to change the base?
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
kill
option
This comment has been minimized.
This comment has been minimized.
@@ -619,6 +619,7 @@ export default defineConfig({ | |||
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. | |||
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. | |||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. | |||
- `kill` ?<{ SIGINT: number } | { SIGTERM: number }> How to shut down the process gracefully. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ SIGINT: 500 }`, the top process is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. |
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.
I am afraid this TS notation will not be parsed. Instead, make two separate properties below, like any location
or position
options we have.
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 types.d.ts
generator understands it, didn't think about that the docs site also needs to parse this, though. updated it.
tests/config/commonFixtures.ts
Outdated
return line.substring(options.prefix.length); | ||
return line; | ||
}) | ||
.filter(line => line.startsWith('%%')) |
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.
Hmm... I'd think prefix
would replace %%
in this function, but turns out it does not. I am worried this version will be hard to use in the future.
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.
agree. i've replaced this with a test-local implementation, outputLines()
isn't even available on the result of runInlineTest
.
const testProcess = await interactWithTestRunner(files(), { workers: 1 }); | ||
|
||
await testProcess.waitForOutput('webserver started'); | ||
process.kill(testProcess.process.pid!, 'SIGINT'); |
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.
I wonder why do you have to interact with the test runner? Shouldn't it send SIGINT/SIGTERM/SIGKILL during the normal testing operation without user interrupt? If so, I'd think that's more important to test.
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.
not sure why I did this. replaced it with bog-standard runInlineTest
await processExit; | ||
} else { | ||
await Promise.race([ | ||
processExit, |
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 process exits before the timer runs out, shouldn't we clear the timer? If we don't, it would probably block Node.js from exiting for the full duration.
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.
good catch, yeah! I've refactored this a tad bit to make it easier on the eye. Noticed that node:timers
has some useful tools around this though, particular ref
and signal
: https://nodejs.org/api/timers.html#timerspromisessettimeoutdelay-value-options
Might be handy somewhere else.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
addressed the feedback
tests/config/commonFixtures.ts
Outdated
return line.substring(options.prefix.length); | ||
return line; | ||
}) | ||
.filter(line => line.startsWith('%%')) |
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.
agree. i've replaced this with a test-local implementation, outputLines()
isn't even available on the result of runInlineTest
.
const testProcess = await interactWithTestRunner(files(), { workers: 1 }); | ||
|
||
await testProcess.waitForOutput('webserver started'); | ||
process.kill(testProcess.process.pid!, 'SIGINT'); |
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.
not sure why I did this. replaced it with bog-standard runInlineTest
await processExit; | ||
} else { | ||
await Promise.race([ | ||
processExit, |
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.
good catch, yeah! I've refactored this a tad bit to make it easier on the eye. Noticed that node:timers
has some useful tools around this though, particular ref
and signal
: https://nodejs.org/api/timers.html#timerspromisessettimeoutdelay-value-options
Might be handy somewhere else.
const timer = timeout !== 0 | ||
? setTimeout(() => { | ||
// @ts-expect-error. SIGINT didn't kill the process, but `processLauncher` will only attempt killing it if this is false | ||
launchedProcess.killed = false; |
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.
Seems pretty hacky to override the process killed property - maybe we can refactor it to avoid that?
Like we don't know what other implications it might end up with.
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.
Yeah agree. It might be fine today, but who knows how node:child_process
changes. I've refactored it in 220ed29, how do you like that?
Co-authored-by: Dmitry Gozman <[email protected]> Signed-off-by: Simon Knott <[email protected]>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
It appears that the tests fail on linux because the webserver process isn't properly cleaned up there, and a following test has a port clash. I'm checking what we can do. |
This comment has been minimized.
This comment has been minimized.
Uh-oh. It appears that because we set |
Alright, the bug was in listening for We still need to send the signal to the entire process group now, which feels off. The alternative would be to disable |
@@ -226,7 +226,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun | |||
killSet.delete(killProcessAndCleanup); | |||
removeProcessHandlersIfNeeded(); | |||
options.log(`[pid=${spawnedProcess.pid}] <kill>`); | |||
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) { |
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.
.killed
means that the process was sent a signal before, not that it was killed. processClosed
is better for that, especially now that it's set by the close
event and not the exit
event. exit
is fired whenever the process receives a signal, close
only after the process exited. some confusing terminology there.
This comment has been minimized.
This comment has been minimized.
Test results for "tests 1"3 failed 1 flaky36865 passed, 682 skipped Merge workflow run. |
Closes #33377, #18209
There's quite some backstory on this that we should keep in mind:
We tried this in the past, then reverted because we wanted to have it spawn a child process instead of a separate process group PR, which we also reverted because it changed from SIGKILL to SIGTERM and that broke Svelte.
This PR proposes a
kill
option that allows users customizing this shutdown behaviour.This PR proposes to keep things mostly as they are. The addition is to try sending a
SIGINT
first, and if it didn't exit after half a second (configurable withshutdownTimeout
setting), continue as usual with aSIGKILL
.Putting 500ms as a default timeout is based on two assumptions about common
webserver
usage:docker compose
setups, which we can expect to understandSIGINT
Users that fulfil both assumptions will now get proper graceful shutdown. User that don't fulfil the second one won't see a benefit until they configure
shutdownTimeout
. Most other users will see a slowdown of 500ms, but no other breaking changes. Only folks that have a weird implementation ofSIGINT
will see a breaking change, which should be fine.Let me know if you think we should use
SIGTERM
instead ofSIGINT
. I'm unsure which one is more idiomatic, since Playwright is a user-controlled tool, but we also initiate the signal programatically.We could also solve some of the
docker compose
cleanup issues by killing only the top process, and not the entire process group (see here). This would help this specificdocker compose
thing, but notdocker run --rm
or any other processes that depend onSIGINT
for cleanup.