fix(gateway): restart watch after child sigterm

This commit is contained in:
Peter Steinberger 2026-04-05 18:18:41 +01:00
parent 1a65c3b06d
commit cb76e5c899
No known key found for this signature in database
2 changed files with 39 additions and 1 deletions

View File

@ -8,6 +8,8 @@ import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node.mj
const WATCH_NODE_RUNNER = "scripts/run-node.mjs";
const WATCH_RESTART_SIGNAL = "SIGTERM";
const WATCH_RESTARTABLE_CHILD_EXIT_CODES = new Set([143]);
const WATCH_RESTARTABLE_CHILD_SIGNALS = new Set(["SIGTERM"]);
const buildRunnerArgs = (args) => [WATCH_NODE_RUNNER, ...args];
@ -27,6 +29,10 @@ const resolveRepoPath = (filePath, cwd) => {
const isIgnoredWatchPath = (filePath, cwd) =>
!isRestartRelevantRunNodePath(resolveRepoPath(filePath, cwd));
const shouldRestartAfterChildExit = (exitCode, exitSignal) =>
(typeof exitCode === "number" && WATCH_RESTARTABLE_CHILD_EXIT_CODES.has(exitCode)) ||
(typeof exitSignal === "string" && WATCH_RESTARTABLE_CHILD_SIGNALS.has(exitSignal));
export async function runWatchMain(params = {}) {
const deps = {
spawn: params.spawn ?? spawn,
@ -90,7 +96,7 @@ export async function runWatchMain(params = {}) {
if (shuttingDown) {
return;
}
if (restartRequested) {
if (restartRequested || shouldRestartAfterChildExit(exitCode, exitSignal)) {
restartRequested = false;
startRunner();
return;

View File

@ -147,6 +147,38 @@ describe("watch-node script", () => {
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
});
it("restarts when the runner exits with a SIGTERM-derived code unexpectedly", async () => {
const childA = Object.assign(new EventEmitter(), {
kill: vi.fn(),
});
const childB = Object.assign(new EventEmitter(), {
kill: vi.fn(() => {}),
});
const spawn = vi.fn().mockReturnValueOnce(childA).mockReturnValueOnce(childB);
const watcher = Object.assign(new EventEmitter(), {
close: vi.fn(async () => {}),
});
const createWatcher = vi.fn(() => watcher);
const fakeProcess = createFakeProcess();
const runPromise = runWatchMain({
args: ["gateway", "--force"],
createWatcher,
process: fakeProcess,
spawn,
});
childA.emit("exit", 143, null);
await new Promise((resolve) => setImmediate(resolve));
expect(spawn).toHaveBeenCalledTimes(2);
fakeProcess.emit("SIGINT");
const exitCode = await runPromise;
expect(exitCode).toBe(130);
expect(childB.kill).toHaveBeenCalledWith("SIGTERM");
expect(watcher.close).toHaveBeenCalledTimes(1);
});
it("forces no-respawn for watch children even when supervisor hints are inherited", async () => {
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();