refactor: health check and error handling (#3620)
* 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
This commit is contained in:
parent
9102c6272f
commit
59b6d8acc0
8 changed files with 128 additions and 100 deletions
|
|
@ -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. ";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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 =
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export function CustomLoadingPage() {
|
||||
return <></>;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 <></>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
@ -105,7 +19,11 @@ export function AppWrapperPage() {
|
|||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<>
|
||||
{modalErrorComponent}
|
||||
<GenericErrorComponent
|
||||
healthCheckTimeout={healthCheckTimeout}
|
||||
fetching={fetchingHealth}
|
||||
retry={refetch}
|
||||
/>
|
||||
<Outlet />
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue