




















@@ -280,6 +280,17 @@ async function waitForStart(started: Promise<void>) {
280280await new Promise<void>((resolve) => setImmediate(resolve));
281281}
282282283+async function waitForLoopCondition(predicate: () => boolean, message: string) {
284+const deadline = Date.now() + 1_000;
285+while (Date.now() < deadline) {
286+if (predicate()) {
287+return;
288+}
289+await new Promise<void>((resolve) => setImmediate(resolve));
290+}
291+throw new Error(message);
292+}
293+283294async function createSignaledLoopHarness(exitCallOrder?: string[]) {
284295const close = vi.fn(async () => {});
285296const { start, started } = createSignaledStart(close);
@@ -595,6 +606,282 @@ describe("runGatewayLoop", () => {
595606});
596607});
597608609+it("queues SIGUSR1 received before the run-loop installs its restart waiter", async () => {
610+vi.clearAllMocks();
611+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
612+respawnGatewayProcessForUpdate.mockReturnValue({
613+mode: "disabled",
614+detail: "OPENCLAW_NO_RESPAWN",
615+});
616+617+await withIsolatedSignals(async ({ captureSignal }) => {
618+const closeFirst = vi.fn(async () => {});
619+const closeSecond = vi.fn(async () => {});
620+const { runtime, exited } = createRuntimeWithExitSignal();
621+let releaseFirstStart!: () => void;
622+const firstStartMayReturn = new Promise<void>((resolve) => {
623+releaseFirstStart = resolve;
624+});
625+let sigusr1: (() => void) | null = null;
626+let resolveSecondStart: (() => void) | null = null;
627+const startedSecond = new Promise<void>((resolve) => {
628+resolveSecondStart = resolve;
629+});
630+const start = vi.fn();
631+start.mockImplementationOnce(async () => {
632+await firstStartMayReturn;
633+sigusr1?.();
634+await waitForLoopCondition(
635+() => markGatewaySigusr1RestartHandled.mock.calls.length > 0,
636+"expected SIGUSR1 handler to consume the restart before startup returned",
637+);
638+return { close: closeFirst };
639+});
640+start.mockImplementationOnce(async () => {
641+resolveSecondStart?.();
642+return { close: closeSecond };
643+});
644+645+const { runGatewayLoop } = await import("./run-loop.js");
646+void runGatewayLoop({
647+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
648+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
649+});
650+await new Promise<void>((resolve) => setImmediate(resolve));
651+sigusr1 = captureSignal("SIGUSR1");
652+const sigterm = captureSignal("SIGTERM");
653+654+try {
655+releaseFirstStart();
656+657+await waitForLoopCondition(
658+() => start.mock.calls.length >= 2,
659+"expected queued SIGUSR1 to trigger the second gateway start",
660+);
661+await startedSecond;
662+expect(closeFirst).toHaveBeenCalledWith({
663+reason: "gateway restarting",
664+restartExpectedMs: 1500,
665+});
666+expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1);
667+expect(markGatewayDraining).toHaveBeenCalledTimes(1);
668+expect(resetAllLanes).toHaveBeenCalledTimes(1);
669+expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(1);
670+expect(reloadTaskRegistryFromStore).toHaveBeenCalledTimes(1);
671+} finally {
672+sigterm();
673+await expect(exited).resolves.toBe(0);
674+}
675+});
676+});
677+678+it("processes SIGINT immediately before startup returns a server", async () => {
679+vi.clearAllMocks();
680+681+await withIsolatedSignals(async ({ captureSignal }) => {
682+const close = vi.fn(async () => {});
683+const startupNeverReturns = new Promise<void>(() => {});
684+const { runtime, exited } = createRuntimeWithExitSignal();
685+const start = vi.fn(async () => {
686+await startupNeverReturns;
687+return { close };
688+});
689+690+const { runGatewayLoop } = await import("./run-loop.js");
691+void runGatewayLoop({
692+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
693+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
694+});
695+await new Promise<void>((resolve) => setImmediate(resolve));
696+const sigint = captureSignal("SIGINT");
697+698+sigint();
699+700+await expect(exited).resolves.toBe(0);
701+expect(close).not.toHaveBeenCalled();
702+expect(start).toHaveBeenCalledTimes(1);
703+expect(acquireGatewayLock).toHaveBeenCalledTimes(1);
704+});
705+});
706+707+it("lets SIGINT override a queued startup restart before startup returns a server", async () => {
708+vi.clearAllMocks();
709+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
710+711+await withIsolatedSignals(async ({ captureSignal }) => {
712+const close = vi.fn(async () => {});
713+const startupNeverReturns = new Promise<void>(() => {});
714+const { runtime, exited } = createRuntimeWithExitSignal();
715+const start = vi.fn(async () => {
716+await startupNeverReturns;
717+return { close };
718+});
719+720+const { runGatewayLoop } = await import("./run-loop.js");
721+void runGatewayLoop({
722+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
723+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
724+});
725+await new Promise<void>((resolve) => setImmediate(resolve));
726+const sigusr1 = captureSignal("SIGUSR1");
727+const sigint = captureSignal("SIGINT");
728+729+sigusr1();
730+await waitForLoopCondition(
731+() => markGatewaySigusr1RestartHandled.mock.calls.length > 0,
732+"expected startup SIGUSR1 to be queued",
733+);
734+735+sigint();
736+737+await expect(exited).resolves.toBe(0);
738+expect(close).not.toHaveBeenCalled();
739+expect(markGatewayDraining).not.toHaveBeenCalled();
740+expect(start).toHaveBeenCalledTimes(1);
741+expect(acquireGatewayLock).toHaveBeenCalledTimes(1);
742+expect(gatewayLog.info).toHaveBeenCalledWith(
743+"received SIGINT; overriding pending startup restart with shutdown",
744+);
745+});
746+});
747+748+it("processes queued SIGUSR1 when restart startup fails before returning a server", async () => {
749+vi.clearAllMocks();
750+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
751+respawnGatewayProcessForUpdate.mockReturnValue({
752+mode: "disabled",
753+detail: "OPENCLAW_NO_RESPAWN",
754+});
755+756+await withIsolatedSignals(async ({ captureSignal }) => {
757+const closeFirst = vi.fn(async () => {});
758+const closeThird = vi.fn(async () => {});
759+const { runtime, exited } = createRuntimeWithExitSignal();
760+let sigusr1: (() => void) | null = null;
761+let resolveThirdStart: (() => void) | null = null;
762+const startedThird = new Promise<void>((resolve) => {
763+resolveThirdStart = resolve;
764+});
765+const start = vi.fn();
766+start.mockResolvedValueOnce({ close: closeFirst });
767+start.mockImplementationOnce(async () => {
768+sigusr1?.();
769+await waitForLoopCondition(
770+() => markGatewaySigusr1RestartHandled.mock.calls.length >= 2,
771+"expected SIGUSR1 during failed startup to be accepted before startup throws",
772+);
773+throw new Error("restart startup failed");
774+});
775+start.mockImplementationOnce(async () => {
776+resolveThirdStart?.();
777+return { close: closeThird };
778+});
779+780+const { runGatewayLoop } = await import("./run-loop.js");
781+void runGatewayLoop({
782+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
783+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
784+});
785+await new Promise<void>((resolve) => setImmediate(resolve));
786+sigusr1 = captureSignal("SIGUSR1");
787+const sigterm = captureSignal("SIGTERM");
788+789+try {
790+sigusr1();
791+792+await waitForLoopCondition(
793+() => start.mock.calls.length >= 3,
794+"expected queued SIGUSR1 to advance past failed restart startup",
795+);
796+await startedThird;
797+expect(closeFirst).toHaveBeenCalledWith({
798+reason: "gateway restarting",
799+restartExpectedMs: 1500,
800+});
801+expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
802+expect(markGatewayDraining).toHaveBeenCalledTimes(2);
803+expect(resetAllLanes).toHaveBeenCalledTimes(2);
804+expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
805+expect(reloadTaskRegistryFromStore).toHaveBeenCalledTimes(2);
806+expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
807+expect(gatewayLog.error).toHaveBeenCalledWith(
808+expect.stringContaining("gateway startup failed: restart startup failed."),
809+);
810+} finally {
811+sigterm();
812+await expect(exited).resolves.toBe(0);
813+}
814+});
815+});
816+817+it("processes SIGUSR1 received after restart startup fails before returning a server", async () => {
818+vi.clearAllMocks();
819+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
820+respawnGatewayProcessForUpdate.mockReturnValue({
821+mode: "disabled",
822+detail: "OPENCLAW_NO_RESPAWN",
823+});
824+825+await withIsolatedSignals(async ({ captureSignal }) => {
826+const closeFirst = vi.fn(async () => {});
827+const closeThird = vi.fn(async () => {});
828+const { runtime, exited } = createRuntimeWithExitSignal();
829+let resolveThirdStart: (() => void) | null = null;
830+const startedThird = new Promise<void>((resolve) => {
831+resolveThirdStart = resolve;
832+});
833+const start = vi.fn();
834+start.mockResolvedValueOnce({ close: closeFirst });
835+start.mockRejectedValueOnce(new Error("restart startup failed"));
836+start.mockImplementationOnce(async () => {
837+resolveThirdStart?.();
838+return { close: closeThird };
839+});
840+841+const { runGatewayLoop } = await import("./run-loop.js");
842+void runGatewayLoop({
843+start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
844+runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
845+});
846+await new Promise<void>((resolve) => setImmediate(resolve));
847+const sigusr1 = captureSignal("SIGUSR1");
848+const sigterm = captureSignal("SIGTERM");
849+850+try {
851+sigusr1();
852+await waitForLoopCondition(
853+() =>
854+gatewayLog.error.mock.calls.some(([message]) =>
855+String(message).includes("gateway startup failed: restart startup failed."),
856+),
857+"expected failed restart startup to be logged",
858+);
859+await new Promise<void>((resolve) => setImmediate(resolve));
860+expect(start).toHaveBeenCalledTimes(2);
861+862+sigusr1();
863+await waitForLoopCondition(
864+() => start.mock.calls.length >= 3,
865+"expected post-failure SIGUSR1 to retry gateway startup",
866+);
867+await startedThird;
868+expect(closeFirst).toHaveBeenCalledWith({
869+reason: "gateway restarting",
870+restartExpectedMs: 1500,
871+});
872+expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
873+expect(markGatewayDraining).toHaveBeenCalledTimes(2);
874+expect(resetAllLanes).toHaveBeenCalledTimes(2);
875+expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
876+expect(reloadTaskRegistryFromStore).toHaveBeenCalledTimes(2);
877+expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
878+} finally {
879+sigterm();
880+await expect(exited).resolves.toBe(0);
881+}
882+});
883+});
884+598885it("uses the default restart drain timeout when config omits deferralTimeoutMs", async () => {
599886vi.clearAllMocks();
600887loadConfig.mockReturnValue({ gateway: { reload: {} } });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。