Fixed SSE connection retry infinite loop (#17723)
This bug was cause by a combination of an expired token stored inside SSE client on the front end, and a server restart or a cache flush, we ended up with an SSE client that tries to reconnect infinitely with an expired token. The fix is to improve the connection retry handler in the frontend to detect an expired token and recreate an SSE client.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS } from '@/sse-db-event/constants/SseConnectionRetryMaxWaitTimeInMs';
|
||||
import { SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_FOR_DEV_MODE } from '@/sse-db-event/constants/SseConnectionRetryWaitTimeInMsForDevMode';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { useHandleSseClientConnectionRetry } from '@/sse-db-event/hooks/useHandleSseClientConnectionRetry';
|
||||
import { activeQueryListenersState } from '@/sse-db-event/states/activeQueryListenersState';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
@@ -11,13 +10,11 @@ import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { getIsDevelopmentEnvironment } from '~/utils/getIsDevelopmentEnvironment';
|
||||
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const SSEClientEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const [sseClient, setSseClient] = useRecoilState(sseClientState);
|
||||
const [tokenPair] = useRecoilState(tokenPairState);
|
||||
|
||||
const handleSSEClientConnected = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
@@ -34,9 +31,11 @@ export const SSEClientEffect = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const { handleSseClientConnectionRetry } =
|
||||
useHandleSseClientConnectionRetry();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && !isDefined(sseClient)) {
|
||||
const tokenPair = getTokenPair();
|
||||
if (isLoggedIn && !isDefined(sseClient) && isDefined(tokenPair)) {
|
||||
const token = tokenPair?.accessOrWorkspaceAgnosticToken?.token;
|
||||
|
||||
const newSseClient = createClient({
|
||||
@@ -48,24 +47,20 @@ export const SSEClientEffect = () => {
|
||||
connected: handleSSEClientConnected,
|
||||
},
|
||||
retryAttempts: Infinity,
|
||||
retry: async () => {
|
||||
const randomWaitTimeInMsToSpaceAllClientsReconnection = Math.round(
|
||||
Math.random() * SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS,
|
||||
);
|
||||
|
||||
const isDevelopmentEnvironment = getIsDevelopmentEnvironment();
|
||||
|
||||
const waitTimeInMs = isDevelopmentEnvironment
|
||||
? SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_FOR_DEV_MODE
|
||||
: randomWaitTimeInMsToSpaceAllClientsReconnection;
|
||||
|
||||
await sleep(waitTimeInMs);
|
||||
},
|
||||
retry: (retryCount: number) =>
|
||||
handleSseClientConnectionRetry(retryCount, token),
|
||||
});
|
||||
|
||||
setSseClient(newSseClient);
|
||||
}
|
||||
}, [handleSSEClientConnected, isLoggedIn, setSseClient, sseClient]);
|
||||
}, [
|
||||
handleSSEClientConnected,
|
||||
isLoggedIn,
|
||||
setSseClient,
|
||||
sseClient,
|
||||
tokenPair,
|
||||
handleSseClientConnectionRetry,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useTriggerEventStreamDestroy } from '@/sse-db-event/hooks/useTriggerEve
|
||||
import { isCreatingSseEventStreamState } from '@/sse-db-event/states/isCreatingSseEventStreamState';
|
||||
import { isDestroyingEventStreamState } from '@/sse-db-event/states/isDestroyingEventStreamState';
|
||||
import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestroyEventStreamState';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { sseEventStreamIdState } from '@/sse-db-event/states/sseEventStreamIdState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
@@ -17,7 +16,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey, OnboardingStatus } from '~/generated/graphql';
|
||||
|
||||
export const SSEEventStreamEffect = () => {
|
||||
const sseClient = useRecoilValue(sseClientState);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const sseEventStreamId = useRecoilValue(sseEventStreamIdState);
|
||||
@@ -40,9 +38,7 @@ export const SSEEventStreamEffect = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const isSseClientAvailabble =
|
||||
isDefined(sseClient) &&
|
||||
!isCreatingSseEventStream &&
|
||||
!isDestroyingEventStream;
|
||||
!isCreatingSseEventStream && !isDestroyingEventStream;
|
||||
|
||||
const willCreateEventStream =
|
||||
isSseClientAvailabble &&
|
||||
@@ -62,7 +58,7 @@ export const SSEEventStreamEffect = () => {
|
||||
if (willDestroyEventStream) {
|
||||
triggerEventStreamDestroy();
|
||||
} else if (willCreateEventStream) {
|
||||
triggerEventStreamCreation(sseClient);
|
||||
triggerEventStreamCreation();
|
||||
}
|
||||
}, [
|
||||
isCreatingSseEventStream,
|
||||
@@ -72,7 +68,6 @@ export const SSEEventStreamEffect = () => {
|
||||
isSseDbEventsEnabled,
|
||||
isDestroyingEventStream,
|
||||
triggerEventStreamDestroy,
|
||||
sseClient,
|
||||
shouldDestroyEventStream,
|
||||
sseEventStreamId,
|
||||
objectMetadataItems,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS = 1_000;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS } from '@/sse-db-event/constants/SseConnectionRetryMaxWaitTimeInMs';
|
||||
import { SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_FOR_DEV_MODE } from '@/sse-db-event/constants/SseConnectionRetryWaitTimeInMsForDevMode';
|
||||
import { SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS } from '@/sse-db-event/constants/SseConnectionRetryWaitTimeInMsToAvoidRaceConditions';
|
||||
import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestroyEventStreamState';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getIsDevelopmentEnvironment } from '~/utils/getIsDevelopmentEnvironment';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const useHandleSseClientConnectionRetry = () => {
|
||||
const handleSseClientConnectionRetry = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (retryCount: number, initialTokenForSseClient: string) => {
|
||||
const sseClient = getSnapshotValue(snapshot, sseClientState);
|
||||
|
||||
if (!isDefined(sseClient)) {
|
||||
await sleep(
|
||||
SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenPair = getSnapshotValue(snapshot, tokenPairState);
|
||||
const currentAppToken =
|
||||
tokenPair?.accessOrWorkspaceAgnosticToken?.token;
|
||||
|
||||
const shouldResetSseClient =
|
||||
!isDefined(currentAppToken) ||
|
||||
currentAppToken !== initialTokenForSseClient ||
|
||||
retryCount > 10;
|
||||
|
||||
if (shouldResetSseClient) {
|
||||
await sleep(
|
||||
SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS,
|
||||
);
|
||||
|
||||
sseClient.dispose();
|
||||
set(shouldDestroyEventStreamState, true);
|
||||
set(sseClientState, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const randomWaitTimeInMsToSpaceAllClientsReconnection = Math.round(
|
||||
Math.random() * SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS,
|
||||
);
|
||||
|
||||
const isDevelopmentEnvironment = getIsDevelopmentEnvironment();
|
||||
|
||||
const waitTimeInMs = isDevelopmentEnvironment
|
||||
? SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_FOR_DEV_MODE
|
||||
: randomWaitTimeInMsToSpaceAllClientsReconnection;
|
||||
|
||||
await sleep(waitTimeInMs);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { handleSseClientConnectionRetry };
|
||||
};
|
||||
+4
-2
@@ -5,12 +5,12 @@ import { disposeFunctionForEventStreamState } from '@/sse-db-event/states/dispos
|
||||
import { isCreatingSseEventStreamState } from '@/sse-db-event/states/isCreatingSseEventStreamState';
|
||||
import { isDestroyingEventStreamState } from '@/sse-db-event/states/isDestroyingEventStreamState';
|
||||
import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestroyEventStreamState';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { sseEventStreamIdState } from '@/sse-db-event/states/sseEventStreamIdState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { captureException } from '@sentry/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
import { type Client } from 'graphql-sse';
|
||||
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -30,7 +30,9 @@ export const useTriggerEventStreamCreation = () => {
|
||||
|
||||
const triggerEventStreamCreation = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(sseClient: Client) => {
|
||||
() => {
|
||||
const sseClient = snapshot.getLoadable(sseClientState).getValue();
|
||||
|
||||
const isCreatingSseEventStream = snapshot
|
||||
.getLoadable(isCreatingSseEventStreamState)
|
||||
.getValue();
|
||||
|
||||
Reference in New Issue
Block a user