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:
Lucas Oliveira 2024-08-29 16:40:21 -03:00 committed by GitHub
commit 59b6d8acc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 128 additions and 100 deletions

View file

@ -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. ";

View file

@ -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,
});

View file

@ -0,0 +1,3 @@
export function CustomLoadingPage() {
return <></>;
}

View file

@ -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();

View file

@ -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 />}
</>
);

View file

@ -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 <></>;
}
}

View file

@ -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 };
}

View file

@ -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>