




















@@ -6,6 +6,7 @@ const acquireGatewayLock = vi.fn(async (_opts?: { port?: number }) => ({
66release: vi.fn(async () => {}),
77}));
88const consumeGatewaySigusr1RestartAuthorization = vi.fn(() => true);
9+const consumeGatewayRestartIntentSync = vi.fn(() => false);
910const isGatewaySigusr1RestartExternallyAllowed = vi.fn(() => false);
1011const markGatewaySigusr1RestartHandled = vi.fn();
1112const scheduleGatewaySigusr1Restart = vi.fn((_opts?: { delayMs?: number; reason?: string }) => ({
@@ -19,7 +20,7 @@ const scheduleGatewaySigusr1Restart = vi.fn((_opts?: { delayMs?: number; reason?
1920}));
2021const getActiveTaskCount = vi.fn(() => 0);
2122const markGatewayDraining = vi.fn();
22-const waitForActiveTasks = vi.fn(async (_timeoutMs: number) => ({ drained: true }));
23+const waitForActiveTasks = vi.fn(async (_timeoutMs?: number) => ({ drained: true }));
2324const resetAllLanes = vi.fn();
2425const restartGatewayProcessWithFreshPid = vi.fn<
2526() => { mode: "spawned" | "supervised" | "disabled" | "failed"; pid?: number; detail?: string }
@@ -28,7 +29,7 @@ const abortEmbeddedPiRun = vi.fn(
2829(_sessionId?: string, _opts?: { mode?: "all" | "compacting" }) => false,
2930);
3031const getActiveEmbeddedRunCount = vi.fn(() => 0);
31-const waitForActiveEmbeddedRuns = vi.fn(async (_timeoutMs: number) => ({ drained: true }));
32+const waitForActiveEmbeddedRuns = vi.fn(async (_timeoutMs?: number) => ({ drained: true }));
3233const DRAIN_TIMEOUT_LOG = "drain timeout reached; proceeding with restart";
3334const loadConfig = vi.fn(() => ({
3435gateway: {
@@ -49,6 +50,7 @@ vi.mock("../../infra/gateway-lock.js", () => ({
49505051vi.mock("../../infra/restart.js", () => ({
5152consumeGatewaySigusr1RestartAuthorization: () => consumeGatewaySigusr1RestartAuthorization(),
53+consumeGatewayRestartIntentSync: () => consumeGatewayRestartIntentSync(),
5254isGatewaySigusr1RestartExternallyAllowed: () => isGatewaySigusr1RestartExternallyAllowed(),
5355markGatewaySigusr1RestartHandled: () => markGatewaySigusr1RestartHandled(),
5456scheduleGatewaySigusr1Restart: (opts?: { delayMs?: number; reason?: string }) =>
@@ -62,15 +64,15 @@ vi.mock("../../infra/process-respawn.js", () => ({
6264vi.mock("../../process/command-queue.js", () => ({
6365getActiveTaskCount: () => getActiveTaskCount(),
6466markGatewayDraining: () => markGatewayDraining(),
65-waitForActiveTasks: (timeoutMs: number) => waitForActiveTasks(timeoutMs),
67+waitForActiveTasks: (timeoutMs?: number) => waitForActiveTasks(timeoutMs),
6668resetAllLanes: () => resetAllLanes(),
6769}));
68706971vi.mock("../../agents/pi-embedded-runner/runs.js", () => ({
7072abortEmbeddedPiRun: (sessionId?: string, opts?: { mode?: "all" | "compacting" }) =>
7173abortEmbeddedPiRun(sessionId, opts),
7274getActiveEmbeddedRunCount: () => getActiveEmbeddedRunCount(),
73-waitForActiveEmbeddedRuns: (timeoutMs: number) => waitForActiveEmbeddedRuns(timeoutMs),
75+waitForActiveEmbeddedRuns: (timeoutMs?: number) => waitForActiveEmbeddedRuns(timeoutMs),
7476}));
75777678vi.mock("../../config/config.js", () => ({
@@ -226,6 +228,50 @@ describe("runGatewayLoop", () => {
226228});
227229});
228230231+it("treats SIGTERM with a restart intent as a draining restart", async () => {
232+vi.clearAllMocks();
233+consumeGatewayRestartIntentSync.mockReturnValueOnce(true);
234+getActiveTaskCount.mockReturnValueOnce(1).mockReturnValue(0);
235+236+await withIsolatedSignals(async ({ captureSignal }) => {
237+const closeFirst = vi.fn(async () => {});
238+const closeSecond = vi.fn(async () => {});
239+const { runtime, exited } = createRuntimeWithExitSignal();
240+const start = vi
241+.fn()
242+.mockResolvedValueOnce({ close: closeFirst })
243+.mockResolvedValueOnce({ close: closeSecond });
244+const { runGatewayLoop } = await import("./run-loop.js");
245+void runGatewayLoop({
246+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
247+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
248+});
249+await new Promise<void>((resolve) => setImmediate(resolve));
250+const sigterm = captureSignal("SIGTERM");
251+const sigint = captureSignal("SIGINT");
252+253+sigterm();
254+await new Promise<void>((resolve) => setImmediate(resolve));
255+await new Promise<void>((resolve) => setImmediate(resolve));
256+257+expect(consumeGatewayRestartIntentSync).toHaveBeenCalledOnce();
258+expect(markGatewayDraining).toHaveBeenCalledOnce();
259+expect(waitForActiveTasks).toHaveBeenCalledWith(90_000);
260+expect(closeFirst).toHaveBeenCalledWith({
261+reason: "gateway restarting",
262+restartExpectedMs: 1500,
263+});
264+expect(start).toHaveBeenCalledTimes(2);
265+266+sigint();
267+await expect(exited).resolves.toBe(0);
268+expect(closeSecond).toHaveBeenCalledWith({
269+reason: "gateway stopping",
270+restartExpectedMs: null,
271+});
272+});
273+});
274+229275it("restarts after SIGUSR1 even when drain times out, and resets lanes for the new iteration", async () => {
230276vi.clearAllMocks();
231277loadConfig.mockReturnValue({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。