Skip to content

Commit 15e055d

Browse files
samzongsteipete
authored andcommitted
fix(gateway): defer update check startup
1 parent 8e9d5c4 commit 15e055d

2 files changed

Lines changed: 176 additions & 22 deletions

File tree

src/gateway/server-startup-post-attach.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,102 @@ describe("startGatewayPostAttachRuntime", () => {
425425
expect(events).toEqual(["startup-log-start", "sidecars", "startup-log-end"]);
426426
});
427427

428+
it("starts the gateway update check after post-attach returns", async () => {
429+
const events: string[] = [];
430+
const stopUpdateCheck = vi.fn();
431+
const scheduleGatewayUpdateCheck = vi.fn(async () => {
432+
events.push("update-check");
433+
return stopUpdateCheck;
434+
});
435+
const startGatewaySidecars = vi.fn(async () => {
436+
events.push("sidecars");
437+
return { pluginServices: null, postReadySidecars: [] };
438+
});
439+
440+
const result = await startGatewayPostAttachRuntime(
441+
createPostAttachParams(),
442+
createPostAttachRuntimeDeps({
443+
refreshLatestUpdateRestartSentinel: vi.fn(async () => null),
444+
scheduleGatewayUpdateCheck,
445+
startGatewaySidecars,
446+
}),
447+
);
448+
events.push("returned");
449+
450+
expect(scheduleGatewayUpdateCheck).not.toHaveBeenCalled();
451+
expect(events).toEqual(["sidecars", "returned"]);
452+
453+
await vi.waitFor(() => {
454+
expect(scheduleGatewayUpdateCheck).toHaveBeenCalledTimes(1);
455+
});
456+
expect(events).toEqual(["sidecars", "returned", "update-check"]);
457+
458+
result.stopGatewayUpdateCheck();
459+
expect(stopUpdateCheck).toHaveBeenCalledTimes(1);
460+
});
461+
462+
it("stops the gateway update check if close wins the deferred startup race", async () => {
463+
let finishUpdateCheckSchedule: (() => void) | undefined;
464+
const stopUpdateCheck = vi.fn();
465+
const scheduleGatewayUpdateCheck = vi.fn(
466+
async () =>
467+
await new Promise<() => void>((resolve) => {
468+
finishUpdateCheckSchedule = () => resolve(stopUpdateCheck);
469+
}),
470+
);
471+
472+
const result = await startGatewayPostAttachRuntime(
473+
createPostAttachParams(),
474+
createPostAttachRuntimeDeps({
475+
refreshLatestUpdateRestartSentinel: vi.fn(async () => null),
476+
scheduleGatewayUpdateCheck,
477+
}),
478+
);
479+
480+
await vi.waitFor(() => {
481+
expect(scheduleGatewayUpdateCheck).toHaveBeenCalledTimes(1);
482+
});
483+
result.stopGatewayUpdateCheck();
484+
expect(stopUpdateCheck).not.toHaveBeenCalled();
485+
486+
if (!finishUpdateCheckSchedule) {
487+
throw new Error("Expected update check schedule release callback to be initialized");
488+
}
489+
finishUpdateCheckSchedule();
490+
491+
await vi.waitFor(() => {
492+
expect(stopUpdateCheck).toHaveBeenCalledTimes(1);
493+
});
494+
});
495+
496+
it("logs deferred gateway update check startup failures without failing ready", async () => {
497+
const log = { info: vi.fn(), warn: vi.fn() };
498+
const scheduleGatewayUpdateCheck = vi.fn(async () => {
499+
throw new Error("boom");
500+
});
501+
502+
await expect(
503+
startGatewayPostAttachRuntime(
504+
{
505+
...createPostAttachParams(),
506+
log,
507+
},
508+
createPostAttachRuntimeDeps({
509+
refreshLatestUpdateRestartSentinel: vi.fn(async () => null),
510+
scheduleGatewayUpdateCheck,
511+
}),
512+
),
513+
).resolves.toEqual(
514+
expect.objectContaining({
515+
stopGatewayUpdateCheck: expect.any(Function),
516+
}),
517+
);
518+
519+
await vi.waitFor(() => {
520+
expect(log.warn).toHaveBeenCalledWith("gateway update check failed to start: Error: boom");
521+
});
522+
});
523+
428524
it("skips heavy restart sentinel refresh when no sentinel file exists", async () => {
429525
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-no-sentinel-"));
430526
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);

src/gateway/server-startup-post-attach.ts

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,66 @@ const defaultGatewayPostAttachRuntimeDeps: GatewayPostAttachRuntimeDeps = {
745745
(await import("./server-tailscale.js")).startGatewayTailscaleExposure(...args),
746746
};
747747

748+
function createDeferredGatewayUpdateCheck(params: {
749+
startupTrace?: GatewayStartupTrace;
750+
runtimeDeps: GatewayPostAttachRuntimeDeps;
751+
cfg: OpenClawConfig;
752+
log: {
753+
info: (msg: string) => void;
754+
warn: (msg: string) => void;
755+
};
756+
isNixMode: boolean;
757+
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
758+
}): { start: () => void; stop: () => void } {
759+
let started = false;
760+
let stopped = false;
761+
let stopUpdateCheck: (() => void) | null = null;
762+
763+
const stop = () => {
764+
stopped = true;
765+
stopUpdateCheck?.();
766+
stopUpdateCheck = null;
767+
};
768+
769+
const start = () => {
770+
if (started || stopped) {
771+
return;
772+
}
773+
started = true;
774+
setImmediate(() => {
775+
if (stopped) {
776+
return;
777+
}
778+
void measureStartup(params.startupTrace, "post-attach.update-check", () =>
779+
params.runtimeDeps.scheduleGatewayUpdateCheck({
780+
cfg: params.cfg,
781+
log: params.log,
782+
isNixMode: params.isNixMode,
783+
onUpdateAvailableChange: (updateAvailable) => {
784+
const payload: GatewayUpdateAvailableEventPayload = { updateAvailable };
785+
params.broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
786+
},
787+
}),
788+
)
789+
.then((nextStop) => {
790+
if (stopped) {
791+
nextStop();
792+
return;
793+
}
794+
stopUpdateCheck = nextStop;
795+
})
796+
.catch((err) => {
797+
if (stopped) {
798+
return;
799+
}
800+
params.log.warn(`gateway update check failed to start: ${String(err)}`);
801+
});
802+
});
803+
};
804+
805+
return { start, stop };
806+
}
807+
748808
export async function startGatewayPostAttachRuntime(
749809
params: {
750810
minimalTestGateway: boolean;
@@ -833,19 +893,16 @@ export async function startGatewayPostAttachRuntime(
833893
}),
834894
);
835895

836-
const stopGatewayUpdateCheckPromise = params.minimalTestGateway
837-
? Promise.resolve(() => {})
838-
: measureStartup(params.startupTrace, "post-attach.update-check", () =>
839-
runtimeDeps.scheduleGatewayUpdateCheck({
840-
cfg: params.cfgAtStart,
841-
log: params.log,
842-
isNixMode: params.isNixMode,
843-
onUpdateAvailableChange: (updateAvailable) => {
844-
const payload: GatewayUpdateAvailableEventPayload = { updateAvailable };
845-
params.broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
846-
},
847-
}),
848-
);
896+
const updateCheck = params.minimalTestGateway
897+
? { start: () => {}, stop: () => {} }
898+
: createDeferredGatewayUpdateCheck({
899+
startupTrace: params.startupTrace,
900+
runtimeDeps,
901+
cfg: params.cfgAtStart,
902+
log: params.log,
903+
isNixMode: params.isNixMode,
904+
broadcast: params.broadcast,
905+
});
849906

850907
const tailscaleCleanupPromise = params.minimalTestGateway
851908
? Promise.resolve(null)
@@ -961,26 +1018,27 @@ export async function startGatewayPostAttachRuntime(
9611018
});
9621019

