Skip to content

Commit 5e63b1d

Browse files
committed
backoff: Make firstBound and maxBound customizable
Also make an editing pass over the [BackoffMachine.wait] doc.
1 parent cba8975 commit 5e63b1d

File tree

2 files changed

+37
-16
lines changed

2 files changed

+37
-16
lines changed

lib/api/backoff.dart

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import 'dart:math';
55
/// Call the constructor before a loop starts, and call [wait] in each iteration
66
/// of the loop. Do not re-use the instance after exiting the loop.
77
class BackoffMachine {
8-
BackoffMachine();
8+
BackoffMachine({
9+
this.firstBound = const Duration(milliseconds: 100),
10+
this.maxBound = const Duration(seconds: 10),
11+
}) : assert(firstBound <= maxBound);
912

1013
/// How many waits have completed so far.
1114
///
@@ -17,13 +20,13 @@ class BackoffMachine {
1720
/// The upper bound on the duration of the first wait.
1821
///
1922
/// The actual duration will vary randomly up to this value; see [wait].
20-
static const firstBound = Duration(milliseconds: 100);
23+
final Duration firstBound;
2124

2225
/// The maximum upper bound on the duration of each wait,
2326
/// even after many waits.
2427
///
2528
/// The actual durations will vary randomly up to this value; see [wait].
26-
static const maxBound = Duration(seconds: 10);
29+
final Duration maxBound;
2730

2831
/// The factor the bound is multiplied by at each wait,
2932
/// until it reaches [maxBound].
@@ -36,17 +39,20 @@ class BackoffMachine {
3639
/// A future that resolves after an appropriate backoff time,
3740
/// with jitter applied to capped exponential growth.
3841
///
39-
/// A popular exponential backoff strategy is to increase the duration
40-
/// exponentially with the number of sleeps completed, with a base of 2,
41-
/// until a ceiling is reached. E.g., if the first duration is 100ms and
42-
/// the ceiling is 10s = 10000ms, the sequence would be, in ms:
42+
/// Each [wait] computes an upper bound on its wait duration,
43+
/// in a sequence growing exponentially from [firstBound]
44+
/// to a cap of [maxBound] by factors of [base].
45+
/// With their default values, this sequence is, in seconds:
4346
///
44-
/// 100, 200, 400, 800, 1600, 3200, 6400, 10000, 10000, 10000, ...
47+
/// 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 10, 10, 10, ...
4548
///
46-
/// Instead of using this strategy directly, we also apply "jitter".
47-
/// We use capped exponential backoff for the *upper bound* on a random
48-
/// duration, where the lower bound is always zero. Mitigating "bursts" is
49-
/// the goal of any "jitter" strategy, and the larger the range of randomness,
49+
/// To provide jitter, the actual wait duration is chosen randomly
50+
/// on the whole interval from zero up to the computed upper bound.
51+
///
52+
/// This jitter strategy with a lower bound of zero is reported to be more
53+
/// effective than some widespread strategies that use narrower intervals.
54+
/// The purpose of jitter is to mitigate "bursts" where many clients make
55+
/// requests in a short period; the larger the range of randomness,
5056
/// the smoother the bursts. Keeping the lower bound at zero
5157
/// maximizes the range while preserving a capped exponential shape on
5258
/// the expected value. Greg discusses this in more detail at:

test/api/backoff_test.dart

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ void main() {
4242

4343
final trialResults = List.generate(numTrials, (_) {
4444
return awaitFakeAsync((async) async {
45-
final backoffMachine = BackoffMachine();
45+
final backoffMachine = BackoffMachine(firstBound: firstBound,
46+
maxBound: maxBound);
4647
final results = <Duration>[];
4748
for (int i = 0; i < expectedMaxDurations.length; i++) {
4849
final duration = await measureWait(backoffMachine.wait());
@@ -75,11 +76,25 @@ void main() {
7576
maxBound: const Duration(seconds: 10));
7677
});
7778

78-
test('BackoffMachine intended bounds, explicitly', () {
79+
test('BackoffMachine timeouts, varying firstBound and maxBound', () {
80+
checkEmpirically(firstBound: const Duration(seconds: 5),
81+
maxBound: const Duration(seconds: 300));
82+
});
83+
84+
test('BackoffMachine timeouts, maxBound equal to firstBound', () {
85+
checkEmpirically(firstBound: const Duration(seconds: 1),
86+
maxBound: const Duration(seconds: 1));
87+
});
88+
89+
test('BackoffMachine default firstBound and maxBound', () {
90+
final backoffMachine = BackoffMachine();
91+
check(backoffMachine.firstBound).equals(const Duration(milliseconds: 100));
92+
check(backoffMachine.maxBound).equals(const Duration(seconds: 10));
93+
7994
// This check on expectedBounds acts as a cross-check on the
80-
// other test case above, confirming what it is it's checking for.
95+
// other test cases above, confirming what it is they're checking for.
8196
final bounds = expectedBounds(length: 11,
82-
firstBound: BackoffMachine.firstBound, maxBound: BackoffMachine.maxBound);
97+
firstBound: backoffMachine.firstBound, maxBound: backoffMachine.maxBound);
8398
check(bounds.map((d) => d.inMilliseconds)).deepEquals([
8499
100, 200, 400, 800, 1600, 3200, 6400, 10000, 10000, 10000, 10000,
85100
]);

0 commit comments

Comments
 (0)