Skip to content

Commit 0d0e576

Browse files
committed
store [nfc]: Move reload-store logic to enclosing whole poll method
As is, this structure looks a bit silly. But in the next few commits we'll make use of it in order to recover from a wider range of errors in the event-poll loop, by using the same remedy of reloading server data and replacing the store as we use when the event queue expires.
1 parent 2fe1eb3 commit 0d0e576

File tree

1 file changed

+115
-104
lines changed

1 file changed

+115
-104
lines changed

lib/model/store.dart

Lines changed: 115 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,119 +1002,130 @@ class UpdateMachine {
10021002

10031003
void poll() async {
10041004
assert(!_disposed);
1005-
1006-
BackoffMachine? backoffMachine;
1007-
int accumulatedTransientFailureCount = 0;
1008-
1009-
/// This only reports transient errors after reaching
1010-
/// a pre-defined threshold of retries.
1011-
void maybeReportToUserTransientError(Object error) {
1012-
accumulatedTransientFailureCount++;
1013-
if (accumulatedTransientFailureCount > transientFailureCountNotifyThreshold) {
1014-
_reportToUserErrorConnectingToServer(error);
1015-
}
1016-
}
1017-
1018-
while (true) {
1019-
if (_debugLoopSignal != null) {
1020-
await _debugLoopSignal!.future;
1021-
if (_disposed) return;
1022-
assert(() {
1023-
_debugLoopSignal = Completer();
1024-
return true;
1025-
}());
1005+
try {
1006+
BackoffMachine? backoffMachine;
1007+
int accumulatedTransientFailureCount = 0;
1008+
1009+
/// This only reports transient errors after reaching
1010+
/// a pre-defined threshold of retries.
1011+
void maybeReportToUserTransientError(Object error) {
1012+
accumulatedTransientFailureCount++;
1013+
if (accumulatedTransientFailureCount > transientFailureCountNotifyThreshold) {
1014+
_reportToUserErrorConnectingToServer(error);
1015+
}
10261016
}
10271017

1028-
final GetEventsResult result;
1029-
try {
1030-
result = await getEvents(store.connection,
1031-
queueId: queueId, lastEventId: lastEventId);
1032-
if (_disposed) return;
1033-
} catch (e) {
1034-
if (_disposed) return;
1035-
1036-
store.isLoading = true;
1037-
bool isUnexpected;
1038-
bool shouldReportToUser;
1039-
switch (e) {
1040-
case ZulipApiException(code: 'BAD_EVENT_QUEUE_ID'):
1041-
assert(debugLog('Lost event queue for $store. Replacing…'));
1042-
// This disposes the store, which disposes this update machine.
1043-
await store._globalStore._reloadPerAccount(store.accountId);
1044-
assert(debugLog('… Event queue replaced.'));
1045-
return;
1046-
1047-
case NetworkException(cause: SocketException()):
1048-
// A [SocketException] is common when the app returns from sleep.
1049-
isUnexpected = false;
1050-
shouldReportToUser = false;
1051-
1052-
case NetworkException():
1053-
case Server5xxException():
1054-
isUnexpected = false;
1055-
shouldReportToUser = true;
1056-
1057-
case ServerException(httpStatus: 429):
1058-
case ZulipApiException(httpStatus: 429):
1059-
case ZulipApiException(code: 'RATE_LIMIT_HIT'):
1060-
// TODO(#946) handle rate-limit errors more generally, in ApiConnection
1061-
isUnexpected = false;
1062-
shouldReportToUser = true;
1063-
1064-
default:
1065-
isUnexpected = true;
1066-
shouldReportToUser = true;
1018+
while (true) {
1019+
if (_debugLoopSignal != null) {
1020+
await _debugLoopSignal!.future;
1021+
if (_disposed) return;
1022+
assert(() {
1023+
_debugLoopSignal = Completer();
1024+
return true;
1025+
}());
10671026
}
10681027

1069-
if (isUnexpected) {
1070-
assert(shouldReportToUser);
1071-
assert(debugLog('Error polling event queue for $store: $e\n'
1072-
'Backing off and retrying even though may be hopeless…'));
1073-
// TODO(#186): Handle unrecoverable failures
1074-
_reportToUserErrorConnectingToServer(e);
1075-
} else {
1076-
assert(debugLog('Transient error polling event queue for $store: $e\n'
1077-
'Backing off, then will retry…'));
1078-
if (shouldReportToUser) {
1079-
maybeReportToUserTransientError(e);
1028+
final GetEventsResult result;
1029+
try {
1030+
result = await getEvents(store.connection,
1031+
queueId: queueId, lastEventId: lastEventId);
1032+
if (_disposed) return;
1033+
} catch (e) {
1034+
if (_disposed) return;
1035+
1036+
store.isLoading = true;
1037+
bool isUnexpected;
1038+
bool shouldReportToUser;
1039+
switch (e) {
1040+
case ZulipApiException(code: 'BAD_EVENT_QUEUE_ID'):
1041+
rethrow;
1042+
1043+
case NetworkException(cause: SocketException()):
1044+
// A [SocketException] is common when the app returns from sleep.
1045+
isUnexpected = false;
1046+
shouldReportToUser = false;
1047+
1048+
case NetworkException():
1049+
case Server5xxException():
1050+
isUnexpected = false;
1051+
shouldReportToUser = true;
1052+
1053+
case ServerException(httpStatus: 429):
1054+
case ZulipApiException(httpStatus: 429):
1055+
case ZulipApiException(code: 'RATE_LIMIT_HIT'):
1056+
// TODO(#946) handle rate-limit errors more generally, in ApiConnection
1057+
isUnexpected = false;
1058+
shouldReportToUser = true;
1059+
1060+
default:
1061+
isUnexpected = true;
1062+
shouldReportToUser = true;
10801063
}
1064+
1065+
if (isUnexpected) {
1066+
assert(shouldReportToUser);
1067+
assert(debugLog('Error polling event queue for $store: $e\n'
1068+
'Backing off and retrying even though may be hopeless…'));
1069+
// TODO(#186): Handle unrecoverable failures
1070+
_reportToUserErrorConnectingToServer(e);
1071+
} else {
1072+
assert(debugLog('Transient error polling event queue for $store: $e\n'
1073+
'Backing off, then will retry…'));
1074+
if (shouldReportToUser) {
1075+
maybeReportToUserTransientError(e);
1076+
}
1077+
}
1078+
await (backoffMachine ??= BackoffMachine()).wait();
1079+
if (_disposed) return;
1080+
assert(debugLog('… Backoff wait complete, retrying poll.'));
1081+
continue;
10811082
}
1082-
await (backoffMachine ??= BackoffMachine()).wait();
1083-
if (_disposed) return;
1084-
assert(debugLog('… Backoff wait complete, retrying poll.'));
1085-
continue;
1086-
}
10871083

1088-
// After one successful request, we reset backoff to its initial state.
1089-
// That way if the user is off the network and comes back on, the app
1090-
// doesn't wind up in a state where it's slow to recover the next time
1091-
// one request fails.
1092-
//
1093-
// This does mean that if the server is having trouble and handling some
1094-
// but not all of its requests, we'll end up doing a lot more retries than
1095-
// if we stayed at the max backoff interval; partway toward what would
1096-
// happen if we weren't backing off at all.
1097-
//
1098-
// But at least for [getEvents] requests, as here, it should be OK,
1099-
// because this is a long-poll. That means a typical successful request
1100-
// takes a long time to come back; in fact longer than our max backoff
1101-
// duration (which is 10 seconds). So if we're getting a mix of successes
1102-
// and failures, the successes themselves should space out the requests.
1103-
backoffMachine = null;
1104-
1105-
store.isLoading = false;
1106-
// Dismiss existing errors, if any.
1107-
reportErrorToUserBriefly(null);
1108-
accumulatedTransientFailureCount = 0;
1109-
1110-
final events = result.events;
1111-
for (final event in events) {
1112-
await store.handleEvent(event);
1113-
if (_disposed) return;
1084+
// After one successful request, we reset backoff to its initial state.
1085+
// That way if the user is off the network and comes back on, the app
1086+
// doesn't wind up in a state where it's slow to recover the next time
1087+
// one request fails.
1088+
//
1089+
// This does mean that if the server is having trouble and handling some
1090+
// but not all of its requests, we'll end up doing a lot more retries than
1091+
// if we stayed at the max backoff interval; partway toward what would
1092+
// happen if we weren't backing off at all.
1093+
//
1094+
// But at least for [getEvents] requests, as here, it should be OK,
1095+
// because this is a long-poll. That means a typical successful request
1096+
// takes a long time to come back; in fact longer than our max backoff
1097+
// duration (which is 10 seconds). So if we're getting a mix of successes
1098+
// and failures, the successes themselves should space out the requests.
1099+
backoffMachine = null;
1100+
1101+
store.isLoading = false;
1102+
// Dismiss existing errors, if any.
1103+
reportErrorToUserBriefly(null);
1104+
accumulatedTransientFailureCount = 0;
1105+
1106+
final events = result.events;
1107+
for (final event in events) {
1108+
await store.handleEvent(event);
1109+
if (_disposed) return;
1110+
}
1111+
if (events.isNotEmpty) {
1112+
lastEventId = events.last.id;
1113+
}
11141114
}
1115-
if (events.isNotEmpty) {
1116-
lastEventId = events.last.id;
1115+
} catch (e) {
1116+
switch (e) {
1117+
case ZulipApiException(code: 'BAD_EVENT_QUEUE_ID'):
1118+
assert(debugLog('Lost event queue for $store. Replacing…'));
1119+
// The old event queue is gone, so we need a new one. This is normal.
1120+
1121+
default:
1122+
rethrow; // TODO(#563) try to recover instead
11171123
}
1124+
1125+
// This disposes the store, which disposes this update machine.
1126+
await store._globalStore._reloadPerAccount(store.accountId);
1127+
assert(debugLog('… Event queue replaced.'));
1128+
return;
11181129
}
11191130
}
11201131

0 commit comments

Comments
 (0)