Skip to content

Commit 54e7074

Browse files
committed
refactor: improve graceful shutdown behavior
1 parent e57ada8 commit 54e7074

File tree

2 files changed

+42
-43
lines changed

2 files changed

+42
-43
lines changed

src/_plugins.ts

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,56 +23,53 @@ export const gracefulShutdownPlugin: ServerPlugin = (server) => {
2323
) {
2424
return;
2525
}
26-
const gracefulShutdown =
26+
const gracefulTimeout =
2727
config === true || !config?.gracefulTimeout
28-
? Number.parseInt(process.env.SERVER_SHUTDOWN_TIMEOUT || "") || 3
28+
? Number.parseInt(process.env.SERVER_SHUTDOWN_TIMEOUT || "") || 5
2929
: config.gracefulTimeout;
30-
const forceShutdown =
31-
config === true || !config?.forceTimeout
32-
? Number.parseInt(process.env.SERVER_FORCE_SHUTDOWN_TIMEOUT || "") || 5
33-
: config.forceTimeout;
30+
3431
let isShuttingDown = false;
35-
let forceClose: (() => void) | undefined;
32+
33+
const w = server.options.silent ? () => {} : process.stderr.write.bind(process.stderr);
34+
35+
const forceClose = async () => {
36+
w(c.red("\x1b[2K\rForcibly closing connections...\n"));
37+
await server.close(true);
38+
w(c.yellow("Server closed.\n"));
39+
globalThis.process.exit(0);
40+
};
41+
3642
const shutdown = async () => {
43+
// Second call: force close immediately
3744
if (isShuttingDown) {
38-
forceClose?.();
39-
return;
45+
return forceClose();
4046
}
47+
4148
isShuttingDown = true;
42-
const w = process.stderr.write.bind(process.stderr);
43-
w(
44-
c.gray(
45-
`\nShutting down server in ${gracefulShutdown}s... (press Ctrl+C again to force close)`,
46-
),
47-
);
48-
let timeout: any;
49-
await Promise.race([
50-
// Graceful shutdown
51-
server.close().finally(() => {
52-
clearTimeout(timeout);
53-
w(c.gray(" Server closed.\n"));
54-
}),
55-
new Promise<void>((resolve) => {
56-
forceClose = () => {
57-
clearTimeout(timeout);
58-
w(c.gray("\nForce closing...\n"));
59-
server.close(true);
60-
resolve();
61-
};
62-
timeout = setTimeout(() => {
63-
// Graceful shutdown timeout
64-
w(c.gray(`\nForce closing connections in ${forceShutdown}s...`));
65-
timeout = setTimeout(() => {
66-
// Force shutdown timeout
67-
w(c.red("\nCould not close connections in time, force exiting."));
68-
resolve();
69-
}, forceShutdown * 1000);
70-
return server.close(true);
71-
}, gracefulShutdown * 1000);
72-
}),
73-
]);
74-
globalThis.process.exit(0);
49+
const closePromise = server.close();
50+
51+
// Countdown with updates each second
52+
for (let remaining = gracefulTimeout; remaining > 0; remaining--) {
53+
w(
54+
c.gray(
55+
`\rStopping server gracefully (${remaining}s)... Press ${c.bold("Ctrl+C")} again to force close.`,
56+
),
57+
);
58+
const closed = await Promise.race([
59+
closePromise.then(() => true),
60+
new Promise<false>((r) => setTimeout(() => r(false), 1000)),
61+
]);
62+
if (closed) {
63+
w("\x1b[2K\r" + c.green("Server closed successfully.\n"));
64+
globalThis.process.exit(0);
65+
}
66+
}
67+
68+
// Graceful period expired: force close
69+
w("\x1b[2K\rGraceful shutdown timed out.\n");
70+
await forceClose();
7571
};
72+
7673
for (const sig of ["SIGINT", "SIGTERM"] as const) {
7774
globalThis.process.on(sig, shutdown);
7875
}

src/cli/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ export async function main(mainOpts: MainOptions): Promise<void> {
101101
}
102102
};
103103
process.on("exit", () => cleanup("SIGTERM"));
104-
process.on("SIGINT" /* ctrl+c */, () => cleanup("SIGINT", 130));
105104
process.on("SIGTERM", () => cleanup("SIGTERM", 143));
105+
if (!cliOpts.prod) {
106+
process.on("SIGINT" /* ctrl+c */, () => cleanup("SIGINT", 130));
107+
}
106108
}
107109

108110
function parseArgs(args: string[]): CLIOptions {

0 commit comments

Comments
 (0)