File size: 2,361 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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));
  }
}