From 8fc331513d26040e5fa65aa826604bae17b52411 Mon Sep 17 00:00:00 2001 From: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:23:34 -0700 Subject: [PATCH] fix(dashboard): avoid overlapping provider health polls (#4557) Integrated into release/v3.8.33 --- src/hooks/useProviderBreakerHealth.ts | 25 +++- .../ui/provider-breaker-health-hook.test.tsx | 130 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 tests/unit/ui/provider-breaker-health-hook.test.tsx diff --git a/src/hooks/useProviderBreakerHealth.ts b/src/hooks/useProviderBreakerHealth.ts index a0f5780889..e82c80fb2a 100644 --- a/src/hooks/useProviderBreakerHealth.ts +++ b/src/hooks/useProviderBreakerHealth.ts @@ -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(EMPTY); + const inFlightRef = useRef(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; @@ -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]); diff --git a/tests/unit/ui/provider-breaker-health-hook.test.tsx b/tests/unit/ui/provider-breaker-health-hook.test.tsx new file mode 100644 index 0000000000..855f8b16f4 --- /dev/null +++ b/tests/unit/ui/provider-breaker-health-hook.test.tsx @@ -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 = { + promise: Promise; + resolve: (value: T) => void; + reject: (error: unknown) => void; +}; + +function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((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(); + const second = deferred(); + const fetchMock = vi + .fn() + .mockReturnValueOnce(first.promise) + .mockReturnValueOnce(second.promise); + vi.stubGlobal("fetch", fetchMock); + + function Probe() { + const snapshot = useProviderBreakerHealth(5000); + return {Object.keys(snapshot.providerHealth).join(",")}; + } + + await act(async () => { + root = createRoot(container!); + root.render(); + }); + + 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((_input, init) => { + signal = init?.signal ?? undefined; + return new Promise(() => {}); + }); + vi.stubGlobal("fetch", fetchMock); + + function Probe() { + useProviderBreakerHealth(5000); + return null; + } + + await act(async () => { + root = createRoot(container!); + root.render(); + }); + + expect(signal?.aborted).toBe(false); + + act(() => { + root?.unmount(); + root = null; + }); + + expect(signal?.aborted).toBe(true); + }); +});