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.";
|
"Check if everything is working properly and try again.";
|
||||||
|
|
||||||
export const TIMEOUT_ERROR_MESSAGE =
|
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 TIMEOUT_ERROR_DESCRIPION = "Server is busy.";
|
||||||
|
|
||||||
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
|
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@ interface getHealthResponse {
|
||||||
variables: string;
|
variables: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface getHealthParams {
|
||||||
|
enableInterval?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const useGetHealthQuery: useQueryFunctionType<
|
export const useGetHealthQuery: useQueryFunctionType<
|
||||||
undefined,
|
getHealthParams,
|
||||||
getHealthResponse
|
getHealthResponse
|
||||||
> = (options) => {
|
> = (params, options) => {
|
||||||
const { query } = UseRequestProcessor();
|
const { query } = UseRequestProcessor();
|
||||||
const setHealthCheckTimeout = useUtilityStore(
|
const setHealthCheckTimeout = useUtilityStore(
|
||||||
(state) => state.setHealthCheckTimeout,
|
(state) => state.setHealthCheckTimeout,
|
||||||
|
|
@ -40,9 +44,13 @@ export const useGetHealthQuery: useQueryFunctionType<
|
||||||
setTimeout(() => reject(createNewError503()), SERVER_HEALTH_INTERVAL),
|
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]);
|
const response = await Promise.race([apiPromise, timeoutPromise]);
|
||||||
setHealthCheckTimeout(null);
|
setHealthCheckTimeout(
|
||||||
|
Object.values(response.data).some((value) => value !== "ok")
|
||||||
|
? "serverDown"
|
||||||
|
: null,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isServerBusy =
|
const isServerBusy =
|
||||||
|
|
@ -60,7 +68,9 @@ export const useGetHealthQuery: useQueryFunctionType<
|
||||||
|
|
||||||
const queryResult = query(["useGetHealthQuery"], getHealthFn, {
|
const queryResult = query(["useGetHealthQuery"], getHealthFn, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
refetchInterval: REFETCH_SERVER_HEALTH_INTERVAL,
|
refetchInterval: params.enableInterval
|
||||||
|
? REFETCH_SERVER_HEALTH_INTERVAL
|
||||||
|
: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function CustomLoadingPage() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { UseRequestProcessor } from "@/controllers/API/services/request-processor";
|
import { UseRequestProcessor } from "@/controllers/API/services/request-processor";
|
||||||
import { useQueryFunctionType } from "@/types/api";
|
import { useQueryFunctionType } from "@/types/api";
|
||||||
|
|
||||||
export const usePrimaryLoading: useQueryFunctionType<undefined, null> = (
|
export const useCustomPrimaryLoading: useQueryFunctionType<undefined, null> = (
|
||||||
options,
|
options,
|
||||||
) => {
|
) => {
|
||||||
const { query } = UseRequestProcessor();
|
const { query } = UseRequestProcessor();
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useGetAutoLogin } from "@/controllers/API/queries/auth";
|
import { useGetAutoLogin } from "@/controllers/API/queries/auth";
|
||||||
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
|
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
|
||||||
import { useGetVersionQuery } from "@/controllers/API/queries/version";
|
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 { useDarkStore } from "@/stores/darkStore";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
@ -13,7 +14,8 @@ export function AppInitPage() {
|
||||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||||
|
|
||||||
const { isFetched: isLoaded } = usePrimaryLoading();
|
const { isFetched: isLoaded } = useCustomPrimaryLoading();
|
||||||
|
|
||||||
const { isFetched } = useGetAutoLogin({ enabled: isLoaded });
|
const { isFetched } = useGetAutoLogin({ enabled: isLoaded });
|
||||||
useGetVersionQuery({ enabled: isFetched });
|
useGetVersionQuery({ enabled: isFetched });
|
||||||
useGetConfig({ enabled: isFetched });
|
useGetConfig({ enabled: isFetched });
|
||||||
|
|
@ -35,7 +37,11 @@ export function AppInitPage() {
|
||||||
return (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
<>
|
<>
|
||||||
{(isLoading || !isFetched) && <LoadingPage overlay />}
|
{isLoaded ? (
|
||||||
|
(isLoading || !isFetched) && <LoadingPage overlay />
|
||||||
|
) : (
|
||||||
|
<CustomLoadingPage />
|
||||||
|
)}
|
||||||
{isFetched && <Outlet />}
|
{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 AlertDisplayArea from "@/alerts/displayArea";
|
||||||
import CrashErrorComponent from "@/components/crashErrorComponent";
|
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 { 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 { ErrorBoundary } from "react-error-boundary";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { GenericErrorComponent } from "./components/GenericErrorComponent";
|
||||||
|
import { useHealthCheck } from "./hooks/use-health-check";
|
||||||
|
|
||||||
export function AppWrapperPage() {
|
export function AppWrapperPage() {
|
||||||
const healthCheckMaxRetries = useFlowsManagerStore(
|
const { healthCheckTimeout, fetchingHealth, refetch } = useHealthCheck();
|
||||||
(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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
|
|
@ -105,7 +19,11 @@ export function AppWrapperPage() {
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{modalErrorComponent}
|
<GenericErrorComponent
|
||||||
|
healthCheckTimeout={healthCheckTimeout}
|
||||||
|
fetching={fetchingHealth}
|
||||||
|
retry={refetch}
|
||||||
|
/>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</>
|
</>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue