Proper graceful shutdown without dropping requests #3756
realfresh
started this conversation in
Show and tell
Replies: 2 comments
-
|
interesting |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
Thanks, helped a bunch :) Here's my refactored version more for pm2 / local It uses a middleware, usage is like this: import { setServer, gracefulShutdownMiddleware } from "./middlewars/graceful-shutdown.ts"
app.use(gracefulShutdownMiddleware);
// other middlewares and routes
setServer(serve(...));and the code: import { type Context } from "hono";
import { EventEmitter } from "node:events";
import { type IncomingMessage } from "node:http";
import { type Socket } from "node:net";
import { createMiddleware } from "hono/factory";
import { connection } from "../models/databases.ts";
import type { ServerType } from "@hono/node-server";
//~ Utility Classes
const debug = (msg: string) => {
if (state.shutdown) {
console.debug(msg);
}
};
class ConnectionTracker {
entries = new Set<Socket>();
track = (req: IncomingMessage) => {
const conn = req.socket;
if (this.entries.has(conn)) {
return;
}
this.entries.add(conn);
debug(`++conn [${this.entries.size}]`);
conn.on("close", () => {
this.entries.delete(conn);
debug(`--conn [${this.entries.size}]`);
});
};
destroy = () => {
if (this.entries.size > 0) {
console.info(`-> destroying sockets: (${this.entries.size})`);
}
this.entries.forEach((conn) => conn.destroy());
};
}
class RequestTracker extends EventEmitter {
active = 0;
paths = new Map<string, number>();
increment = (c: Context) => {
this.active++;
this.paths.set(c.req.path, (this.paths.get(c.req.path) ?? 0) + 1);
debug(`++req (${this.active})`);
};
decrement = (c: Context) => {
this.active--;
const val = this.paths.get(c.req.path);
if (!val) {
throw new Error(`Request path ${c.req.path} not found in paths`);
}
this.paths.set(c.req.path, val - 1);
if (val === 1) {
this.paths.delete(c.req.path);
}
debug(`--req (${this.active})`);
if (this.active === 0) {
this.emit("complete");
}
};
/** Wait for all ongoing requests to finish */
onComplete = async () => {
if (this.active > 0) {
await Promise.race([
// The event allows for a fast exit if all requests are completed,
// but the interval is a backup mechanism to avoid race conditions
this.onCompleteEvent(),
this.onCompleteInterval(),
]);
}
console.info(`-> outstanding requests completed`);
};
private onCompleteEvent = () => {
return new Promise((resolve) => this.once("complete", resolve));
};
private onCompleteInterval = async () => {
while (this.active > 0) {
this.logPaths();
console.info(`-> requests remaining (${this.active})`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
};
private logPaths = () => {
console.info("-> paths in-flight", { paths: this.paths });
};
}
const state = { shutdown: false };
const requests = new RequestTracker();
const connections = new ConnectionTracker();
let server: ServerType | null = null;
export function setServer(serverArg: ServerType) {
server = serverArg;
}
export const gracefulShutdownMiddleware = createMiddleware(async (c, next) => {
const { incoming } = c.env;
connections.track(incoming);
requests.increment(c);
// On Shutdown:
if (state.shutdown) {
c.header("connection", "close");
}
// Process the HTTP request:
// This apparently doesn't throw any errors
// so omit the try/catch to avoid performance overhead
await next();
// On Shutdown
if (state.shutdown) {
// 1. Close keep-alive connections
// Setting this header informs keep-alive connections to disconnect after response
// c.res.headers.set("connection", "close");
c.header("connection", "close");
// 2. Destroy the socket
// Use `destroySoon()` which is more graceful than `destroy()` by
// allowing the response to be sent first before closing the socket
c.env.incoming.socket.destroySoon();
}
requests.decrement(c);
});
const handleShutdown = async () => {
state.shutdown = true;
console.info("SIGINT <- start shutdown");
await requests.onComplete();
console.info("SIGINT <- destroy remaining sockets");
connections.destroy();
console.info("SIGINT <- close database connection and http server");
if (!server) {
console.error("SIGINT <- server not set, cannot close http server");
}
await Promise.all([server ? server.close() : Promise.resolve(undefined), connection.close()]);
console.info("SIGINT <- should exit gracefully without further action");
const timeout = setTimeout(() => {
console.info("SIGINT <- timeout, force exiting, not everything was cleaned up");
process.exit(1);
}, 1_000);
timeout.unref();
};
// User manually Ctrl+C, or pm2
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on("SIGINT", async () => {
if (state.shutdown) {
return;
}
await handleShutdown();
});
// node --watch reloading
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on("SIGTERM", async () => {
if (state.shutdown) {
console.error("SIGTERM <- already shutting down, exiting");
process.exit(1);
}
await handleShutdown();
}); |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I've finally done it, graceful shutdown that ensures all in-flight requests are completed, connections closed properly, then finally exiting.
This is specific to Kubernetes, and the code below is taken from our codebase, some the imports may not make sense. But I'm sure you will be able to adapt it to your project correctly.
Beta Was this translation helpful? Give feedback.
All reactions