












@@ -9,6 +9,9 @@ const acquireGatewayLock = vi.fn(async (_opts?: { port?: number }) => ({
99const consumeGatewayRestartIntentPayloadSync = vi.fn<
1010() => { reason?: string; force?: boolean; waitMs?: number } | null
1111>(() => null);
12+const consumeGatewaySigusr1RestartIntent = vi.fn<
13+() => { reason?: string; force?: boolean; waitMs?: number } | null
14+>(() => null);
1215const consumeGatewaySigusr1RestartAuthorization = vi.fn(() => true);
1316const consumeGatewayRestartIntentSync = vi.fn(() => false);
1417const isGatewaySigusr1RestartExternallyAllowed = vi.fn(() => false);
@@ -81,6 +84,8 @@ const markRestartAbortedMainSessions = vi.fn(async (_params: unknown) => ({
8184}));
8285const waitForActiveEmbeddedRuns = vi.fn(async (_timeoutMs?: number) => ({ drained: true }));
8386const DRAIN_TIMEOUT_LOG = "drain timeout reached; proceeding with restart";
87+const ACTIVE_RUN_DRAIN_TIMEOUT_LOG =
88+"active embedded run drain timeout reached; aborting active run(s) before restart";
8489const DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS = 300_000;
8590const loadConfig = vi.fn<() => { gateway: { reload: { deferralTimeoutMs?: number } } }>(() => ({
8691gateway: {
@@ -101,6 +106,7 @@ vi.mock("../../infra/gateway-lock.js", () => ({
101106102107vi.mock("../../infra/restart.js", () => ({
103108consumeGatewayRestartIntentPayloadSync: () => consumeGatewayRestartIntentPayloadSync(),
109+consumeGatewaySigusr1RestartIntent: () => consumeGatewaySigusr1RestartIntent(),
104110consumeGatewaySigusr1RestartAuthorization: () => consumeGatewaySigusr1RestartAuthorization(),
105111consumeGatewayRestartIntentSync: () => consumeGatewayRestartIntentSync(),
106112isGatewaySigusr1RestartExternallyAllowed: () => isGatewaySigusr1RestartExternallyAllowed(),
@@ -430,6 +436,7 @@ describe("runGatewayLoop", () => {
430436vi.clearAllMocks();
431437consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({ waitMs: 2_500 });
432438getActiveTaskCount.mockReturnValueOnce(1).mockReturnValue(0);
439+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
433440434441await withIsolatedSignals(async ({ captureSignal }) => {
435442const { start, exited } = await createSignaledLoopHarness();
@@ -441,6 +448,7 @@ describe("runGatewayLoop", () => {
441448await new Promise<void>((resolve) => setImmediate(resolve));
442449443450expect(waitForActiveTasks).toHaveBeenCalledWith(2_500);
451+expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(2_500);
444452expect(start).toHaveBeenCalledTimes(2);
445453446454sigint();
@@ -469,7 +477,65 @@ describe("runGatewayLoop", () => {
469477});
470478});
471479472-it("aborts active embedded runs after a short restart drain grace", async () => {
480+it("waits indefinitely for active embedded runs on unbounded restarts", async () => {
481+vi.clearAllMocks();
482+consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({ waitMs: 0 });
483+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
484+waitForActiveEmbeddedRuns.mockResolvedValueOnce({ drained: true });
485+486+await withIsolatedSignals(async ({ captureSignal }) => {
487+const { close, start, exited } = await createSignaledLoopHarness();
488+const sigterm = captureSignal("SIGTERM");
489+const sigint = captureSignal("SIGINT");
490+491+sigterm();
492+await new Promise<void>((resolve) => setImmediate(resolve));
493+await new Promise<void>((resolve) => setImmediate(resolve));
494+495+expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(undefined);
496+expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" });
497+expect(abortEmbeddedPiRun).not.toHaveBeenCalledWith(undefined, { mode: "all" });
498+expectRestartCloseCall(close, 15_000);
499+expect(start).toHaveBeenCalledTimes(2);
500+501+sigint();
502+await expect(exited).resolves.toBe(0);
503+});
504+});
505+506+it("waits indefinitely for active embedded runs when reload config disables deferral timeout", async () => {
507+vi.clearAllMocks();
508+loadConfig.mockReturnValueOnce({
509+gateway: {
510+reload: {
511+deferralTimeoutMs: 0,
512+},
513+},
514+});
515+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
516+waitForActiveEmbeddedRuns.mockResolvedValueOnce({ drained: true });
517+518+await withIsolatedSignals(async ({ captureSignal }) => {
519+const { close, start, exited } = await createSignaledLoopHarness();
520+const sigusr1 = captureSignal("SIGUSR1");
521+const sigint = captureSignal("SIGINT");
522+523+sigusr1();
524+await new Promise<void>((resolve) => setImmediate(resolve));
525+await new Promise<void>((resolve) => setImmediate(resolve));
526+527+expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(undefined);
528+expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" });
529+expect(abortEmbeddedPiRun).not.toHaveBeenCalledWith(undefined, { mode: "all" });
530+expectRestartCloseCall(close, 15_000);
531+expect(start).toHaveBeenCalledTimes(2);
532+533+sigint();
534+await expect(exited).resolves.toBe(0);
535+});
536+});
537+538+it("uses the restart drain timeout for active embedded runs before aborting", async () => {
473539vi.clearAllMocks();
474540consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({});
475541getActiveTaskCount.mockReturnValueOnce(1).mockReturnValue(0);
@@ -490,12 +556,10 @@ describe("runGatewayLoop", () => {
490556await new Promise<void>((resolve) => setImmediate(resolve));
491557492558expect(waitForActiveTasks).toHaveBeenCalledWith(90_000);
493-expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(30_000);
559+expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(90_000);
494560expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" });
495561expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" });
496-expect(gatewayLog.warn).toHaveBeenCalledWith(
497-"active embedded run drain grace reached; aborting active run(s) before restart",
498-);
562+expect(gatewayLog.warn).toHaveBeenCalledWith(ACTIVE_RUN_DRAIN_TIMEOUT_LOG);
499563expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG);
500564expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({
501565cfg: {
@@ -520,6 +584,51 @@ describe("runGatewayLoop", () => {
520584});
521585});
522586587+it("skips a second active-work drain after a SIGUSR1 deferral timeout intent", async () => {
588+vi.clearAllMocks();
589+consumeGatewaySigusr1RestartIntent.mockReturnValueOnce({
590+force: true,
591+reason: "config reload forced restart",
592+});
593+getActiveTaskCount.mockReturnValueOnce(1).mockReturnValue(0);
594+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
595+listActiveEmbeddedRunSessionIds.mockReturnValueOnce(["session-deferral-timeout"]);
596+listActiveEmbeddedRunSessionKeys.mockReturnValueOnce(["agent:main:deferral-timeout"]);
597+598+await withIsolatedSignals(async ({ captureSignal }) => {
599+const { close, start, exited } = await createSignaledLoopHarness();
600+const sigusr1 = captureSignal("SIGUSR1");
601+const sigint = captureSignal("SIGINT");
602+603+sigusr1();
604+await new Promise<void>((resolve) => setImmediate(resolve));
605+await new Promise<void>((resolve) => setImmediate(resolve));
606+607+expect(waitForActiveTasks).not.toHaveBeenCalled();
608+expect(waitForActiveEmbeddedRuns).not.toHaveBeenCalled();
609+expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" });
610+expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" });
611+expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({
612+cfg: {
613+gateway: {
614+reload: {
615+deferralTimeoutMs: 90_000,
616+},
617+},
618+},
619+sessionIds: new Set(["session-deferral-timeout"]),
620+sessionKeys: new Set(["agent:main:deferral-timeout"]),
621+reason: "config reload forced restart",
622+});
623+expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledOnce();
624+expectRestartCloseCall(close, 0);
625+expect(start).toHaveBeenCalledTimes(2);
626+627+sigint();
628+await expect(exited).resolves.toBe(0);
629+});
630+});
631+523632it("forces SIGTERM restarts without waiting for active task drain", async () => {
524633vi.clearAllMocks();
525634consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({ force: true });
@@ -1029,6 +1138,7 @@ describe("runGatewayLoop", () => {
1029113810301139await withIsolatedSignals(async ({ captureSignal }) => {
10311140getActiveTaskCount.mockReturnValueOnce(1).mockReturnValue(0);
1141+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
1032114210331143const { start } = await createSignaledLoopHarness();
10341144const sigusr1 = captureSignal("SIGUSR1");
@@ -1038,6 +1148,7 @@ describe("runGatewayLoop", () => {
10381148await new Promise<void>((resolve) => setImmediate(resolve));
1039114910401150expect(waitForActiveTasks).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS);
1151+expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS);
10411152expect(markGatewayDraining).toHaveBeenCalledOnce();
10421153expect(start).toHaveBeenCalledTimes(2);
10431154});
@@ -1090,6 +1201,53 @@ describe("runGatewayLoop", () => {
10901201});
10911202});
109212031204+it("clears the in-flight restart token when a file intent handles authorized SIGUSR1", async () => {
1205+vi.clearAllMocks();
1206+consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({
1207+force: true,
1208+reason: "file-intent restart",
1209+});
1210+loadConfig.mockReturnValueOnce({
1211+gateway: {
1212+reload: {
1213+deferralTimeoutMs: 90_000,
1214+},
1215+},
1216+});
1217+getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0);
1218+listActiveEmbeddedRunSessionIds.mockReturnValueOnce(["session-file-intent"]);
1219+listActiveEmbeddedRunSessionKeys.mockReturnValueOnce(["agent:main:file-intent"]);
1220+1221+await withIsolatedSignals(async ({ captureSignal }) => {
1222+const { start, exited } = await createSignaledLoopHarness();
1223+const sigusr1 = captureSignal("SIGUSR1");
1224+const sigint = captureSignal("SIGINT");
1225+1226+sigusr1();
1227+await new Promise<void>((resolve) => setImmediate(resolve));
1228+await new Promise<void>((resolve) => setImmediate(resolve));
1229+1230+expect(consumeGatewaySigusr1RestartAuthorization).toHaveBeenCalledOnce();
1231+expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledOnce();
1232+expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({
1233+cfg: {
1234+gateway: {
1235+reload: {
1236+deferralTimeoutMs: 90_000,
1237+},
1238+},
1239+},
1240+sessionIds: new Set(["session-file-intent"]),
1241+sessionKeys: new Set(["agent:main:file-intent"]),
1242+reason: "file-intent restart",
1243+});
1244+expect(start).toHaveBeenCalledTimes(2);
1245+1246+sigint();
1247+await expect(exited).resolves.toBe(0);
1248+});
1249+});
1250+10931251it("releases the lock before exiting on spawned restart", async () => {
10941252vi.clearAllMocks();
10951253peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。