Created
September 16, 2025 15:39
-
-
Save ljanecek/9680d38369fd7039e8a8e6ef7ea32686 to your computer and use it in GitHub Desktop.
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 http from "http"; | |
| import express from "express"; | |
| import { Server } from "socket.io"; | |
| const app = express(); | |
| const server = http.createServer(app); | |
| // ---- nastavení ---- | |
| const MAX_SOCKET_AGE_MS = Number(process.env.MAX_SOCKET_AGE_MS ?? 10 * 60_000); // např. 10 min | |
| const DRAIN_TIMEOUT_MS = Number(process.env.DRAIN_TIMEOUT_MS ?? 30_000); // např. 30 s | |
| const RECOVERY_WINDOW_MS = Number(process.env.RECOVERY_WINDOW_MS ?? 30_000); // např. 30 s | |
| // readiness/liveness | |
| let isReady = true; | |
| let shuttingDown = false; | |
| app.get("/livez", (_req, res) => res.status(200).send("ok")); | |
| app.get("/readyz", (_req, res) => res.status(isReady ? 200 : 503).send(isReady ? "ok" : "shutting down")); | |
| // Socket.IO | |
| const io = new Server(server, { | |
| connectionStateRecovery: { | |
| maxDisconnectionDuration: RECOVERY_WINDOW_MS, | |
| }, | |
| // volitelně: pokud používáte sticky sessions a chcete omezit reconnect šum: | |
| // transports: ["websocket"], // vypne long-polling | |
| }); | |
| // 1) Označíme nové handshaky jako nepovolené během shutdownu | |
| io.use((socket, next) => { | |
| if (shuttingDown) { | |
| const err = new Error("SERVER_SHUTTING_DOWN"); | |
| // klient dostane "connect_error" a může se zkusit připojit na jiný pod | |
| // (při multi-pod nasazení ho LB přesměruje jinam) | |
| return next(err); | |
| } | |
| return next(); | |
| }); | |
| // 2) U každého socketu si zapíšeme čas připojení (pro „stáří“) | |
| io.on("connection", (socket) => { | |
| socket.data.connectedAt = Date.now(); | |
| socket.on("disconnect", (reason) => { | |
| // debug logy dle potřeby | |
| // console.log(`socket ${socket.id} disconnected: ${reason}`); | |
| }); | |
| }); | |
| // --- Graceful shutdown flow --- | |
| process.on("SIGTERM", async () => { | |
| shuttingDown = true; | |
| isReady = false; | |
| // 1) přestaň přijímat nová HTTP/WS spojení | |
| // - Node server přestane akceptovat nové TCP, existující zůstanou | |
| server.close(); | |
| // 2) okamžitě odpoj ty klienty, kteří už přesáhli limit stáří | |
| try { | |
| // Lokální instance: projdeme jen sokety na TOMTO podu | |
| // (pro multi-node fetch přes adapter existuje io.fetchSockets(), ale to by sahalo i mimo pod) | |
| for (const [id, socket] of io.of("/").sockets) { | |
| const connectedAt = socket.data.connectedAt ?? Date.now(); | |
| const age = Date.now() - connectedAt; | |
| if (age > MAX_SOCKET_AGE_MS) { | |
| // true = "close underlying transport" => rychlé ukončení | |
| socket.disconnect(true); | |
| // případně: log/metric | |
| // console.log(`Disconnected aged socket ${id} (age=${Math.round(age/1000)}s)`); | |
| } else { | |
| // mladší sokety necháme doběhnout v drain okně | |
| // pro jistotu jim zakážeme připojovat se do nových rooms apod. (volitelné) | |
| // socket.data.draining = true; | |
| } | |
| } | |
| } catch (e) { | |
| // swallow & log | |
| // console.error("Error while culling aged sockets", e); | |
| } | |
| // 3) Po drain timeoutu shodíme zbylé sokety na tomto podu | |
| setTimeout(() => { | |
| // io.close() ukončí zbývající sockety a nepustí nové (už stejně blokujeme v middleware) | |
| io.close(() => { | |
| // 4) proces může skončit | |
| process.exit(0); | |
| }); | |
| // pojistka, kdyby callback nepřišel | |
| setTimeout(() => process.exit(0), 5_000); | |
| }, DRAIN_TIMEOUT_MS); | |
| }); | |
| // start | |
| const PORT = process.env.PORT || 3000; | |
| server.listen(PORT, () => { | |
| console.log(`listening on :${PORT}`); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment