fix(gateway): preserve restart drain for active runs

Fixes https://github.com/openclaw/openclaw/issues/65485
This commit is contained in:
Vincent Koc
2026-04-25 01:35:47 -07:00
committed by GitHub
parent 734748d4f4
commit ec1f72b6c5
17 changed files with 453 additions and 64 deletions

View File

@@ -16,6 +16,8 @@ const loadConfig = vi.fn<() => OpenClawConfig>(() => ({
},
},
}));
const writeGatewayRestartIntentSync = vi.fn();
const clearGatewayRestartIntentSync = vi.fn();
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfig(),
@@ -26,6 +28,11 @@ vi.mock("../../runtime.js", () => ({
defaultRuntime,
}));
vi.mock("../../infra/restart.js", () => ({
clearGatewayRestartIntentSync: () => clearGatewayRestartIntentSync(),
writeGatewayRestartIntentSync: (opts: unknown) => writeGatewayRestartIntentSync(opts),
}));
let runServiceRestart: typeof import("./lifecycle-core.js").runServiceRestart;
let runServiceStart: typeof import("./lifecycle-core.js").runServiceStart;
let runServiceStop: typeof import("./lifecycle-core.js").runServiceStop;
@@ -92,6 +99,8 @@ describe("runServiceRestart token drift", () => {
},
});
resetLifecycleServiceMocks();
writeGatewayRestartIntentSync.mockClear();
clearGatewayRestartIntentSync.mockClear();
service.readCommand.mockResolvedValue({
programArguments: [],
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
@@ -182,6 +191,7 @@ describe("runServiceRestart token drift", () => {
expect(loadConfig).not.toHaveBeenCalled();
expect(service.readCommand).not.toHaveBeenCalled();
expect(writeGatewayRestartIntentSync).not.toHaveBeenCalled();
const payload = readJsonLog<{ warnings?: string[] }>();
expect(payload.warnings).toBeUndefined();
});
@@ -305,6 +315,27 @@ describe("runServiceRestart token drift", () => {
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
});
it("writes a restart intent before service-manager restart", async () => {
service.readRuntime.mockResolvedValue({ status: "running", pid: 1234 });
await runServiceRestart(createServiceRunArgs());
expect(writeGatewayRestartIntentSync).toHaveBeenCalledWith({ targetPid: 1234 });
expect(clearGatewayRestartIntentSync).not.toHaveBeenCalled();
expect(service.restart).toHaveBeenCalledTimes(1);
});
it("clears restart intent when service-manager restart fails before signaling", async () => {
service.readRuntime.mockResolvedValue({ status: "running", pid: 1234 });
writeGatewayRestartIntentSync.mockReturnValueOnce(true);
service.restart.mockRejectedValueOnce(new Error("launchctl failed before signaling"));
await expect(runServiceRestart(createServiceRunArgs())).rejects.toThrow("__exit__:1");
expect(writeGatewayRestartIntentSync).toHaveBeenCalledWith({ targetPid: 1234 });
expect(clearGatewayRestartIntentSync).toHaveBeenCalledOnce();
});
it("emits scheduled when service start routes through a scheduled restart", async () => {
service.restart.mockResolvedValue({ outcome: "scheduled" });

View File

@@ -9,6 +9,10 @@ import type { GatewayService } from "../../daemon/service.js";
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
import { isGatewaySecretRefUnavailableError } from "../../gateway/credentials.js";
import {
clearGatewayRestartIntentSync,
writeGatewayRestartIntentSync,
} from "../../infra/restart.js";
import { isWSL } from "../../infra/wsl.js";
import { defaultRuntime } from "../../runtime.js";
import { resolveGatewayTokenForDriftCheck } from "./gateway-token-drift.js";
@@ -458,7 +462,21 @@ export async function runServiceRestart(params: {
try {
let restartResult: GatewayServiceRestartResult = { outcome: "completed" };
if (loaded) {
restartResult = await params.service.restart({ env: process.env, stdout });
let wroteRestartIntent = false;
if (params.serviceNoun === "Gateway") {
const runtime = await params.service.readRuntime(process.env).catch(() => null);
wroteRestartIntent = writeGatewayRestartIntentSync({
targetPid: runtime?.pid,
});
}
try {
restartResult = await params.service.restart({ env: process.env, stdout });
} catch (err) {
if (wroteRestartIntent) {
clearGatewayRestartIntentSync();
}
throw err;
}
}
let restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
if (restartStatus.scheduled) {