Spaces:
Runtime error
Runtime error
| import net from "node:net"; | |
| // #4425: bumped from 30s β the old window reset the crash counter too quickly, so during | |
| // an EADDRINUSE cascade the supervisor kept "recovering" then crashing within the window | |
| // and exhausted its restart budget. A longer window keeps the counter meaningful. | |
| export const RESTART_RESET_MS = 60_000; | |
| // #4425: bumped from 2 β more recovery headroom before the supervisor gives up. | |
| export const DEFAULT_MAX_RESTARTS = 3; | |
| /** | |
| * #4425: a clean child exit (code 0) is only intentional when the supervisor itself is | |
| * shutting down. A spontaneous code-0 exit is anomalous β e.g. a systemd `MemoryMax` | |
| * cgroup kill reports the process exited with code 0 β and MUST be restarted, not treated | |
| * as a graceful stop (which left the gateway dead with `Restart=on-failure`). | |
| */ | |
| export function shouldExitInsteadOfRestart(isShuttingDown) { | |
| return isShuttingDown === true; | |
| } | |
| /** Exponential backoff (1s, 2s, 4s, β¦) capped at 10s, matching the prior inline formula. */ | |
| export function computeRestartDelayMs(restartCount) { | |
| return Math.min(1000 * 2 ** (Math.max(1, restartCount) - 1), 10_000); | |
| } | |
| /** Resolve true when nothing is listening on `port` (so a restart won't hit EADDRINUSE). */ | |
| export function isPortFree(port, host = "127.0.0.1") { | |
| return new Promise((resolve) => { | |
| const tester = net.createServer(); | |
| tester.once("error", (err) => { | |
| // EADDRINUSE = something is bound β not free. Any other error β treat as free. | |
| resolve(!(err && err.code === "EADDRINUSE")); | |
| }); | |
| tester.once("listening", () => { | |
| tester.close(() => resolve(true)); | |
| }); | |
| tester.listen(port, host); | |
| }); | |
| } | |
| /** | |
| * #4425: wait until `port` is free before respawning. After a crash the OS may not have | |
| * released the listen socket yet; restarting immediately produced the EADDRINUSE cascade | |
| * that exhausted the restart budget. Polls up to `timeoutMs`, then proceeds anyway so a | |
| * stuck port never blocks recovery forever. | |
| */ | |
| export async function waitUntilPortFree(port, timeoutMs = 10_000, intervalMs = 250) { | |
| const p = Number(port); | |
| if (!Number.isFinite(p) || p <= 0) return true; | |
| const deadline = Date.now() + timeoutMs; | |
| for (;;) { | |
| if (await isPortFree(p)) return true; | |
| if (Date.now() >= deadline) return false; | |
| await new Promise((r) => setTimeout(r, intervalMs)); | |
| } | |
| } | |