From 66575982afb9c08962e48bd768ff83f0c13c003b Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Wed, 25 Mar 2026 05:57:15 +0000 Subject: [PATCH 1/3] fix: add request timeout wrapper to prevent E2E test hang Add withTimeout() helper that wraps ApiProxy.request calls with a 2s timeout. This prevents the plugin from hanging indefinitely when CRD requests fail or network issues occur in the E2E environment. Root cause: ApiProxy.request to non-existent CRDs would hang forever, causing the Loading Intel GPU data... progressbar to never resolve. Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index bf724a9..6ae9a4c 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,10 @@ 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); From 52b1429ba0ef77c7624a7ee622b1c4a008d761ea Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:18:19 +0000 Subject: [PATCH 2/3] fix: reformat withTimeout call and add unit test for timeout behavior - Reformat withTimeout call to single line (prettier) - Add unit test for CRD timeout behavior (crdAvailable=false when API fails) Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 25 +++++++++++++++++++++++++ src/api/IntelGpuDataContext.tsx | 5 +---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 631a395..4eb9750 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -151,4 +151,29 @@ 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).mockRejectedValue( + new Error('Request timed out after 2000ms') + ); + + const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(100); + }); + expect(result.current.loading).toBe(false); + expect(result.current.crdAvailable).toBe(false); + vi.useRealTimers(); + }); }); diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index 6ae9a4c..bd4beb8 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -154,10 +154,7 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await withTimeout( - ApiProxy.request(url), - DEFAULT_REQUEST_TIMEOUT_MS - ); + const list = await withTimeout(ApiProxy.request(url), DEFAULT_REQUEST_TIMEOUT_MS); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods); From 957cf144a746224cf4704fd8c17d29dda075fdcc Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:21:22 +0000 Subject: [PATCH 3/3] fix: reapply formatting after rebase Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 4eb9750..dc61f85 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -155,17 +155,9 @@ describe('IntelGpuDataProvider', () => { 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).mockRejectedValue( - new Error('Request timed out after 2000ms') - ); + 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).mockRejectedValue(new Error('Request timed out after 2000ms')); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });