-
-
Notifications
You must be signed in to change notification settings - Fork 779
fix: call runtime close hook on shutdown signals
#4016
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { useNitroApp } from "../app.ts"; | ||
|
|
||
| export function setupShutdownHooks() { | ||
| const handler = () => { | ||
| useNitroApp().hooks?.callHook("close"); | ||
| }; | ||
| for (const sig of ["SIGTERM", "SIGINT"] as const) { | ||
| process.on(sig, handler); | ||
| } | ||
|
Comment on lines
+3
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: handler does not Two interrelated problems:
Additionally, there's no guard against double invocation β a rapid Ctrl+C twice, or SIGTERM followed by SIGINT, will call the close hook multiple times, which may cause errors in plugins that tear down resources once. Proposed fix export function setupShutdownHooks() {
+ let shutdownPromise: Promise<void> | undefined;
const handler = () => {
- useNitroApp().hooks?.callHook("close");
+ if (shutdownPromise) {
+ return;
+ }
+ shutdownPromise = (useNitroApp().hooks?.callHook("close") ?? Promise.resolve())
+ .finally(() => {
+ process.exit(0);
+ });
};
for (const sig of ["SIGTERM", "SIGINT"] as const) {
process.on(sig, handler);
}
}π€ Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| const callHook = vi.fn(); | ||
|
|
||
| vi.mock("../../src/runtime/internal/app.ts", () => ({ | ||
| useNitroApp: () => ({ | ||
| hooks: { callHook }, | ||
| }), | ||
| })); | ||
|
|
||
| import { setupShutdownHooks } from "../../src/runtime/internal/shutdown.ts"; | ||
|
|
||
| describe("setupShutdownHooks", () => { | ||
| let savedSIGTERM: Function[]; | ||
| let savedSIGINT: Function[]; | ||
|
|
||
| beforeEach(() => { | ||
| savedSIGTERM = process.listeners("SIGTERM").slice(); | ||
| savedSIGINT = process.listeners("SIGINT").slice(); | ||
| callHook.mockClear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.removeAllListeners("SIGTERM"); | ||
| process.removeAllListeners("SIGINT"); | ||
| for (const fn of savedSIGTERM) process.on("SIGTERM", fn as NodeJS.SignalsListener); | ||
| for (const fn of savedSIGINT) process.on("SIGINT", fn as NodeJS.SignalsListener); | ||
| }); | ||
|
|
||
| it("registers SIGTERM and SIGINT handlers", () => { | ||
| const beforeTERM = process.listenerCount("SIGTERM"); | ||
| const beforeINT = process.listenerCount("SIGINT"); | ||
| setupShutdownHooks(); | ||
| expect(process.listenerCount("SIGTERM")).toBe(beforeTERM + 1); | ||
| expect(process.listenerCount("SIGINT")).toBe(beforeINT + 1); | ||
| }); | ||
|
|
||
| it("calls close hook on SIGTERM", () => { | ||
| setupShutdownHooks(); | ||
| process.emit("SIGTERM", "SIGTERM"); | ||
| expect(callHook).toHaveBeenCalledWith("close"); | ||
| }); | ||
|
|
||
| it("calls close hook on SIGINT", () => { | ||
| setupShutdownHooks(); | ||
| process.emit("SIGINT", "SIGINT"); | ||
| expect(callHook).toHaveBeenCalledWith("close"); | ||
| }); | ||
|
Comment on lines
+38
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Looking at the implementation in const handler = () => {
useNitroApp().hooks?.callHook("close");
};
The test should use an async mock to surface this: Suggested test improvement (after fixing the implementation to await) it("calls close hook on SIGTERM", async () => {
+ callHook.mockResolvedValueOnce(undefined);
setupShutdownHooks();
- process.emit("SIGTERM", "SIGTERM");
+ await process.emit("SIGTERM", "SIGTERM");
expect(callHook).toHaveBeenCalledWith("close");
});And in the implementation: - const handler = () => {
- useNitroApp().hooks?.callHook("close");
+ const handler = async () => {
+ await useNitroApp().hooks?.callHook("close");
};π€ Prompt for AI Agents |
||
| }); | ||
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.
π§© Analysis chain
π Script executed:
Repository: nitrojs/nitro
Length of output: 39
π Script executed:
Repository: nitrojs/nitro
Length of output: 1193
π Script executed:
Repository: nitrojs/nitro
Length of output: 1161
π Script executed:
Repository: nitrojs/nitro
Length of output: 372
Remove
NITRO_SHUTDOWN_DISABLEDfrom documentation or implement the feature in code.The environment variable
NITRO_SHUTDOWN_DISABLEDis documented on line 36 but does not exist in the implementation. ThesetupShutdownHooks()function insrc/runtime/internal/shutdown.tsunconditionally registers signal handlers with no check for this env var. Users will be misled by the documentation. Either implement the env var check insetupShutdownHooks()to conditionally skip shutdown hook registration, or remove this env var from the docs.π€ Prompt for AI Agents