Skip to content

Commit c8a0e3a

Browse files
authored
feat(node, deno, bun): graceful shutdown (#136)
1 parent e0124ef commit c8a0e3a

File tree

5 files changed

+79
-1
lines changed

5 files changed

+79
-1
lines changed

src/_plugins.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Colors } from "./_utils.cli.ts";
12
import type { ServerPlugin } from "./types.ts";
23

34
export const errorPlugin: ServerPlugin = (server) => {
@@ -14,3 +15,61 @@ export const errorPlugin: ServerPlugin = (server) => {
1415
}
1516
});
1617
};
18+
19+
export const gracefulShutdownPlugin: ServerPlugin = (server) => {
20+
const config = server.options?.gracefulShutdown;
21+
if (
22+
!globalThis.process?.on ||
23+
config === false ||
24+
(config === undefined && (process.env.CI || process.env.TEST))
25+
) {
26+
return;
27+
}
28+
const gracefulShutdown =
29+
config === true || !config?.gracefulTimeout ? 3 : config.gracefulTimeout;
30+
const forceShutdown =
31+
config === true || !config?.forceTimeout ? 3 : config.forceTimeout;
32+
let isShuttingDown = false;
33+
const shutdown = async () => {
34+
if (isShuttingDown) return;
35+
isShuttingDown = true;
36+
console.log(
37+
Colors.gray(
38+
`\nShutting down server... (timeout in ${gracefulShutdown}+${forceShutdown}s)`,
39+
),
40+
);
41+
let timeout: any;
42+
await Promise.race([
43+
// Graceful shutdown
44+
server.close().finally(() => {
45+
clearTimeout(timeout);
46+
console.log(Colors.green("Server closed all connections."));
47+
}),
48+
new Promise<void>((resolve) => {
49+
timeout = setTimeout(() => {
50+
// Graceful shutdown timeout
51+
console.warn(
52+
Colors.yellow(
53+
`Forcing closing connections to exit... (timeout in ${forceShutdown}s)`,
54+
),
55+
);
56+
timeout = setTimeout(() => {
57+
// Force shutdown timeout
58+
console.error(
59+
Colors.red("Could not close connections in time, force exiting."),
60+
);
61+
resolve();
62+
}, 1000);
63+
return server.close(true).finally(() => {
64+
clearTimeout(timeout);
65+
resolve();
66+
});
67+
}, 1000);
68+
}),
69+
]);
70+
globalThis.process.exit(0);
71+
};
72+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
73+
globalThis.process.on(sig, shutdown);
74+
}
75+
};

src/adapters/bun.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createWaitUntil,
99
} from "../_utils.ts";
1010
import { wrapFetch } from "../_middleware.ts";
11+
import { gracefulShutdownPlugin } from "../_plugins.ts";
1112

1213
export { FastURL } from "../_url.ts";
1314
export const FastResponse: typeof globalThis.Response = Response;
@@ -32,6 +33,8 @@ class BunServer implements Server<BunFetchHandler> {
3233

3334
for (const plugin of options.plugins || []) plugin(this);
3435

36+
gracefulShutdownPlugin(this);
37+
3538
const fetchHandler = wrapFetch(this);
3639

3740
this.#wait = createWaitUntil();

src/adapters/deno.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
resolveTLSOptions,
88
} from "../_utils.ts";
99
import { wrapFetch } from "../_middleware.ts";
10+
import { gracefulShutdownPlugin } from "../_plugins.ts";
1011

1112
export { FastURL } from "../_url.ts";
1213
export const FastResponse: typeof globalThis.Response = Response;
@@ -36,6 +37,8 @@ class DenoServer implements Server<DenoFetchHandler> {
3637

3738
for (const plugin of options.plugins || []) plugin(this);
3839

40+
gracefulShutdownPlugin(this);
41+
3942
const fetchHandler = wrapFetch(this);
4043

4144
this.#wait = createWaitUntil();

src/adapters/node.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
createWaitUntil,
99
} from "../_utils.ts";
1010
import { wrapFetch } from "../_middleware.ts";
11-
import { errorPlugin } from "../_plugins.ts";
11+
import { errorPlugin, gracefulShutdownPlugin } from "../_plugins.ts";
1212

1313
import nodeHTTP from "node:http";
1414
import nodeHTTPS from "node:https";
@@ -58,6 +58,8 @@ class NodeServer implements Server {
5858
for (const plugin of options.plugins || []) plugin(this);
5959
errorPlugin(this);
6060

61+
gracefulShutdownPlugin(this);
62+
6163
const fetchHandler = (this.fetch = wrapFetch(this));
6264

6365
this.#wait = createWaitUntil();

src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ export interface ServerOptions {
114114
*/
115115
silent?: boolean;
116116

117+
/**
118+
* Graceful shutdown on SIGINT and SIGTERM signals.
119+
*
120+
* Supported for Node.js, Deno and Bun runtimes.
121+
*
122+
* @default true (disabled in test and ci environments)
123+
*/
124+
gracefulShutdown?:
125+
| boolean
126+
| { gracefulTimeout?: number; forceTimeout?: number };
127+
117128
/**
118129
* TLS server options.
119130
*/

0 commit comments

Comments
 (0)