blob: bde22c259b58ab8798dbdbbef46ae273848d55bb [file] [log] [blame]
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Retry asynchronous functions with exponential backoff.
///
/// For a simple solution see [retry], to modify and persist retry options see
/// [RetryOptions]. Note, in many cases the added configurability is
/// unnecessary and using [retry] is perfectly fine.
library retry;
import 'dart:async';
import 'dart:math' as math;
final _rand = math.Random();
/// Object holding options for retrying a function.
///
/// With the default configuration functions will be retried up-to 7 times
/// (8 attempts in total), sleeping 1st, 2nd, 3rd, ..., 7th attempt:
/// 1. 400 ms +/- 25%
/// 2. 800 ms +/- 25%
/// 3. 1600 ms +/- 25%
/// 4. 3200 ms +/- 25%
/// 5. 6400 ms +/- 25%
/// 6. 12800 ms +/- 25%
/// 7. 25600 ms +/- 25%
///
/// **Example**
/// ```dart
/// final r = RetryOptions();
/// final response = await r.retry(
/// // Make a GET request
/// () => http.get('https://google.com').timeout(Duration(seconds: 5)),
/// // Retry on SocketException or TimeoutException
/// retryIf: (e) => e is SocketException || e is TimeoutException,
/// );
/// print(response.body);
/// ```
class RetryOptions {
/// Delay factor to double after every attempt.
///
/// Defaults to 200 ms, which results in the following delays:
///
/// 1. 400 ms
/// 2. 800 ms
/// 3. 1600 ms
/// 4. 3200 ms
/// 5. 6400 ms
/// 6. 12800 ms
/// 7. 25600 ms
///
/// Before application of [randomizationFactor].
final Duration delayFactor;
/// Percentage the delay should be randomized, given as fraction between
/// 0 and 1.
///
/// If [randomizationFactor] is `0.25` (default) this indicates 25 % of the
/// delay should be increased or decreased by 25 %.
final double randomizationFactor;
/// Maximum delay between retries, defaults to 30 seconds.
final Duration maxDelay;
/// Maximum number of attempts before giving up, defaults to 8.
final int maxAttempts;
/// Create a set of [RetryOptions].
///
/// Defaults to 8 attempts, sleeping as following after 1st, 2nd, 3rd, ...,
/// 7th attempt:
/// 1. 400 ms +/- 25%
/// 2. 800 ms +/- 25%
/// 3. 1600 ms +/- 25%
/// 4. 3200 ms +/- 25%
/// 5. 6400 ms +/- 25%
/// 6. 12800 ms +/- 25%
/// 7. 25600 ms +/- 25%
const RetryOptions({
this.delayFactor = const Duration(milliseconds: 200),
this.randomizationFactor = 0.25,
this.maxDelay = const Duration(seconds: 30),
this.maxAttempts = 8,
});
/// Delay after [attempt] number of attempts.
///
/// This is computed as `pow(2, attempt) * delayFactor`, then is multiplied by
/// between `-randomizationFactor` and `randomizationFactor` at random.
Duration delay(int attempt) {
assert(attempt >= 0, 'attempt cannot be negative');
if (attempt <= 0) {
return Duration.zero;
}
final rf = (randomizationFactor * (_rand.nextDouble() * 2 - 1) + 1);
final exp = math.min(attempt, 31); // prevent overflows.
final delay = (delayFactor * math.pow(2.0, exp) * rf);
return delay < maxDelay ? delay : maxDelay;
}
/// Call [fn] retrying so long as [retryIf] return `true` for the exception
/// thrown.
///
/// At every retry the [onRetry] function will be called (if given). The
/// function [fn] will be invoked at-most [this.attempts] times.
///
/// If no [retryIf] function is given this will retry any for any [Exception]
/// thrown. To retry on an [Error], the error must be caught and _rethrown_
/// as an [Exception].
Future<T> retry<T>(
FutureOr<T> Function() fn, {
FutureOr<bool> Function(Exception)? retryIf,
FutureOr<void> Function(Exception)? onRetry,
}) async {
var attempt = 0;
// ignore: literal_only_boolean_expressions
while (true) {
attempt++; // first invocation is the first attempt
try {
return await fn();
} on Exception catch (e) {
if (attempt >= maxAttempts ||
(retryIf != null && !(await retryIf(e)))) {
rethrow;
}
if (onRetry != null) {
await onRetry(e);
}
}
// Sleep for a delay
await Future.delayed(delay(attempt));
}
}
}
/// Call [fn] retrying so long as [retryIf] return `true` for the exception
/// thrown, up-to [maxAttempts] times.
///
/// Defaults to 8 attempts, sleeping as following after 1st, 2nd, 3rd, ...,
/// 7th attempt:
/// 1. 400 ms +/- 25%
/// 2. 800 ms +/- 25%
/// 3. 1600 ms +/- 25%
/// 4. 3200 ms +/- 25%
/// 5. 6400 ms +/- 25%
/// 6. 12800 ms +/- 25%
/// 7. 25600 ms +/- 25%
///
/// ```dart
/// final response = await retry(
/// // Make a GET request
/// () => http.get('https://google.com').timeout(Duration(seconds: 5)),
/// // Retry on SocketException or TimeoutException
/// retryIf: (e) => e is SocketException || e is TimeoutException,
/// );
/// print(response.body);
/// ```
///
/// If no [retryIf] function is given this will retry any for any [Exception]
/// thrown. To retry on an [Error], the error must be caught and _rethrown_
/// as an [Exception].
Future<T> retry<T>(
FutureOr<T> Function() fn, {
Duration delayFactor = const Duration(milliseconds: 200),
double randomizationFactor = 0.25,
Duration maxDelay = const Duration(seconds: 30),
int maxAttempts = 8,
FutureOr<bool> Function(Exception)? retryIf,
FutureOr<void> Function(Exception)? onRetry,
}) =>
RetryOptions(
delayFactor: delayFactor,
randomizationFactor: randomizationFactor,
maxDelay: maxDelay,
maxAttempts: maxAttempts,
).retry(fn, retryIf: retryIf, onRetry: onRetry);