Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43b812da7b | |||
| 5670c008e1 | |||
| f9325772bd |
@@ -66,7 +66,7 @@ test.describe('Intel GPU plugin smoke tests', () => {
|
||||
});
|
||||
|
||||
await page.goto('/c/main/intel-gpu/nodes');
|
||||
await expect(page.getByRole('heading', { name: /node/i })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('heading', { name: /intel gpu.*nodes/i })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await page.goto('/c/main/intel-gpu/pods');
|
||||
await expect(page.getByRole('heading', { name: /pod/i })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -151,4 +151,21 @@ describe('IntelGpuDataProvider', () => {
|
||||
expect(callCountAfter).toBeGreaterThan(callCountBefore);
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a hanging CRD request as unavailable after 2s timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(ApiProxy.request).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.crdAvailable).toBe(false);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, 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);
|
||||
|
||||
@@ -106,13 +106,11 @@ describe('MetricsPage', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows loader when ctxLoading=true but heading is visible immediately', () => {
|
||||
it('shows loader when ctxLoading=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
// fetchGpuMetrics should never be called in loading state
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(null);
|
||||
render(<MetricsPage />);
|
||||
// Heading renders immediately, loader appears below it while waiting for context
|
||||
expect(screen.getByText('Intel GPU — Metrics')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Intel GPU data...');
|
||||
});
|
||||
|
||||
|
||||
@@ -230,6 +230,10 @@ export default function MetricsPage() {
|
||||
};
|
||||
}, [ctxLoading, fetchSeq]);
|
||||
|
||||
if (ctxLoading) {
|
||||
return <Loader title="Loading Intel GPU data..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -243,7 +247,7 @@ export default function MetricsPage() {
|
||||
<SectionHeader title="Intel GPU — Metrics" />
|
||||
<button
|
||||
onClick={() => void doFetch()}
|
||||
disabled={fetching || ctxLoading}
|
||||
disabled={fetching}
|
||||
aria-label="Refresh metrics"
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
@@ -251,18 +255,15 @@ export default function MetricsPage() {
|
||||
color: 'var(--mui-palette-primary-main, #0071c5)',
|
||||
border: '1px solid var(--mui-palette-primary-main, #0071c5)',
|
||||
borderRadius: '4px',
|
||||
cursor: fetching || ctxLoading ? 'not-allowed' : 'pointer',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
opacity: fetching || ctxLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{fetching ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ctxLoading && <Loader title="Loading Intel GPU data..." />}
|
||||
|
||||
<MetricRequirements />
|
||||
|
||||
{fetching && !metrics && <Loader title="Querying Prometheus for GPU metrics..." />}
|
||||
|
||||
Reference in New Issue
Block a user