Skip to content

Commit 567be0d

Browse files
committed
msglist: Throttle fetchOlder retries
The main purpose of having a separate class is to wait for the BackoffMachine without blocking fetchOlder so that we can reach the finally block and return. This approach is different from how a BackoffMachine is typically used, because the message list doesn't send and retry requests in a loop; its caller retries rapidly on scroll changes, and we want to ignore the excessive requests. The test drops irrelevant requests with `connection.takeRequests` without checking, as we are only interested in verifying that no request was sent. Fixes: #945 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 921eacd commit 567be0d

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

lib/model/message_list.dart

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:collection/collection.dart';
22
import 'package:flutter/foundation.dart';
33

4+
import '../api/backoff.dart';
45
import '../api/model/events.dart';
56
import '../api/model/model.dart';
67
import '../api/route/messages.dart';
@@ -469,7 +470,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
469470
Future<void> fetchInitial() async {
470471
// TODO(#80): fetch from anchor firstUnread, instead of newest
471472
// TODO(#82): fetch from a given message ID as anchor
472-
assert(!fetched && !haveOldest && !fetchingOlder);
473+
assert(!fetched && !haveOldest && !fetchingOlder && !_fetchOlderBackoff.isWaiting);
473474
assert(messages.isEmpty && contents.isEmpty);
474475
// TODO schedule all this in another isolate
475476
final generation = this.generation;
@@ -493,24 +494,33 @@ class MessageListView with ChangeNotifier, _MessageSequence {
493494
notifyListeners();
494495
}
495496

497+
final _fetchOlderBackoff = _RetryBackoff();
498+
496499
/// Fetch the next batch of older messages, if applicable.
497500
Future<void> fetchOlder() async {
498501
if (haveOldest) return;
499502
if (fetchingOlder) return;
503+
if (_fetchOlderBackoff.isWaiting) return;
500504
assert(fetched);
501505
assert(messages.isNotEmpty);
502506
_fetchingOlder = true;
503507
_updateEndMarkers();
504508
notifyListeners();
505509
final generation = this.generation;
506510
try {
507-
final result = await getMessages(store.connection,
508-
narrow: narrow.apiEncode(),
509-
anchor: NumericAnchor(messages[0].id),
510-
includeAnchor: false,
511-
numBefore: kMessageListFetchBatchSize,
512-
numAfter: 0,
513-
);
511+
final GetMessagesResult result;
512+
try {
513+
result = await getMessages(store.connection,
514+
narrow: narrow.apiEncode(),
515+
anchor: NumericAnchor(messages[0].id),
516+
includeAnchor: false,
517+
numBefore: kMessageListFetchBatchSize,
518+
numAfter: 0,
519+
);
520+
} catch (e) {
521+
_fetchOlderBackoff.start();
522+
rethrow;
523+
}
514524
if (this.generation > generation) return;
515525

516526
if (result.messages.isNotEmpty
@@ -528,6 +538,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
528538

529539
_insertAllMessages(0, fetchedMessages);
530540
_haveOldest = result.foundOldest;
541+
_fetchOlderBackoff.reset();
531542
} finally {
532543
if (this.generation == generation) {
533544
_fetchingOlder = false;
@@ -708,3 +719,21 @@ class MessageListView with ChangeNotifier, _MessageSequence {
708719
notifyListeners();
709720
}
710721
}
722+
723+
class _RetryBackoff {
724+
bool get isWaiting => _isWaiting;
725+
bool _isWaiting = false;
726+
727+
BackoffMachine? _backoffMachine;
728+
729+
void start() async {
730+
assert(!_isWaiting);
731+
_isWaiting = true;
732+
await (_backoffMachine ??= BackoffMachine()).wait();
733+
_isWaiting = false;
734+
}
735+
736+
void reset() {
737+
_backoffMachine = null;
738+
}
739+
}

test/model/message_list_test.dart

+28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:convert';
33
import 'package:checks/checks.dart';
44
import 'package:http/http.dart' as http;
55
import 'package:test/scaffolding.dart';
6+
import 'package:zulip/api/exception.dart';
67
import 'package:zulip/api/model/events.dart';
78
import 'package:zulip/api/model/model.dart';
89
import 'package:zulip/api/model/narrow.dart';
@@ -236,6 +237,33 @@ void main() {
236237
..messages.length.equals(30);
237238
});
238239

240+
test('fetchOlder nop during backoff', () => awaitFakeAsync((async) async {
241+
final olderMessages = List.generate(5, (i) => eg.streamMessage());
242+
final initialMessages = List.generate(5, (i) => eg.streamMessage());
243+
await prepare(narrow: const CombinedFeedNarrow());
244+
await prepareMessages(foundOldest: false, messages: initialMessages);
245+
check(model).messages.length.equals(5);
246+
247+
connection.prepare(httpStatus: 400, json: {
248+
'result': 'error', 'code': 'BAD_REQUEST', 'msg': 'Bad request'});
249+
check(async.pendingTimers).isEmpty();
250+
await check(model.fetchOlder()).throws<ZulipApiException>();
251+
// The backoff timer has started.
252+
check(async.pendingTimers).single;
253+
254+
connection.takeRequests();
255+
await model.fetchOlder();
256+
check(connection.lastRequest).isNull();
257+
check(async.pendingTimers).single;
258+
check(model).messages.length.equals(5);
259+
260+
async.flushTimers();
261+
connection.prepare(json: olderResult(
262+
anchor: 1000, foundOldest: false, messages: olderMessages).toJson());
263+
await model.fetchOlder();
264+
check(model).messages.length.equals(10);
265+
}));
266+
239267
test('fetchOlder handles servers not understanding includeAnchor', () async {
240268
const narrow = CombinedFeedNarrow();
241269
await prepare(narrow: narrow);

0 commit comments

Comments
 (0)