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

View file

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

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

View file

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

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