Created
April 25, 2025 15:10
-
-
Save td0m/409a3b228dfe24f3135c7289c0b0382c to your computer and use it in GitHub Desktop.
NodeJS: Zero Downtime with `node:cluster`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import cluster from "node:cluster"; | |
| import { availableParallelism } from "node:os"; | |
| export async function clustered({ | |
| numWorkers = availableParallelism(), | |
| primary, | |
| worker, | |
| recoverWorkers = true, | |
| shutdownPrimary, | |
| }: { | |
| numWorkers?: number; | |
| primary?: () => Promise<void>; | |
| worker: () => Promise<void>; | |
| recoverWorkers?: boolean; | |
| shutdownPrimary?: () => Promise<void>; | |
| }) { | |
| if (cluster.isPrimary) { | |
| process.on("SIGHUP", () => { | |
| console.info("SIGHUP"); | |
| const workersAtSighup = Object.values(cluster.workers || {}).filter( | |
| (worker) => worker?.isConnected() | |
| ); | |
| for (const worker of workersAtSighup) { | |
| const newWorker = cluster.fork(); | |
| newWorker.on("listening", () => { | |
| console.info(`killing worker ${worker?.process.pid}`); | |
| worker?.disconnect(); | |
| }); | |
| } | |
| }); | |
| const shutdown = async () => { | |
| for (const worker of Object.values(cluster.workers || {})) { | |
| worker?.disconnect(); | |
| } | |
| if (shutdownPrimary) { | |
| await shutdownPrimary(); | |
| } | |
| console.log("exiting now"); | |
| process.exit(0); | |
| }; | |
| // eslint-disable-next-line @typescript-eslint/no-misused-promises | |
| process.on("SIGTERM", async () => { | |
| console.info("SIGTERM"); | |
| await shutdown(); | |
| }); | |
| // eslint-disable-next-line @typescript-eslint/no-misused-promises | |
| process.on("SIGINT", async () => { | |
| console.info("SIGINT"); | |
| await shutdown(); | |
| }); | |
| // Fork | |
| for (let i = 0; i < numWorkers; i++) { | |
| cluster.fork(); | |
| } | |
| if (recoverWorkers) { | |
| cluster.on("exit", (worker, code, signal) => { | |
| // we did this on purpose, so we don't need to restart the worker | |
| if (worker.exitedAfterDisconnect) { | |
| console.info( | |
| `worker ${worker.process.pid} exited after disconnect. not restarting.` | |
| ); | |
| return; | |
| } | |
| console.info( | |
| { worker_pid: worker.process.pid, signal, code }, | |
| "worker exited, restarting..." | |
| ); | |
| cluster.fork(); | |
| }); | |
| } | |
| if (primary) { | |
| await primary(); | |
| } | |
| } else { | |
| await worker(); | |
| } | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { createServer } from "node:http"; | |
| import { clustered } from "./clustered.ts"; | |
| const server = createServer((req, res) => { | |
| res.writeHead(200, { "Content-Type": "text/plain" }); | |
| res.end("Hello, World!\n"); | |
| }); | |
| await clustered({ | |
| numWorkers: 3, | |
| primary: async () => { | |
| console.log("primary started!", process.pid); | |
| }, | |
| worker: async () => { | |
| console.log("worker started!", process.pid); | |
| server.listen(3000); | |
| }, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To run it (requires node
23or above for type stripping):To send the
HUPsignal (just likesystemctl restartwould), find out thePIDof the main process and run:kill -HUP {{PID}}