Skip to content

Commit

Permalink
[web/lib/keyserver] avoid out of sync updatesCurrentAsOf between cl…
Browse files Browse the repository at this point in the history
…ient and keyserver

Summary:
This is fix to issue described in: [ENG-5906](https://linear.app/comm/issue/ENG-5906/web-app-stuck-in-crash-loop-even-after-refreshing#comment-11a5f277).

In D9948 I started to persist `updatesCurrentAsOf`.
I also updated `setInitialReduxState` action to rely on rehydrated data when it's a staff user.
What I missed is updating `currentAsOfPromise`, which for `updatesCurrentAsOf` was using server time [here](https://github.com/CommE2E/comm/blob/f4e61ed90d7f972909d2372a397ee3da5fdbd17e/keyserver/src/responders/redux-state-responders.js#L306).
This means that after reloading the app, the client (staff) has its own `updatesCurrentAsOf` from the previously received update (previous session) while the keyserver has newer `updatesCurrentAsOf` created while returning the initial state version.
This causes [this condition](https://github.com/CommE2E/comm/blob/f4e61ed90d7f972909d2372a397ee3da5fdbd17e/keyserver/src/socket/session-utils.js#L340C10-L340C10) to be `true`, and as a result was causing `FULL_STATE_SYNC` [here](https://github.com/CommE2E/comm/blob/f4e61ed90d7f972909d2372a397ee3da5fdbd17e/keyserver/src/socket/socket.js#L510C19-L510C19) which on prod kills the socket because of the amount of data to transfer.

This diff should make `updatesCurrentAsOf` synced between client and server.

This also fixes issue described [here](https://linear.app/comm/issue/ENG-5906/web-app-stuck-in-crash-loop-even-after-refreshing#comment-92c744ce). Previously, when `updatesCurrentAsOf` was just started to be persisted, it gets rehydrated with `undefined/0` value, this means after loading app all updates were started to be fetched. Right now if `updatesCurrentAsOf` is `0` we fetch everything in `InitialReduxStateResponder` and set `updatesCurrentAsOf` as `serverTime`.

This is only for staff, for standard users everything should work as previously.

Test Plan:
1. Reload app and make sure that there is no `FULL_STATE_SYNC`.
2. Make sure updates work properly.
3. Make sure `updatesCurrentAsOf` has correct value.
4. Make sure policies logic (which is related to `updatesCurrentAsOf` value) works.

Reviewers: inka, tomek, michal

Reviewed By: inka, michal

Subscribers: ashoat

Differential Revision: https://phab.comm.dev/D10070
  • Loading branch information
xsanm committed Nov 29, 2023
1 parent fd4dc67 commit 06cb756
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 18 deletions.
16 changes: 10 additions & 6 deletions keyserver/src/responders/redux-state-responders.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const initialReduxStateRequestValidator: TInterface<InitialReduxStateRequ
tShape<InitialReduxStateRequest>({
urlInfo: urlInfoValidator,
excludedData: excludedDataValidator,
clientUpdatesCurrentAsOf: t.Number,
});

const initialKeyserverInfoValidator = tShape<InitialKeyserverInfo>({
Expand All @@ -95,7 +96,7 @@ async function getInitialReduxStateResponder(
viewer: Viewer,
request: InitialReduxStateRequest,
): Promise<InitialReduxStateResponse> {
const { urlInfo, excludedData } = request;
const { urlInfo, excludedData, clientUpdatesCurrentAsOf } = request;
const useDatabase = viewer.loggedIn && canUseDatabaseOnWeb(viewer.userID);

const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies(
Expand Down Expand Up @@ -140,7 +141,10 @@ async function getInitialReduxStateResponder(
};
})();
const messageSelectionCriteria = { joinedThreads: true };
const initialTime = Date.now();
const serverUpdatesCurrentAsOf =
useDatabase && clientUpdatesCurrentAsOf
? clientUpdatesCurrentAsOf
: Date.now();

const threadInfoPromise = fetchThreadInfos(viewer);
const messageInfoPromise = fetchMessageInfos(
Expand All @@ -158,7 +162,7 @@ async function getInitialReduxStateResponder(
const sessionIDPromise = (async () => {
const calendarQuery = await calendarQueryPromise;
if (viewer.loggedIn) {
await setNewSession(viewer, calendarQuery, initialTime);
await setNewSession(viewer, calendarQuery, serverUpdatesCurrentAsOf);
}
return viewer.sessionID;
})();
Expand Down Expand Up @@ -197,7 +201,7 @@ async function getInitialReduxStateResponder(
{
[ashoatKeyserverID]: mostRecentMessageTimestamp(
rawMessageInfos,
initialTime,
serverUpdatesCurrentAsOf,
),
},
threadInfos,
Expand All @@ -219,7 +223,7 @@ async function getInitialReduxStateResponder(
return {
entryInfos: _keyBy('id')(rawEntryInfos),
daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos),
lastUserInteractionCalendar: initialTime,
lastUserInteractionCalendar: serverUpdatesCurrentAsOf,
};
})();
const userInfosPromise = (async () => {
Expand Down Expand Up @@ -303,7 +307,7 @@ async function getInitialReduxStateResponder(
})();
const currentAsOfPromise = (async () => {
const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise;
return hasNotAcknowledgedPolicies ? 0 : initialTime;
return hasNotAcknowledgedPolicies ? 0 : serverUpdatesCurrentAsOf;
})();

const pushApiPublicKeyPromise: Promise<?string> = (async () => {
Expand Down
15 changes: 15 additions & 0 deletions lib/selectors/keyserver-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ const updatesCurrentAsOfSelector: (
keyserverID: string,
) => (state: AppState) => number = _memoize(baseUpdatesCurrentAsOfSelector);

const allUpdatesCurrentAsOfSelector: (state: AppState) => {
+[keyserverID: string]: number,
} = createSelector(
(state: AppState) => state.keyserverStore.keyserverInfos,
(infos: { +[key: string]: KeyserverInfo }) => {
const allUpdatesCurrentAsOf: { [string]: number } = {};
for (const keyserverID in infos) {
allUpdatesCurrentAsOf[keyserverID] =
infos[keyserverID].updatesCurrentAsOf;
}
return allUpdatesCurrentAsOf;
},
);

const baseCurrentAsOfSelector: (
keyserverID: string,
) => (state: AppState) => number = keyserverID => (state: AppState) =>
Expand Down Expand Up @@ -153,4 +167,5 @@ export {
deviceTokensSelector,
deviceTokenSelector,
selectedKeyserversSelector,
allUpdatesCurrentAsOfSelector,
};
32 changes: 27 additions & 5 deletions web/redux/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { defaultCalendarFilters } from 'lib/types/filter-types.js';
import { extractKeyserverIDFromID } from 'lib/utils/action-utils.js';
import { useKeyserverCall } from 'lib/utils/keyserver-call.js';
import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js';
import type { URLInfo } from 'lib/utils/url-utils.js';
import { ashoatKeyserverID } from 'lib/utils/validation-utils.js';

import type {
ExcludedData,
InitialReduxState,
InitialReduxStateResponse,
InitialKeyserverInfo,
Expand All @@ -19,22 +21,42 @@ export const updateWindowActiveActionType = 'UPDATE_WINDOW_ACTIVE';
export const setInitialReduxState = 'SET_INITIAL_REDUX_STATE';

const getInitialReduxStateCallServerEndpointOptions = { timeout: 300000 };

type GetInitialReduxStateInput = {
+urlInfo: URLInfo,
+excludedData: ExcludedData,
+allUpdatesCurrentAsOf: {
+[keyserverID: string]: number,
},
};
const getInitialReduxState =
(
callKeyserverEndpoint: CallKeyserverEndpoint,
allKeyserverIDs: $ReadOnlyArray<string>,
): ((input: InitialReduxStateRequest) => Promise<InitialReduxState>) =>
): ((input: GetInitialReduxStateInput) => Promise<InitialReduxState>) =>
async input => {
const requests: { [string]: InitialReduxStateRequest } = {};
const { urlInfo, excludedData } = input;
const { urlInfo, excludedData, allUpdatesCurrentAsOf } = input;
const { thread, inviteSecret, ...rest } = urlInfo;
const threadKeyserverID = thread ? extractKeyserverIDFromID(thread) : null;

for (const keyserverID of allKeyserverIDs) {
const clientUpdatesCurrentAsOf = allUpdatesCurrentAsOf[keyserverID];
const keyserverExcludedData: ExcludedData = {
threadStore: !!excludedData.threadStore && !!clientUpdatesCurrentAsOf,
};
if (keyserverID === threadKeyserverID) {
requests[keyserverID] = { urlInfo, excludedData };
requests[keyserverID] = {
urlInfo,
excludedData: keyserverExcludedData,
clientUpdatesCurrentAsOf,
};
} else {
requests[keyserverID] = { urlInfo: rest, excludedData };
requests[keyserverID] = {
urlInfo: rest,
excludedData: keyserverExcludedData,
clientUpdatesCurrentAsOf,
};
}
}

Expand Down Expand Up @@ -142,7 +164,7 @@ const getInitialReduxState =
};

function useGetInitialReduxState(): (
input: InitialReduxStateRequest,
input: GetInitialReduxStateInput,
) => Promise<InitialReduxState> {
return useKeyserverCall(getInitialReduxState);
}
Expand Down
10 changes: 8 additions & 2 deletions web/redux/initial-state-gate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Persistor } from 'redux-persist/es/types';

import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js';
import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js';
import { allUpdatesCurrentAsOfSelector } from 'lib/selectors/keyserver-selectors.js';
import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js';
import type { RawThreadInfo } from 'lib/types/thread-types.js';
import { convertIDToNewSchema } from 'lib/utils/migration-utils.js';
Expand Down Expand Up @@ -42,6 +43,8 @@ function InitialReduxStateGate(props: Props): React.Node {
}, [initError]);

const isRehydrated = useSelector(state => !!state._persist?.rehydrated);
const allUpdatesCurrentAsOf = useSelector(allUpdatesCurrentAsOfSelector);

const prevIsRehydrated = React.useRef(false);
React.useEffect(() => {
if (!prevIsRehydrated.current && isRehydrated) {
Expand All @@ -60,7 +63,10 @@ function InitialReduxStateGate(props: Props): React.Node {

const payload = await callGetInitialReduxState({
urlInfo,
excludedData: { threadStore: !!clientDBStore.threadStore },
excludedData: {
threadStore: !!clientDBStore.threadStore,
},
allUpdatesCurrentAsOf,
});

const currentLoggedInUserID = payload.currentUserInfo?.anonymous
Expand Down Expand Up @@ -113,7 +119,7 @@ function InitialReduxStateGate(props: Props): React.Node {
}
})();
}
}, [callGetInitialReduxState, dispatch, isRehydrated]);
}, [callGetInitialReduxState, dispatch, isRehydrated, allUpdatesCurrentAsOf]);

const initialStateLoaded = useSelector(state => state.initialStateLoaded);

Expand Down
5 changes: 0 additions & 5 deletions web/redux/redux-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import baseReducer from 'lib/reducers/master-reducer.js';
import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { invalidSessionDowngrade } from 'lib/shared/session-utils.js';
import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js';
import type { Shape } from 'lib/types/core.js';
import type { CryptoStore } from 'lib/types/crypto-types.js';
import type { DraftStore } from 'lib/types/draft-types.js';
Expand Down Expand Up @@ -126,13 +125,9 @@ export function reducer(oldState: AppState | void, action: Action): AppState {
const { userInfos, keyserverInfos, ...rest } = action.payload;
const newKeyserverInfos = { ...state.keyserverStore.keyserverInfos };
for (const keyserverID in keyserverInfos) {
const newUpdatesCurrentAsOf = canUseDatabaseOnWeb(rest.currentUserInfo.id)
? newKeyserverInfos[keyserverID].updatesCurrentAsOf
: keyserverInfos[keyserverID].updatesCurrentAsOf;
newKeyserverInfos[keyserverID] = {
...newKeyserverInfos[keyserverID],
...keyserverInfos[keyserverID],
updatesCurrentAsOf: newUpdatesCurrentAsOf,
};
}
return validateStateAndProcessDBOperations(
Expand Down
1 change: 1 addition & 0 deletions web/types/redux-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ export type ExcludedData = {
export type InitialReduxStateRequest = {
+urlInfo: URLInfo,
+excludedData: ExcludedData,
+clientUpdatesCurrentAsOf: number,
};

0 comments on commit 06cb756

Please sign in to comment.