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
23
or above for type stripping):To send the
HUP
signal (just likesystemctl restart
would), find out thePID
of the main process and run:kill -HUP {{PID}}