Skip to content

Commit 40093f5

Browse files
committed
fix(gateway): track effective watcher polling mode
1 parent d085143 commit 40093f5

2 files changed

Lines changed: 53 additions & 8 deletions

File tree

src/gateway/config-reload.test.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -574,9 +574,11 @@ describe("resolveGatewayReloadSettings", () => {
574574
type WatcherHandler = () => void;
575575
type WatcherEvent = "add" | "change" | "unlink" | "error";
576576

577-
function createWatcherMock() {
577+
function createWatcherMock(effectiveUsePolling?: boolean) {
578578
const handlers = new Map<WatcherEvent, WatcherHandler[]>();
579579
return {
580+
effectiveUsePolling,
581+
options: { usePolling: false },
580582
on(event: WatcherEvent, handler: WatcherHandler) {
581583
const existing = handlers.get(event) ?? [];
582584
existing.push(handler);
@@ -1602,9 +1604,15 @@ describe("startGatewayConfigReloader watcher error recovery", () => {
16021604

16031605
function startReloaderWithWatchers(watchers: ReturnType<typeof createWatcherMock>[]) {
16041606
const watchSpy = vi.spyOn(chokidar, "watch");
1605-
for (const watcher of watchers) {
1606-
watchSpy.mockReturnValueOnce(watcher as unknown as never);
1607-
}
1607+
let watcherIndex = 0;
1608+
watchSpy.mockImplementation((_path, options) => {
1609+
const watcher = watchers[watcherIndex++];
1610+
if (!watcher) {
1611+
throw new Error("missing watcher mock");
1612+
}
1613+
watcher.options.usePolling = watcher.effectiveUsePolling ?? Boolean(options?.usePolling);
1614+
return watcher as unknown as never;
1615+
});
16081616
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
16091617
const reloader = startGatewayConfigReloader({
16101618
initialConfig: { gateway: { reload: { debounceMs: 0 } } },
@@ -1676,9 +1684,7 @@ describe("startGatewayConfigReloader watcher error recovery", () => {
16761684
// Fourth native error triggers degradation to polling mode (not disabled).
16771685
watchers[3]?.emit("error");
16781686
expect(reloader.hotReloadStatus()).toBe("active");
1679-
expect(log.warn).toHaveBeenCalledWith(
1680-
expect.stringContaining("degrading to polling mode"),
1681-
);
1687+
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("degrading to polling mode"));
16821688
await vi.advanceTimersByTimeAsync(500);
16831689
expect(watchSpy).toHaveBeenCalledTimes(5);
16841690
expect(watchOptions(4)?.usePolling).toBe(true);
@@ -1772,6 +1778,45 @@ describe("startGatewayConfigReloader watcher error recovery", () => {
17721778
}
17731779
});
17741780

1781+
it("uses chokidar's effective polling mode when the platform forces it on", async () => {
1782+
const originalVitest = process.env.VITEST;
1783+
const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
1784+
delete process.env.VITEST;
1785+
delete process.env.CHOKIDAR_USEPOLLING;
1786+
let reloader: { stop: () => Promise<void>; hotReloadStatus: () => string } | undefined;
1787+
try {
1788+
const watchers = Array.from({ length: 4 }, () => createWatcherMock(true));
1789+
const started = startReloaderWithWatchers(watchers);
1790+
const { log } = started;
1791+
reloader = started.reloader;
1792+
const backoffs = [500, 2000, 5000] as const;
1793+
1794+
for (let index = 0; index < watchers.length - 1; index += 1) {
1795+
watchers[index]?.emit("error");
1796+
await vi.advanceTimersByTimeAsync(backoffs[index] ?? 0);
1797+
}
1798+
watchers.at(-1)?.emit("error");
1799+
1800+
expect(reloader.hotReloadStatus()).toBe("disabled");
1801+
expect(log.warn).not.toHaveBeenCalledWith(
1802+
expect.stringContaining("degrading to polling mode"),
1803+
);
1804+
expect(log.error).toHaveBeenCalledWith(expect.stringContaining("in polling mode"));
1805+
} finally {
1806+
if (originalVitest === undefined) {
1807+
delete process.env.VITEST;
1808+
} else {
1809+
process.env.VITEST = originalVitest;
1810+
}
1811+
if (originalChokidarPolling === undefined) {
1812+
delete process.env.CHOKIDAR_USEPOLLING;
1813+
} else {
1814+
process.env.CHOKIDAR_USEPOLLING = originalChokidarPolling;
1815+
}
1816+
await reloader?.stop();
1817+
}
1818+
});
1819+
17751820
it("does not report polling fallback when chokidar polling is forced off", async () => {
17761821
const originalVitest = process.env.VITEST;
17771822
const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;

src/gateway/config-reload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ export function startGatewayConfigReloader(opts: {
443443
handleWatcherError(next, err);
444444
});
445445
watcher = next;
446-
watcherUsesPolling = usePolling;
446+
watcherUsesPolling = next.options.usePolling;
447447
hotReloadStatus = "active";
448448
};
449449

0 commit comments

Comments
 (0)