Skip to content

Commit b19154a

Browse files
committed
store: Retry registerQueue on failure
In addition to the test this adds, I wanted to manually test it end-to-end, to help be sure this covered the scenario where this retry is known to be needed in the wild: * The app is offline for a while, perhaps because the device is asleep. * The app comes online, tries polling, and finds the event queue has expired, so it attempts a re-register. * Before that completes (which after all takes several seconds if the realm is a large one), the app goes offline again. * That request's response therefore never reaches the app, and so when it eventually comes back online it needs to retry. Step 1 is annoying to carry out literally, because it means waiting 10 minutes for the event queue to expire. To work around that, I sabotaged the getEvents binding function to use a wrong `queue_id` value: 'queue_id': RawParameter('wrong' + queueId), so that the server would always respond with a BAD_EVENT_QUEUE_ID error, just the same as if the queue had expired. Then to take the app offline and online again, I just turned airplane mode on and off on my device. Because I used a physical device connected to my computer over USB, that caused no interference to my watching the logs on the console. In my manual testing, the retries worked perfectly: no matter how many times I turned airplane mode on and off, or with what timing, the app always returned to getting a fresh queue and polling it for events. Fixes: #556
1 parent 3901aa7 commit b19154a

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

lib/model/store.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ class UpdateMachine {
605605
final connection = globalStore.apiConnectionFromAccount(account);
606606

607607
final stopwatch = Stopwatch()..start();
608-
final initialSnapshot = await registerQueue(connection); // TODO retry
608+
final initialSnapshot = await _registerQueueWithRetry(connection);
609609
final t = (stopwatch..stop()).elapsed;
610610
assert(debugLog("initial fetch time: ${t.inMilliseconds}ms"));
611611

@@ -628,6 +628,22 @@ class UpdateMachine {
628628
final String queueId;
629629
int lastEventId;
630630

631+
static Future<InitialSnapshot> _registerQueueWithRetry(
632+
ApiConnection connection) async {
633+
BackoffMachine? backoffMachine;
634+
while (true) {
635+
try {
636+
return await registerQueue(connection);
637+
} catch (e) {
638+
assert(debugLog('Error fetching initial snapshot: $e\n'
639+
'Backing off, then will retry…'));
640+
// TODO tell user if initial-fetch errors persist, or look non-transient
641+
await (backoffMachine ??= BackoffMachine()).wait();
642+
assert(debugLog('… Backoff wait complete, retrying initial fetch.'));
643+
}
644+
}
645+
}
646+
631647
Completer<void>? _debugLoopSignal;
632648

633649
/// In debug mode, causes the polling loop to pause before the next

test/model/store_test.dart

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ void main() {
149149
addTearDown(() => UpdateMachine.debugEnableRegisterNotificationToken = true);
150150
}
151151

152-
// ignore: unused_element
153152
void checkLastRequest() {
154153
check(connection.takeLastRequest()).isA<http.Request>()
155154
..method.equals('POST')
@@ -174,6 +173,35 @@ void main() {
174173
users.map((expected) => (it) => it.fullName.equals(expected.fullName)));
175174
}));
176175

176+
test('retries registerQueue on NetworkError', () => awaitFakeAsync((async) async {
177+
await prepareStore();
178+
179+
// Try to load, inducing an error in the request.
180+
connection.prepare(exception: Exception('failed'));
181+
final future = UpdateMachine.load(globalStore, eg.selfAccount.id);
182+
bool complete = false;
183+
future.whenComplete(() => complete = true);
184+
async.flushMicrotasks();
185+
checkLastRequest();
186+
check(complete).isFalse();
187+
188+
// The retry doesn't happen immediately; there's a timer.
189+
check(async.pendingTimers).length.equals(1);
190+
async.elapse(Duration.zero);
191+
check(connection.lastRequest).isNull();
192+
check(async.pendingTimers).length.equals(1);
193+
194+
// After a timer, we retry.
195+
final users = [eg.selfUser, eg.otherUser];
196+
connection.prepare(json: eg.initialSnapshot(realmUsers: users).toJson());
197+
final updateMachine = await future;
198+
updateMachine.debugPauseLoop();
199+
check(complete).isTrue();
200+
// checkLastRequest(); TODO UpdateMachine.debugPauseLoop was too late; see comment above
201+
check(updateMachine.store.users.values).unorderedMatches(
202+
users.map((expected) => (it) => it.fullName.equals(expected.fullName)));
203+
}));
204+
177205
// TODO test UpdateMachine.load starts polling loop
178206
// TODO test UpdateMachine.load calls registerNotificationToken
179207
});

0 commit comments

Comments
 (0)