fix(dashboard): avoid overlapping provider health polls (#4557)

Integrated into release/v3.8.33
This commit is contained in:
KooshaPari
2026-06-21 14:23:34 -07:00
committed by GitHub
parent 0b402e9bf3
commit 8fc331513d
2 changed files with 153 additions and 2 deletions

View File

@@ -1,12 +1,13 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import type {
ProviderBreakerSnapshot,
ConnectionCooldownSnapshot,
} from "@/app/(dashboard)/dashboard/combos/live/comboFlowModel";
const DEFAULT_POLL_MS = 5000;
const POLL_TIMEOUT_MS = 8000;
export interface ResilienceHealthSnapshot {
/** Per-provider circuit-breaker state (`providerHealth`). */
@@ -28,13 +29,26 @@ const EMPTY: ResilienceHealthSnapshot = { providerHealth: {}, connectionHealth:
*/
export function useProviderBreakerHealth(pollMs = DEFAULT_POLL_MS): ResilienceHealthSnapshot {
const [snapshot, setSnapshot] = useState<ResilienceHealthSnapshot>(EMPTY);
const inFlightRef = useRef<AbortController | null>(null);
useEffect(() => {
let cancelled = false;
const poll = async () => {
if (inFlightRef.current) return;
const controller = new AbortController();
inFlightRef.current = controller;
const timeoutId = setTimeout(
() => controller.abort("Provider breaker health timeout"),
POLL_TIMEOUT_MS
);
try {
const res = await fetch("/api/monitoring/health");
const res = await fetch("/api/monitoring/health", {
signal: controller.signal,
cache: "no-store",
});
if (!res.ok) return;
const json = (await res.json()) as {
providerHealth?: Record<string, ProviderBreakerSnapshot>;
@@ -53,6 +67,11 @@ export function useProviderBreakerHealth(pollMs = DEFAULT_POLL_MS): ResilienceHe
});
} catch {
// Fail-soft: keep the previous snapshot; cascade degrades to no badges.
} finally {
clearTimeout(timeoutId);
if (inFlightRef.current === controller) {
inFlightRef.current = null;
}
}
};
@@ -60,6 +79,8 @@ export function useProviderBreakerHealth(pollMs = DEFAULT_POLL_MS): ResilienceHe
const id = setInterval(poll, pollMs);
return () => {
cancelled = true;
inFlightRef.current?.abort("Provider breaker health unmounted");
inFlightRef.current = null;
clearInterval(id);
};
}, [pollMs]);

View File

@@ -0,0 +1,130 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useProviderBreakerHealth } from "../../../src/hooks/useProviderBreakerHealth";
type Deferred<T> = {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (error: unknown) => void;
};
function deferred<T>(): Deferred<T> {
let resolve!: (value: T) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
function okResponse(body: unknown): Response {
return {
ok: true,
json: async () => body,
} as Response;
}
describe("useProviderBreakerHealth", () => {
let root: Root | null = null;
let container: HTMLDivElement | null = null;
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
if (root) {
act(() => root?.unmount());
}
root = null;
container?.remove();
container = null;
document.body.innerHTML = "";
vi.useRealTimers();
vi.unstubAllGlobals();
});
it("skips overlapping health polls while a request is in flight", async () => {
const first = deferred<Response>();
const second = deferred<Response>();
const fetchMock = vi
.fn<typeof fetch>()
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise);
vi.stubGlobal("fetch", fetchMock);
function Probe() {
const snapshot = useProviderBreakerHealth(5000);
return <output>{Object.keys(snapshot.providerHealth).join(",")}</output>;
}
await act(async () => {
root = createRoot(container!);
root.render(<Probe />);
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({ cache: "no-store" });
await act(async () => {
vi.advanceTimersByTime(5000);
});
expect(fetchMock).toHaveBeenCalledTimes(1);
await act(async () => {
first.resolve(
okResponse({
providerHealth: { openai: { state: "closed" } },
connectionHealth: {},
})
);
await first.promise;
});
expect(container!.textContent).toBe("openai");
await act(async () => {
vi.advanceTimersByTime(5000);
});
expect(fetchMock).toHaveBeenCalledTimes(2);
second.resolve(okResponse({ providerHealth: {}, connectionHealth: {} }));
});
it("aborts an active health poll on unmount", async () => {
let signal: AbortSignal | undefined;
const fetchMock = vi.fn<typeof fetch>((_input, init) => {
signal = init?.signal ?? undefined;
return new Promise<Response>(() => {});
});
vi.stubGlobal("fetch", fetchMock);
function Probe() {
useProviderBreakerHealth(5000);
return null;
}
await act(async () => {
root = createRoot(container!);
root.render(<Probe />);
});
expect(signal?.aborted).toBe(false);
act(() => {
root?.unmount();
root = null;
});
expect(signal?.aborted).toBe(true);
});
});