From 903f8b01f6b847cfdbf6ef9a77af532d25e7bc55 Mon Sep 17 00:00:00 2001 From: Morgan Wowk Date: Wed, 17 Dec 2025 13:26:01 -0800 Subject: [PATCH] fix: Querying data unreliable with custom backend **Changes:** * Clear backend query cache when backend settings change * Wait until we have determined the backend url fully before executing backend queries --- src/components/Home/RunSection/RunSection.tsx | 2 +- .../PipelineRun/RunDetails.test.tsx | 56 ++++++++++++++++++- .../TaskOverview/IOSection/IOSection.tsx | 5 +- .../FlowCanvas/TaskNode/TaskOverview/logs.tsx | 11 ++-- src/hooks/usePipelineRunData.ts | 17 +++--- src/hooks/useSubgraphBreadcrumbs.ts | 18 ++++-- src/providers/BackendProvider.tsx | 19 ++++++- src/services/executionService.ts | 13 +++-- src/utils/constants.ts | 4 ++ 9 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/components/Home/RunSection/RunSection.tsx b/src/components/Home/RunSection/RunSection.tsx index 2ec03c0ec..4a39f2f88 100644 --- a/src/components/Home/RunSection/RunSection.tsx +++ b/src/components/Home/RunSection/RunSection.tsx @@ -75,7 +75,7 @@ export const RunSection = ({ onEmptyList }: { onEmptyList?: () => void }) => { const { data, isLoading, isFetching, error, isFetched } = useQuery({ - queryKey: ["runs", backendUrl, pageToken, search.filter], + queryKey: ["runs", pageToken, search.filter], refetchOnWindowFocus: false, enabled: configured && available, queryFn: async () => { diff --git a/src/components/PipelineRun/RunDetails.test.tsx b/src/components/PipelineRun/RunDetails.test.tsx index 507e1ee61..cade9be6a 100644 --- a/src/components/PipelineRun/RunDetails.test.tsx +++ b/src/components/PipelineRun/RunDetails.test.tsx @@ -17,6 +17,7 @@ import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; import { ExecutionDataProvider } from "@/providers/ExecutionDataProvider"; import * as executionService from "@/services/executionService"; import type { ComponentSpec } from "@/utils/componentSpec"; +import { BACKEND_QUERY_KEY } from "@/utils/constants"; import { RunDetails } from "./RunDetails"; @@ -67,6 +68,8 @@ describe("", () => { }, }); + const MOCK_BACKEND_URL = "http://localhost:8000"; + const mockExecutionDetails: GetExecutionInfoResponse = { id: "test-execution-id", pipeline_run_id: "123", @@ -138,7 +141,10 @@ describe("", () => { error: null, }); - queryClient.setQueryData(["pipeline-run-metadata", "123"], mockPipelineRun); + queryClient.setQueryData( + [BACKEND_QUERY_KEY, "pipeline-run-metadata", "123"], + mockPipelineRun, + ); vi.mocked(executionService.countTaskStatuses).mockReturnValue({ total: 2, @@ -160,7 +166,7 @@ describe("", () => { configured: true, available: true, ready: true, - backendUrl: "http://localhost:8000", + backendUrl: MOCK_BACKEND_URL, isConfiguredFromEnv: false, isConfiguredFromRelativePath: false, setEnvConfig: vi.fn(), @@ -193,6 +199,50 @@ describe("", () => { }); }; + describe("Backend Configuration", () => { + test("should render run details when backend is configured", async () => { + // The default mock has configured: true and backendUrl: MOCK_BACKEND_URL + // act + renderWithProviders(); + + // assert + await waitFor(() => { + expect(screen.getByText("Test Pipeline")).toBeInTheDocument(); + expect(screen.getByText("Run Id:")).toBeInTheDocument(); + expect(screen.getByText("123")).toBeInTheDocument(); + }); + }); + + test("should render run details when backendUrl is empty string", async () => { + // arrange - simulate custom backend toggle disabled (empty backendUrl) + vi.mocked(useBackend).mockReturnValue({ + configured: true, + available: true, + ready: true, + backendUrl: "", + isConfiguredFromEnv: false, + isConfiguredFromRelativePath: true, + setEnvConfig: vi.fn(), + setRelativePathConfig: vi.fn(), + setBackendUrl: vi.fn(), + ping: vi.fn(), + }); + + // Query key no longer includes backendUrl - cache is shared regardless of URL + // and invalidated when backend URL changes + + // act + renderWithProviders(); + + // assert + await waitFor(() => { + expect(screen.getByText("Test Pipeline")).toBeInTheDocument(); + expect(screen.getByText("Run Id:")).toBeInTheDocument(); + expect(screen.getByText("123")).toBeInTheDocument(); + }); + }); + }); + describe("Inspect Pipeline Button", () => { test("should render inspect button when pipeline exists", async () => { // arrange @@ -282,7 +332,7 @@ describe("", () => { }; queryClient.setQueryData( - ["pipeline-run-metadata", "123"], + [BACKEND_QUERY_KEY, "pipeline-run-metadata", "123"], pipelineRunWithDifferentCreator, ); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOSection.tsx index 151458b8d..7d1b9aac5 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOSection.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOSection.tsx @@ -8,6 +8,7 @@ import { useBackend } from "@/providers/BackendProvider"; import { getExecutionArtifacts } from "@/services/executionService"; import { getBackendStatusString } from "@/utils/backend"; import type { TaskSpec } from "@/utils/componentSpec"; +import { BACKEND_QUERY_KEY } from "@/utils/constants"; import IOExtras from "./IOExtras"; import IOInputs from "./IOInputs"; @@ -28,9 +29,9 @@ const IOSection = ({ taskSpec, executionId, readOnly }: IOSectionProps) => { isFetching, error, } = useQuery({ - queryKey: ["artifacts", executionId], + queryKey: [BACKEND_QUERY_KEY, "artifacts", executionId], queryFn: () => getExecutionArtifacts(String(executionId), backendUrl), - enabled: !!executionId, + enabled: !!executionId && configured, }); if (!configured) { diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/logs.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/logs.tsx index fd028b843..17b914b08 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/logs.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/logs.tsx @@ -9,6 +9,7 @@ import { Spinner } from "@/components/ui/spinner"; import { useBackend } from "@/providers/BackendProvider"; import type { RunStatus } from "@/types/pipelineRun"; import { getBackendStatusString } from "@/utils/backend"; +import { BACKEND_QUERY_KEY } from "@/utils/constants"; const LogDisplay = ({ logs, @@ -114,10 +115,10 @@ const Logs = ({ log_text?: string; system_error_exception_full?: string; }>(); - const { data, isLoading, error, refetch } = useQuery({ - queryKey: ["logs", executionId], + const { data, isLoading, error } = useQuery({ + queryKey: [BACKEND_QUERY_KEY, "logs", executionId], queryFn: () => getLogs(String(executionId), backendUrl), - enabled: isLogging, + enabled: isLogging && configured, refetchInterval: 5000, refetchIntervalInBackground: false, }); @@ -142,10 +143,6 @@ const Logs = ({ } }, [data, error]); - useEffect(() => { - refetch(); - }, [backendUrl, refetch]); - if (!configured) { return ( diff --git a/src/hooks/usePipelineRunData.ts b/src/hooks/usePipelineRunData.ts index 0bd11cec4..c5fa19aab 100644 --- a/src/hooks/usePipelineRunData.ts +++ b/src/hooks/usePipelineRunData.ts @@ -10,11 +10,12 @@ import { getRunStatus, isStatusComplete, } from "@/services/executionService"; +import { BACKEND_QUERY_KEY } from "@/utils/constants"; const useRootExecutionId = (id: string) => { - const { backendUrl } = useBackend(); + const { backendUrl, configured } = useBackend(); const { data: rootExecutionId } = useQuery({ - queryKey: ["pipeline-run-execution-id", id], + queryKey: [BACKEND_QUERY_KEY, "pipeline-run-execution-id", id], queryFn: async () => { const rootExecutionId = await fetchPipelineRun(id, backendUrl) .then((res) => res.root_execution_id) @@ -27,7 +28,7 @@ const useRootExecutionId = (id: string) => { // assuming id is root_execution_id return id; }, - enabled: !!id && id.length > 0, + enabled: !!id && id.length > 0 && configured, staleTime: Infinity, }); @@ -36,13 +37,13 @@ const useRootExecutionId = (id: string) => { /* Accepts root_execution_id or run_id and returns execution details and state */ export const usePipelineRunData = (id: string) => { - const { backendUrl } = useBackend(); + const { backendUrl, configured } = useBackend(); const rootExecutionId = useRootExecutionId(id); const { data: executionDetails } = useQuery({ - enabled: !!rootExecutionId, - queryKey: ["execution-details", rootExecutionId], + enabled: !!rootExecutionId && configured, + queryKey: [BACKEND_QUERY_KEY, "execution-details", rootExecutionId], queryFn: async () => { if (!rootExecutionId) { throw new Error("No root execution id found"); @@ -58,8 +59,8 @@ export const usePipelineRunData = (id: string) => { error, isLoading, } = useQuery({ - enabled: !!rootExecutionId && !!executionDetails, - queryKey: ["pipeline-run", rootExecutionId], + enabled: !!rootExecutionId && !!executionDetails && configured, + queryKey: [BACKEND_QUERY_KEY, "pipeline-run", rootExecutionId], queryFn: async () => { if (!rootExecutionId) { throw new Error("No root execution id found"); diff --git a/src/hooks/useSubgraphBreadcrumbs.ts b/src/hooks/useSubgraphBreadcrumbs.ts index 30135625a..5dca327bd 100644 --- a/src/hooks/useSubgraphBreadcrumbs.ts +++ b/src/hooks/useSubgraphBreadcrumbs.ts @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { useBackend } from "@/providers/BackendProvider"; import { fetchExecutionDetails } from "@/services/executionService"; import { isGraphImplementationOutput } from "@/utils/componentSpec"; -import { ONE_MINUTE_IN_MS } from "@/utils/constants"; +import { BACKEND_QUERY_KEY, ONE_MINUTE_IN_MS } from "@/utils/constants"; export interface BreadcrumbSegment { taskId: string; @@ -27,11 +27,16 @@ export const useSubgraphBreadcrumbs = ( rootExecutionId: string | undefined, subgraphExecutionId: string | undefined, ): SubgraphBreadcrumbsResult => { - const { backendUrl } = useBackend(); + const { backendUrl, configured } = useBackend(); const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ - queryKey: ["subgraph-breadcrumbs", rootExecutionId, subgraphExecutionId], + queryKey: [ + BACKEND_QUERY_KEY, + "subgraph-breadcrumbs", + rootExecutionId, + subgraphExecutionId, + ], queryFn: async () => { if (!rootExecutionId || !subgraphExecutionId) { return { segments: [] }; @@ -44,7 +49,7 @@ export const useSubgraphBreadcrumbs = ( const segmentsInReverseOrder: BreadcrumbSegment[] = []; let currentExecutionId = subgraphExecutionId; let currentDetails = await queryClient.ensureQueryData({ - queryKey: ["execution-details", currentExecutionId], + queryKey: [BACKEND_QUERY_KEY, "execution-details", currentExecutionId], queryFn: () => fetchExecutionDetails(currentExecutionId, backendUrl), staleTime: ONE_MINUTE_IN_MS, }); @@ -57,7 +62,7 @@ export const useSubgraphBreadcrumbs = ( } const parentDetails = await queryClient.ensureQueryData({ - queryKey: ["execution-details", parentExecutionId], + queryKey: [BACKEND_QUERY_KEY, "execution-details", parentExecutionId], queryFn: () => fetchExecutionDetails(parentExecutionId, backendUrl), staleTime: ONE_MINUTE_IN_MS, }); @@ -95,7 +100,8 @@ export const useSubgraphBreadcrumbs = ( enabled: !!rootExecutionId && !!subgraphExecutionId && - rootExecutionId !== subgraphExecutionId, + rootExecutionId !== subgraphExecutionId && + configured, staleTime: ONE_MINUTE_IN_MS, retry: 1, }); diff --git a/src/providers/BackendProvider.tsx b/src/providers/BackendProvider.tsx index deee1b5ec..498301715 100644 --- a/src/providers/BackendProvider.tsx +++ b/src/providers/BackendProvider.tsx @@ -1,13 +1,15 @@ +import { useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useCallback, useEffect, useMemo, + useRef, useState, } from "react"; import useToastNotification from "@/hooks/useToastNotification"; -import { API_URL } from "@/utils/constants"; +import { API_URL, BACKEND_QUERY_KEY } from "@/utils/constants"; import { getUseEnv, getUserBackendUrl, @@ -45,6 +47,7 @@ const BackendContext = export const BackendProvider = ({ children }: { children: ReactNode }) => { const notify = useToastNotification(); + const queryClient = useQueryClient(); const backendUrlFromEnv = API_URL; @@ -55,6 +58,9 @@ export const BackendProvider = ({ children }: { children: ReactNode }) => { const [settingsLoaded, setSettingsLoaded] = useState(false); const [ready, setReady] = useState(false); + // Track the previous backend URL to detect changes + const previousBackendUrlRef = useRef(null); + let backendUrl = ""; if (useEnv && backendUrlFromEnv) { backendUrl = backendUrlFromEnv; @@ -133,6 +139,17 @@ export const BackendProvider = ({ children }: { children: ReactNode }) => { } }, [backendUrl, settingsLoaded]); + // Invalidate only backend-dependent queries when the backend URL changes + useEffect(() => { + if ( + previousBackendUrlRef.current !== null && + previousBackendUrlRef.current !== backendUrl + ) { + queryClient.invalidateQueries({ queryKey: [BACKEND_QUERY_KEY] }); + } + previousBackendUrlRef.current = backendUrl; + }, [backendUrl, queryClient]); + useEffect(() => { const getSettings = async () => { const url = await getUserBackendUrl(); diff --git a/src/services/executionService.ts b/src/services/executionService.ts index 0503ac42f..691c9faaf 100644 --- a/src/services/executionService.ts +++ b/src/services/executionService.ts @@ -11,6 +11,7 @@ import type { import { useBackend } from "@/providers/BackendProvider"; import type { RunStatus, TaskStatusCounts } from "@/types/pipelineRun"; import { + BACKEND_QUERY_KEY, DEFAULT_RATE_LIMIT_RPS, TWENTY_FOUR_HOURS_IN_MS, } from "@/utils/constants"; @@ -45,12 +46,12 @@ export const fetchPipelineRun = async ( }; export const useFetchPipelineRunMetadata = (runId: string | undefined) => { - const { backendUrl } = useBackend(); + const { backendUrl, configured } = useBackend(); return useQuery({ - queryKey: ["pipeline-run-metadata", runId], + queryKey: [BACKEND_QUERY_KEY, "pipeline-run-metadata", runId], queryFn: () => fetchPipelineRun(runId!, backendUrl), - enabled: !!runId, + enabled: !!runId && configured, refetchOnWindowFocus: false, staleTime: TWENTY_FOUR_HOURS_IN_MS, }); @@ -68,10 +69,12 @@ export const useFetchContainerExecutionState = ( executionId: string | undefined, backendUrl: string, ) => { + const { configured } = useBackend(); + return useQuery({ - queryKey: ["container-execution-state", executionId], + queryKey: [BACKEND_QUERY_KEY, "container-execution-state", executionId], queryFn: () => fetchContainerExecutionState(executionId!, backendUrl), - enabled: !!executionId, + enabled: !!executionId && configured, refetchOnWindowFocus: false, }); }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f1e92e023..90065061e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -80,3 +80,7 @@ export const ISO8601_DURATION_ZERO_DAYS = "P0D"; export const DEFAULT_RATE_LIMIT_RPS = 10; // requests per second export const MINUTES = 60 * 1000; + +// Query key prefix for backend-dependent queries +// All queries with this prefix are invalidated when the backend URL changes +export const BACKEND_QUERY_KEY = "backend";