Skip to content

Commit 4ed6526

Browse files
author
Chris Bobbe
committed
eventActions.js: Clarify loop-break logic and fix race condition in startEventPolling.
This follows a discussion at and around https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/Event.20polling.20optimization.3F/near/810784. The `queueId !== getState().session.eventQueueId` is not the most direct way to check that the same user has remained logged in. So, replace that check and the `auth` check above it with a call to the new helper function authEquivalent, to check the maybe-stale auth against the current auth. Also, the `queueId !== ...` check allows a race condition! We change `session.eventQueueId` at `REALM_INIT`, which only happens after a `/register` request *completes*. So a user could switch accounts, triggering a new `/register` request, but the old account's event poll could return before that new `/register` completes, and the `queueId !== ...` will pass, triggering a state update with the old account's events instead of the new account's. Checking auth information is what we want here. It's true that this auth check will not catch the case that a queue was killed following a period of inactivity (i.e., the BAD_EVENT_QUEUE_ID). However, this is not a flaw, because the loop already handles that case correctly without the `queueId !== ...` check. In particular, if a queue is killed, api.pollForEvents will reject, and that rejection is handled in the catch block. When modifying startEventPolling to accept an auth parameter, I noticed (and added a comment to explain) that some actions being dispatched in doInitialFetch will throw unhandled errors if a user logs out while the /register request is in progress. This will be of concern when we work on #3706.
1 parent b16a0c8 commit 4ed6526

File tree

2 files changed

+25
-9
lines changed

2 files changed

+25
-9
lines changed

src/events/eventActions.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* @flow strict-local */
22
import { batchActions } from 'redux-batched-actions';
33

4-
import type { Action, Dispatch, GeneralEvent, GetState, GlobalState } from '../types';
4+
import type { Auth, Action, Dispatch, GeneralEvent, GetState, GlobalState } from '../types';
55
import * as api from '../api';
66
import { logout } from '../account/accountActions';
77
import { deadQueue } from '../session/sessionActions';
@@ -11,6 +11,7 @@ import { tryGetAuth } from '../selectors';
1111
import actionCreator from '../actionCreator';
1212
import progressiveTimeout from '../utils/progressiveTimeout';
1313
import { ApiError } from '../api/apiErrors';
14+
import { authEquivalent } from '../account/accountMisc';
1415

1516
/** Convert an `/events` response into a sequence of our Redux actions. */
1617
export const responseToActions = (
@@ -44,12 +45,13 @@ export const dispatchOrBatch = (dispatch: Dispatch, actions: $ReadOnlyArray<Acti
4445
};
4546

4647
/**
47-
* Poll an event queue on the Zulip server for updates, in a loop.
48+
* Poll an event queue on the Zulip server for updates, in a loop, as long as
49+
* the user remains logged into the same account.
4850
*
4951
* This is part of our use of the Zulip events system; see `doInitialFetch`
5052
* for discussion.
5153
*/
52-
export const startEventPolling = (queueId: number, eventId: number) => async (
54+
export const startEventPolling = (auth: Auth, queueId: number, eventId: number) => async (
5355
dispatch: Dispatch,
5456
getState: GetState,
5557
) => {
@@ -58,17 +60,18 @@ export const startEventPolling = (queueId: number, eventId: number) => async (
5860
/* eslint-disable no-await-in-loop */
5961
// eslint-disable-next-line no-constant-condition
6062
while (true) {
61-
const auth = tryGetAuth(getState());
62-
if (!auth) {
63-
// User switched accounts or logged out
63+
if (!authEquivalent(auth, tryGetAuth(getState()))) {
64+
// The user logged out or switched accounts during progressiveTimeout (see catch block, below).
65+
// (If tryGetAuth returns undefined, the user is not logged in.)
6466
break;
6567
}
6668

6769
try {
6870
const { events } = await api.pollForEvents(auth, queueId, lastEventId);
6971

70-
// User switched accounts or logged out
71-
if (queueId !== getState().session.eventQueueId) {
72+
if (!authEquivalent(auth, tryGetAuth(getState()))) {
73+
// The user logged out or switched accounts while api.pollForEvents was in progress.
74+
// (If tryGetAuth returns undefined, the user is not logged in.)
7275
break;
7376
}
7477

src/message/fetchActions.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Narrow, Dispatch, GetState, GlobalState, Message, Action } from '.
33
import * as api from '../api';
44
import {
55
getAuth,
6+
tryGetAuth,
67
getSession,
78
getFirstMessageId,
89
getLastMessageId,
@@ -242,7 +243,19 @@ export const doInitialFetch = () => async (dispatch: Dispatch, getState: GetStat
242243
dispatch(realmInit(initData));
243244
dispatch(fetchTopMostNarrow());
244245
dispatch(initialFetchComplete());
245-
dispatch(startEventPolling(initData.queue_id, initData.last_event_id));
246+
247+
const authNow = tryGetAuth(getState());
248+
if (authNow !== undefined) {
249+
// The user may have logged out while api.registerForEvents was in progress.
250+
// If so, don't start polling events.
251+
252+
// TODO: If the user did log out during that time, other actions that are dispatched
253+
// here (in doInitialFetch, after api.registerForEvents) will throw an error,
254+
// which may be a cause of #3706! These include the calls to fetchTopMostNarrow,
255+
// fetchPrivateMessages, and fetchMessagesInNarrow, because they all dispatch
256+
// fetchMessages, which calls getAuth, which throws with 'Active account not logged in'.
257+
dispatch(startEventPolling(authNow, initData.queue_id, initData.last_event_id));
258+
}
246259

247260
dispatch(fetchPrivateMessages());
248261

0 commit comments

Comments
 (0)