9631020
if (params.deferSidecars !== true) {
964-
const [, stopGatewayUpdateCheck, tailscaleCleanup, sidecarsResult] = await Promise.all([
1021+
const [, tailscaleCleanup, sidecarsResult] = await Promise.all([
9651022
startupLogPromise,
966-
stopGatewayUpdateCheckPromise,
9671023
tailscaleCleanupPromise,
9681024
sidecarsPromise,
9691025
]);
1026+
updateCheck.start();
9701027
return {
971-
stopGatewayUpdateCheck,
1028+
stopGatewayUpdateCheck: updateCheck.stop,
9721029
tailscaleCleanup,
9731030
pluginServices: sidecarsResult.pluginServices,
9741031
};
9751032
}
9761033

977-
const [, stopGatewayUpdateCheck, tailscaleCleanup] = await Promise.all([
978-
startupLogPromise,
979-
stopGatewayUpdateCheckPromise,
980-
tailscaleCleanupPromise,
981-
]);
1034+
const [, tailscaleCleanup] = await Promise.all([startupLogPromise, tailscaleCleanupPromise]);
1035+
updateCheck.start();
9821036

983-
return { stopGatewayUpdateCheck, tailscaleCleanup, pluginServices: reportedPluginServices };
1037+
return {
1038+
stopGatewayUpdateCheck: updateCheck.stop,
1039+
tailscaleCleanup,
1040+
pluginServices: reportedPluginServices,
1041+
};
9841042
}
9851043

9861044
export const __testing = {

0 commit comments

Comments
 (0)