Skip to content

Commit e85d6c5

Browse files
committed
backoff: Always wait a positive duration
Fixes: #602
1 parent 012b601 commit e85d6c5

File tree

2 files changed

+29
-1
lines changed

2 files changed

+29
-1
lines changed

lib/api/backoff.dart

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ class BackoffMachine {
3737
/// maximizes the range while preserving a capped exponential shape on
3838
/// the expected value. Greg discusses this in more detail at:
3939
/// https://github.com/zulip/zulip-mobile/pull/3841
40+
///
41+
/// The duration is always positive; [Duration] works in microseconds, so
42+
/// we deviate from the idealized uniform distribution just by rounding
43+
/// the smallest durations up to one microsecond instead of down to zero.
44+
/// Because in the real world any delay takes nonzero time, this mainly
45+
/// affects tests that use fake time, and keeps their behavior more realistic.
4046
Future<void> wait() async {
4147
_startTime ??= DateTime.now();
4248

@@ -45,7 +51,8 @@ class BackoffMachine {
4551
* min(_durationCeilingMs,
4652
_firstDurationMs * pow(_base, _waitsCompleted));
4753

48-
await Future<void>.delayed(Duration(milliseconds: durationMs.round()));
54+
await Future<void>.delayed(Duration(
55+
microseconds: max(1, (1000 * durationMs).round())));
4956

5057
_waitsCompleted++;
5158
}

test/api/backoff_test.dart

+21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:math';
23

34
import 'package:checks/checks.dart';
45
import 'package:clock/clock.dart';
@@ -51,4 +52,24 @@ void main() {
5152
check(maxFromAllTrials).isGreaterThan(expectedMax * 0.75);
5253
}
5354
});
55+
56+
test('BackoffMachine timeouts are always positive', () {
57+
// Regression test for: https://github.com/zulip/zulip-flutter/issues/602
58+
// This is a randomized test with a false-failure rate of zero.
59+
60+
// In the pre-#602 implementation, the first timeout was zero
61+
// when a random number from [0, 100] was below 0.5.
62+
// [numTrials] is chosen so that an implementation with that behavior
63+
// will fail the test with probability 99%.
64+
const hypotheticalFailureRate = 0.5 / 100;
65+
const numTrials = 2 * ln10 / hypotheticalFailureRate;
66+
67+
awaitFakeAsync((async) async {
68+
for (int i = 0; i < numTrials; i++) {
69+
final duration = await measureWait(BackoffMachine().wait());
70+
check(duration).isGreaterThan(Duration.zero);
71+
}
72+
check(async.pendingTimers).isEmpty();
73+
});
74+
});
5475
}

0 commit comments

Comments
 (0)