Skip to content

Instantly share code, notes, and snippets.

@ljanecek
Created September 16, 2025 15:39
Show Gist options
  • Select an option

  • Save ljanecek/9680d38369fd7039e8a8e6ef7ea32686 to your computer and use it in GitHub Desktop.

Select an option

Save ljanecek/9680d38369fd7039e8a8e6ef7ea32686 to your computer and use it in GitHub Desktop.
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