diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 631a395..d9e97bf 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -151,4 +151,27 @@ describe('IntelGpuDataProvider', () => { expect(callCountAfter).toBeGreaterThan(callCountBefore); }); }); + + it('treats a hanging CRD request as unavailable after 2s timeout', async () => { + vi.useFakeTimers(); + const nodeWrapper = { jsonData: {} }; + vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(ApiProxy.request) + .mockReturnValueOnce(new Promise(() => {})) + .mockResolvedValueOnce({ items: [] }) + .mockResolvedValueOnce({ items: [] }) + .mockResolvedValueOnce({ items: [] }); + + const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); + + expect(result.current.loading).toBe(true); + + vi.advanceTimersByTime(2000); + await act(async () => {}); + expect(result.current.crdAvailable).toBe(false); + expect(result.current.loading).toBe(false); + + vi.useRealTimers(); + }); }); diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index bf724a9..bd4beb8 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -69,6 +69,18 @@ export function useIntelGpuContext(): IntelGpuContextValue { // Helpers // --------------------------------------------------------------------------- +const DEFAULT_REQUEST_TIMEOUT_MS = 2_000; + +/** Wraps a promise with a timeout, rejecting if it doesn't settle within ms. */ +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms) + ), + ]); +} + /** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */ const extractJsonData = (items: unknown[]): unknown[] => items.map(item => @@ -108,8 +120,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } try { // GpuDevicePlugin CRDs — graceful degradation if CRD not installed try { - const pluginList = await ApiProxy.request( - `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + const pluginList = await withTimeout( + ApiProxy.request( + `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + ), + DEFAULT_REQUEST_TIMEOUT_MS ); if (!cancelled && isKubeList(pluginList)) { setCrdAvailable(true); @@ -139,7 +154,7 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await ApiProxy.request(url); + const list = await withTimeout(ApiProxy.request(url), DEFAULT_REQUEST_TIMEOUT_MS); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods);