mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
fix(gateway): preserve restart hooks across coalescing
This commit is contained in:
@@ -116,6 +116,63 @@ describe("infra runtime", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the latest preparation hook when scheduled restarts coalesce", async () => {
|
||||
const firstBeforeEmit = vi.fn(async () => {});
|
||||
const latestBeforeEmit = vi.fn(async () => {});
|
||||
const emitSpy = vi.spyOn(process, "emit");
|
||||
const handler = () => {};
|
||||
process.on("SIGUSR1", handler);
|
||||
try {
|
||||
const first = scheduleGatewaySigusr1Restart({
|
||||
delayMs: 1_000,
|
||||
reason: "first",
|
||||
emitHooks: { beforeEmit: firstBeforeEmit },
|
||||
});
|
||||
const second = scheduleGatewaySigusr1Restart({
|
||||
delayMs: 1_000,
|
||||
reason: "second",
|
||||
emitHooks: { beforeEmit: latestBeforeEmit },
|
||||
});
|
||||
|
||||
expect(first.coalesced).toBe(false);
|
||||
expect(second.coalesced).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
|
||||
expect(firstBeforeEmit).not.toHaveBeenCalled();
|
||||
expect(latestBeforeEmit).toHaveBeenCalledTimes(1);
|
||||
expect(emitSpy).toHaveBeenCalledWith("SIGUSR1");
|
||||
} finally {
|
||||
process.removeListener("SIGUSR1", handler);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps existing preparation hook when a hookless restart coalesces", async () => {
|
||||
const beforeEmit = vi.fn(async () => {});
|
||||
const emitSpy = vi.spyOn(process, "emit");
|
||||
const handler = () => {};
|
||||
process.on("SIGUSR1", handler);
|
||||
try {
|
||||
scheduleGatewaySigusr1Restart({
|
||||
delayMs: 1_000,
|
||||
emitHooks: { beforeEmit },
|
||||
});
|
||||
const second = scheduleGatewaySigusr1Restart({
|
||||
delayMs: 1_000,
|
||||
reason: "hookless",
|
||||
});
|
||||
|
||||
expect(second.coalesced).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
|
||||
expect(beforeEmit).toHaveBeenCalledTimes(1);
|
||||
expect(emitSpy).toHaveBeenCalledWith("SIGUSR1");
|
||||
} finally {
|
||||
process.removeListener("SIGUSR1", handler);
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back prepared restart state when emission is rejected", async () => {
|
||||
const beforeEmit = vi.fn(async () => {});
|
||||
const afterEmitRejected = vi.fn(async () => {});
|
||||
|
||||
@@ -36,6 +36,7 @@ let lastRestartEmittedAt = 0;
|
||||
let pendingRestartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pendingRestartDueAt = 0;
|
||||
let pendingRestartReason: string | undefined;
|
||||
let pendingRestartEmitHooks: RestartEmitHooks | undefined;
|
||||
const activeDeferralPolls = new Set<ReturnType<typeof setInterval>>();
|
||||
|
||||
function hasUnconsumedRestartSignal(): boolean {
|
||||
@@ -49,6 +50,7 @@ function clearPendingScheduledRestart(): void {
|
||||
pendingRestartTimer = null;
|
||||
pendingRestartDueAt = 0;
|
||||
pendingRestartReason = undefined;
|
||||
pendingRestartEmitHooks = undefined;
|
||||
}
|
||||
|
||||
function clearActiveDeferralPolls(): void {
|
||||
@@ -202,6 +204,12 @@ export type RestartEmitHooks = {
|
||||
afterEmitRejected?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function updatePendingRestartEmitHooks(hooks?: RestartEmitHooks): void {
|
||||
if (hooks) {
|
||||
pendingRestartEmitHooks = hooks;
|
||||
}
|
||||
}
|
||||
|
||||
async function emitPreparedGatewayRestart(hooks?: RestartEmitHooks): Promise<void> {
|
||||
try {
|
||||
await hooks?.beforeEmit?.();
|
||||
@@ -482,6 +490,7 @@ export function scheduleGatewaySigusr1Restart(opts?: {
|
||||
restartLog.warn(
|
||||
`restart request coalesced (already scheduled) reason=${reason ?? "unspecified"} pendingReason=${pendingRestartReason ?? "unspecified"} delayMs=${remainingMs} ${formatRestartAudit(opts?.audit)}`,
|
||||
);
|
||||
updatePendingRestartEmitHooks(opts?.emitHooks);
|
||||
return {
|
||||
ok: true,
|
||||
pid: process.pid,
|
||||
@@ -497,21 +506,24 @@ export function scheduleGatewaySigusr1Restart(opts?: {
|
||||
|
||||
pendingRestartDueAt = requestedDueAt;
|
||||
pendingRestartReason = reason;
|
||||
pendingRestartEmitHooks = opts?.emitHooks;
|
||||
pendingRestartTimer = setTimeout(
|
||||
() => {
|
||||
pendingRestartTimer = null;
|
||||
pendingRestartDueAt = 0;
|
||||
pendingRestartReason = undefined;
|
||||
const emitHooks = pendingRestartEmitHooks;
|
||||
pendingRestartEmitHooks = undefined;
|
||||
const pendingCheck = preRestartCheck;
|
||||
if (!pendingCheck) {
|
||||
void emitPreparedGatewayRestart(opts?.emitHooks);
|
||||
void emitPreparedGatewayRestart(emitHooks);
|
||||
return;
|
||||
}
|
||||
const cfg = getRuntimeConfig();
|
||||
deferGatewayRestartUntilIdle({
|
||||
getPendingCount: pendingCheck,
|
||||
maxWaitMs: cfg.gateway?.reload?.deferralTimeoutMs,
|
||||
emitHooks: opts?.emitHooks,
|
||||
emitHooks,
|
||||
});
|
||||
},
|
||||
Math.max(0, requestedDueAt - nowMs),
|
||||
|
||||
Reference in New Issue
Block a user