Spaces:
Paused
Paused
| import { describe, expect, it, vi } from "vitest"; | |
| const acquireGatewayLock = vi.fn(async () => ({ | |
| release: vi.fn(async () => {}), | |
| })); | |
| const consumeGatewaySigusr1RestartAuthorization = vi.fn(() => true); | |
| const isGatewaySigusr1RestartExternallyAllowed = vi.fn(() => false); | |
| const markGatewaySigusr1RestartHandled = vi.fn(); | |
| const getActiveTaskCount = vi.fn(() => 0); | |
| const waitForActiveTasks = vi.fn(async () => ({ drained: true })); | |
| const resetAllLanes = vi.fn(); | |
| const DRAIN_TIMEOUT_LOG = "drain timeout reached; proceeding with restart"; | |
| const gatewayLog = { | |
| info: vi.fn(), | |
| warn: vi.fn(), | |
| error: vi.fn(), | |
| }; | |
| vi.mock("../../infra/gateway-lock.js", () => ({ | |
| acquireGatewayLock: () => acquireGatewayLock(), | |
| })); | |
| vi.mock("../../infra/restart.js", () => ({ | |
| consumeGatewaySigusr1RestartAuthorization: () => consumeGatewaySigusr1RestartAuthorization(), | |
| isGatewaySigusr1RestartExternallyAllowed: () => isGatewaySigusr1RestartExternallyAllowed(), | |
| markGatewaySigusr1RestartHandled: () => markGatewaySigusr1RestartHandled(), | |
| })); | |
| vi.mock("../../process/command-queue.js", () => ({ | |
| getActiveTaskCount: () => getActiveTaskCount(), | |
| waitForActiveTasks: (timeoutMs: number) => waitForActiveTasks(timeoutMs), | |
| resetAllLanes: () => resetAllLanes(), | |
| })); | |
| vi.mock("../../logging/subsystem.js", () => ({ | |
| createSubsystemLogger: () => gatewayLog, | |
| })); | |
| function removeNewSignalListeners( | |
| signal: NodeJS.Signals, | |
| existing: Set<(...args: unknown[]) => void>, | |
| ) { | |
| for (const listener of process.listeners(signal)) { | |
| const fn = listener as (...args: unknown[]) => void; | |
| if (!existing.has(fn)) { | |
| process.removeListener(signal, fn); | |
| } | |
| } | |
| } | |
| describe("runGatewayLoop", () => { | |
| it("restarts after SIGUSR1 even when drain times out, and resets lanes for the new iteration", async () => { | |
| vi.clearAllMocks(); | |
| getActiveTaskCount.mockReturnValueOnce(2).mockReturnValueOnce(0); | |
| waitForActiveTasks.mockResolvedValueOnce({ drained: false }); | |
| type StartServer = () => Promise<{ | |
| close: (opts: { reason: string; restartExpectedMs: number | null }) => Promise<void>; | |
| }>; | |
| const closeFirst = vi.fn(async () => {}); | |
| const closeSecond = vi.fn(async () => {}); | |
| const start = vi | |
| .fn<StartServer>() | |
| .mockResolvedValueOnce({ close: closeFirst }) | |
| .mockResolvedValueOnce({ close: closeSecond }) | |
| .mockRejectedValueOnce(new Error("stop-loop")); | |
| const beforeSigterm = new Set( | |
| process.listeners("SIGTERM") as Array<(...args: unknown[]) => void>, | |
| ); | |
| const beforeSigint = new Set( | |
| process.listeners("SIGINT") as Array<(...args: unknown[]) => void>, | |
| ); | |
| const beforeSigusr1 = new Set( | |
| process.listeners("SIGUSR1") as Array<(...args: unknown[]) => void>, | |
| ); | |
| const loopPromise = import("./run-loop.js").then(({ runGatewayLoop }) => | |
| runGatewayLoop({ | |
| start, | |
| runtime: { | |
| exit: vi.fn(), | |
| } as { exit: (code: number) => never }, | |
| }), | |
| ); | |
| try { | |
| await vi.waitFor(() => { | |
| expect(start).toHaveBeenCalledTimes(1); | |
| }); | |
| process.emit("SIGUSR1"); | |
| await vi.waitFor(() => { | |
| expect(start).toHaveBeenCalledTimes(2); | |
| }); | |
| expect(waitForActiveTasks).toHaveBeenCalledWith(30_000); | |
| expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG); | |
| expect(closeFirst).toHaveBeenCalledWith({ | |
| reason: "gateway restarting", | |
| restartExpectedMs: 1500, | |
| }); | |
| expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1); | |
| expect(resetAllLanes).toHaveBeenCalledTimes(1); | |
| process.emit("SIGUSR1"); | |
| await expect(loopPromise).rejects.toThrow("stop-loop"); | |
| expect(closeSecond).toHaveBeenCalledWith({ | |
| reason: "gateway restarting", | |
| restartExpectedMs: 1500, | |
| }); | |
| expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2); | |
| expect(resetAllLanes).toHaveBeenCalledTimes(2); | |
| } finally { | |
| removeNewSignalListeners("SIGTERM", beforeSigterm); | |
| removeNewSignalListeners("SIGINT", beforeSigint); | |
| removeNewSignalListeners("SIGUSR1", beforeSigusr1); | |
| } | |
| }); | |
| }); | |