Skip to content

Commit

Permalink
refactor: health check and error handling (langflow-ai#3620)
Browse files Browse the repository at this point in the history
* Refactored getHealth to get if any value of health check is not ok

* Added custom health check

* Created generic error component to display error popups

* added useHealthCheck hook

* Updated wrapper page to use health check hook

* Removed custom error

* Added custom loading page for when custom primary loading is not done

* Changed health check to be disabled when flow is building or any request is pending

* Changed text of ttimeout error
  • Loading branch information
lucaseduoli authored Aug 29, 2024
1 parent 9102c62 commit 59b6d8a
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 100 deletions.
2 changes: 1 addition & 1 deletion src/frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ export const FETCH_ERROR_DESCRIPION =
"Check if everything is working properly and try again.";

export const TIMEOUT_ERROR_MESSAGE =
"Please wait a few seconds to server process your request.";
"Please wait a few moments while the server processes your request.";
export const TIMEOUT_ERROR_DESCRIPION = "Server is busy.";

export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
Expand Down
20 changes: 15 additions & 5 deletions src/frontend/src/controllers/API/queries/health/use-get-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ interface getHealthResponse {
variables: string;
}

interface getHealthParams {
enableInterval?: boolean;
}

export const useGetHealthQuery: useQueryFunctionType<
undefined,
getHealthParams,
getHealthResponse
> = (options) => {
> = (params, options) => {
const { query } = UseRequestProcessor();
const setHealthCheckTimeout = useUtilityStore(
(state) => state.setHealthCheckTimeout,
Expand All @@ -40,9 +44,13 @@ export const useGetHealthQuery: useQueryFunctionType<
setTimeout(() => reject(createNewError503()), SERVER_HEALTH_INTERVAL),
);

const apiPromise = api.get<{ data: getHealthResponse }>("/health");
const apiPromise = api.get<getHealthResponse>("/health");
const response = await Promise.race([apiPromise, timeoutPromise]);
setHealthCheckTimeout(null);
setHealthCheckTimeout(
Object.values(response.data).some((value) => value !== "ok")
? "serverDown"
: null,
);
return response.data;
} catch (error) {
const isServerBusy =
Expand All @@ -60,7 +68,9 @@ export const useGetHealthQuery: useQueryFunctionType<

const queryResult = query(["useGetHealthQuery"], getHealthFn, {
placeholderData: keepPreviousData,
refetchInterval: REFETCH_SERVER_HEALTH_INTERVAL,
refetchInterval: params.enableInterval
? REFETCH_SERVER_HEALTH_INTERVAL
: false,
retry: false,
...options,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function CustomLoadingPage() {
return <></>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UseRequestProcessor } from "@/controllers/API/services/request-processor";
import { useQueryFunctionType } from "@/types/api";

export const usePrimaryLoading: useQueryFunctionType<undefined, null> = (
export const useCustomPrimaryLoading: useQueryFunctionType<undefined, null> = (
options,
) => {
const { query } = UseRequestProcessor();
Expand Down
12 changes: 9 additions & 3 deletions src/frontend/src/pages/AppInitPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useGetAutoLogin } from "@/controllers/API/queries/auth";
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
import { useGetVersionQuery } from "@/controllers/API/queries/version";
import { usePrimaryLoading } from "@/customization/hooks/use-primary-loading";
import { CustomLoadingPage } from "@/customization/components/custom-loading-page";
import { useCustomPrimaryLoading } from "@/customization/hooks/use-custom-primary-loading";
import { useDarkStore } from "@/stores/darkStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useEffect } from "react";
Expand All @@ -13,7 +14,8 @@ export function AppInitPage() {
const refreshStars = useDarkStore((state) => state.refreshStars);
const isLoading = useFlowsManagerStore((state) => state.isLoading);

const { isFetched: isLoaded } = usePrimaryLoading();
const { isFetched: isLoaded } = useCustomPrimaryLoading();

const { isFetched } = useGetAutoLogin({ enabled: isLoaded });
useGetVersionQuery({ enabled: isFetched });
useGetConfig({ enabled: isFetched });
Expand All @@ -35,7 +37,11 @@ export function AppInitPage() {
return (
//need parent component with width and height
<>
{(isLoading || !isFetched) && <LoadingPage overlay />}
{isLoaded ? (
(isLoading || !isFetched) && <LoadingPage overlay />
) : (
<CustomLoadingPage />
)}
{isFetched && <Outlet />}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import FetchErrorComponent from "@/components/fetchErrorComponent";
import TimeoutErrorComponent from "@/components/timeoutErrorComponent";
import {
FETCH_ERROR_DESCRIPION,
FETCH_ERROR_MESSAGE,
TIMEOUT_ERROR_DESCRIPION,
TIMEOUT_ERROR_MESSAGE,
} from "@/constants/constants";

export function GenericErrorComponent({ healthCheckTimeout, fetching, retry }) {
switch (healthCheckTimeout) {
case "serverDown":
return (
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
openModal={true}
setRetry={retry}
isLoadingHealth={fetching}
></FetchErrorComponent>
);
case "timeout":
return (
<TimeoutErrorComponent
description={TIMEOUT_ERROR_MESSAGE}
message={TIMEOUT_ERROR_DESCRIPION}
openModal={true}
setRetry={retry}
isLoadingHealth={fetching}
></TimeoutErrorComponent>
);
default:
return <></>;
}
}
56 changes: 56 additions & 0 deletions src/frontend/src/pages/AppWrapperPage/hooks/use-health-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useGetHealthQuery } from "@/controllers/API/queries/health";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { useIsFetching, useIsMutating } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useEffect, useState } from "react";

export function useHealthCheck() {
const healthCheckMaxRetries = useFlowsManagerStore(
(state) => state.healthCheckMaxRetries,
);

const healthCheckTimeout = useUtilityStore(
(state) => state.healthCheckTimeout,
);

const isMutating = useIsMutating();
const isFetching = useIsFetching({
predicate: (query) => query.queryKey[0] !== "useGetHealthQuery",
});
const isBuilding = useFlowStore((state) => state.isBuilding);

const disabled = isMutating || isFetching || isBuilding;

const {
isFetching: fetchingHealth,
isError: isErrorHealth,
error,
refetch,
} = useGetHealthQuery({ enableInterval: !disabled });
const [retryCount, setRetryCount] = useState(0);

useEffect(() => {
const isServerBusy =
(error as AxiosError)?.response?.status === 503 ||
(error as AxiosError)?.response?.status === 429;

if (isServerBusy && isErrorHealth && !disabled) {
const maxRetries = healthCheckMaxRetries;
if (retryCount < maxRetries) {
const delay = Math.pow(2, retryCount) * 1000;
const timer = setTimeout(() => {
refetch();
setRetryCount(retryCount + 1);
}, delay);

return () => clearTimeout(timer);
}
} else {
setRetryCount(0);
}
}, [isErrorHealth, retryCount, refetch]);

return { healthCheckTimeout, refetch, fetchingHealth };
}
98 changes: 8 additions & 90 deletions src/frontend/src/pages/AppWrapperPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,99 +1,13 @@
import AlertDisplayArea from "@/alerts/displayArea";
import CrashErrorComponent from "@/components/crashErrorComponent";
import FetchErrorComponent from "@/components/fetchErrorComponent";
import TimeoutErrorComponent from "@/components/timeoutErrorComponent";
import {
FETCH_ERROR_DESCRIPION,
FETCH_ERROR_MESSAGE,
TIMEOUT_ERROR_DESCRIPION,
TIMEOUT_ERROR_MESSAGE,
} from "@/constants/constants";
import { useGetHealthQuery } from "@/controllers/API/queries/health";
import { CustomHeader } from "@/customization/components/custom-header";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { AxiosError } from "axios";
import { useEffect, useMemo, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Outlet } from "react-router-dom";
import { GenericErrorComponent } from "./components/GenericErrorComponent";
import { useHealthCheck } from "./hooks/use-health-check";

export function AppWrapperPage() {
const healthCheckMaxRetries = useFlowsManagerStore(
(state) => state.healthCheckMaxRetries,
);

const healthCheckTimeout = useUtilityStore(
(state) => state.healthCheckTimeout,
);

const {
data: healthData,
isFetching: fetchingHealth,
isError: isErrorHealth,
error,
refetch,
} = useGetHealthQuery();

const isServerDown =
isErrorHealth ||
(healthData && Object.values(healthData).some((value) => value !== "ok")) ||
healthCheckTimeout === "serverDown";

const isTimeoutResponseServer = healthCheckTimeout === "timeout";

const [retryCount, setRetryCount] = useState(0);

useEffect(() => {
const isServerBusy =
(error as AxiosError)?.response?.status === 503 ||
(error as AxiosError)?.response?.status === 429;

if (isServerBusy && isErrorHealth) {
const maxRetries = healthCheckMaxRetries;
if (retryCount < maxRetries) {
const delay = Math.pow(2, retryCount) * 1000;
const timer = setTimeout(() => {
refetch();
setRetryCount(retryCount + 1);
}, delay);

return () => clearTimeout(timer);
}
} else {
setRetryCount(0);
}
}, [isErrorHealth, retryCount, refetch]);

const modalErrorComponent = useMemo(() => {
switch (healthCheckTimeout) {
case "serverDown":
return (
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
openModal={isServerDown}
setRetry={() => {
refetch();
}}
isLoadingHealth={fetchingHealth}
></FetchErrorComponent>
);
case "timeout":
return (
<TimeoutErrorComponent
description={TIMEOUT_ERROR_MESSAGE}
message={TIMEOUT_ERROR_DESCRIPION}
openModal={isTimeoutResponseServer}
setRetry={() => {
refetch();
}}
isLoadingHealth={fetchingHealth}
></TimeoutErrorComponent>
);
default:
return null;
}
}, [healthCheckTimeout, fetchingHealth]);
const { healthCheckTimeout, fetchingHealth, refetch } = useHealthCheck();

return (
<div className="flex h-full flex-col">
Expand All @@ -105,7 +19,11 @@ export function AppWrapperPage() {
FallbackComponent={CrashErrorComponent}
>
<>
{modalErrorComponent}
<GenericErrorComponent
healthCheckTimeout={healthCheckTimeout}
fetching={fetchingHealth}
retry={refetch}
/>
<Outlet />
</>
</ErrorBoundary>
Expand Down

0 comments on commit 59b6d8a

Please sign in to comment.