mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 08:31:10 +00:00
fix(gateway): config hot-reload permanently disables after sporadic recovered watcher errors (#109682)
* fix(gateway): stop disabling config hot-reload after recovered watcher errors * test(gateway): prove watcher retry budget resets per episode --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -3804,6 +3804,63 @@ describe("startGatewayConfigReloader watcher error recovery", () => {
|
||||
await reloader.stop();
|
||||
});
|
||||
|
||||
it("keeps hot-reload active when transient errors are separated by working watchers", async () => {
|
||||
const originalVitest = process.env.VITEST;
|
||||
const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
|
||||
delete process.env.VITEST;
|
||||
delete process.env.CHOKIDAR_USEPOLLING;
|
||||
let reloader: { stop: () => Promise<void>; hotReloadStatus: () => string } | undefined;
|
||||
try {
|
||||
// One initial watcher plus one re-create per error/recovery round.
|
||||
const watchers = Array.from({ length: 5 }, () => createWatcherMock());
|
||||
const started = startReloaderWithWatchers(watchers);
|
||||
const { watchSpy, log } = started;
|
||||
reloader = started.reloader;
|
||||
const watchOptions = (index: number) =>
|
||||
watchSpy.mock.calls[index]?.[1] as { usePolling?: boolean } | undefined;
|
||||
|
||||
// The initial error consumes one attempt. Every replacement then proves
|
||||
// itself before failing, so each new episode must restart at attempt one.
|
||||
watchers[0]?.emit("error");
|
||||
expect(log.warn).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("re-creating watcher (attempt 1/3 in 500ms)"),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
|
||||
for (let round = 1; round < 4; round += 1) {
|
||||
watchers[round]?.emit("change");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
watchers[round]?.emit("error");
|
||||
expect(log.warn).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("re-creating watcher (attempt 1/3 in 500ms)"),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(watchSpy).toHaveBeenCalledTimes(round + 2);
|
||||
}
|
||||
|
||||
// Hot-reload survives in native mode: no polling degradation, no disable.
|
||||
expect(reloader.hotReloadStatus()).toBe("active");
|
||||
expect(log.warn).toHaveBeenCalledTimes(4);
|
||||
expect(watchOptions(4)?.usePolling).toBe(false);
|
||||
expect(log.warn).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("degrading to polling mode"),
|
||||
);
|
||||
expect(log.error).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalVitest === undefined) {
|
||||
delete process.env.VITEST;
|
||||
} else {
|
||||
process.env.VITEST = originalVitest;
|
||||
}
|
||||
if (originalChokidarPolling === undefined) {
|
||||
delete process.env.CHOKIDAR_USEPOLLING;
|
||||
} else {
|
||||
process.env.CHOKIDAR_USEPOLLING = originalChokidarPolling;
|
||||
}
|
||||
await reloader?.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("degrades to polling then disables after both native and polling retries are exhausted", async () => {
|
||||
const originalVitest = process.env.VITEST;
|
||||
const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
|
||||
|
||||
@@ -913,9 +913,15 @@ export function startGatewayConfigReloader(opts: {
|
||||
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
||||
usePolling,
|
||||
});
|
||||
next.on("add", scheduleExternalRefresh);
|
||||
next.on("change", scheduleExternalRefresh);
|
||||
next.on("unlink", scheduleExternalRefresh);
|
||||
// A file event proves this watcher recovered. Reset only here so plugin
|
||||
// metadata refreshes and consecutive watcher errors cannot refill the budget.
|
||||
const scheduleFromWatcherEvent = () => {
|
||||
watcherRecreateRetries = 0;
|
||||
scheduleExternalRefresh();
|
||||
};
|
||||
next.on("add", scheduleFromWatcherEvent);
|
||||
next.on("change", scheduleFromWatcherEvent);
|
||||
next.on("unlink", scheduleFromWatcherEvent);
|
||||
next.on("error", (err) => {
|
||||
handleWatcherError(next, err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user