retry<T> static method
Re-runs operation up to maxAttempts times.
Returns the first Success produced. If every attempt fails, returns the
final Error. If retryIf returns false for a failure, retries stop
immediately and that failure is returned.
onRetry is invoked after a failure that will be retried, with the
1-based attempt number that failed and the failure itself. It is not
invoked for the final failure or for a Success.
maxDelay caps the wait between attempts after exponential backoff is
applied. Use this to bound the worst-case wait when maxAttempts and
backoffFactor are both large.
jitter adds randomness in the range [0, jitter] as a multiplier on
the computed wait — e.g. jitter: 0.3 produces waits between 100% and
130% of the backed-off delay. Defaults to 0.0 (no jitter). Useful for
preventing many clients from retrying in lockstep after a shared
outage. Pass random to make the jitter deterministic in tests.
attemptTimeout bounds each individual attempt. If operation does
not produce a Result within this duration, the attempt is converted
into an Error(Failure.timeout()) and is subject to the normal
retryIf / maxAttempts logic. Leave null to wait indefinitely on
each attempt.
Implementation
static Future<Result<T>> retry<T>(
Future<Result<T>> Function() operation, {
int maxAttempts = 3,
Duration delay = const Duration(milliseconds: 500),
double backoffFactor = 2.0,
Duration? maxDelay,
double jitter = 0.0,
Random? random,
Duration? attemptTimeout,
bool Function(Failure failure)? retryIf,
void Function(int attempt, Failure failure)? onRetry,
}) async {
assert(maxAttempts > 0, 'maxAttempts must be > 0');
assert(backoffFactor > 0, 'backoffFactor must be > 0');
assert(jitter >= 0, 'jitter must be >= 0');
final rng = random ?? Random();
Result<T>? last;
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
Result<T> result;
try {
result = attemptTimeout != null
? await operation().timeout(attemptTimeout)
: await operation();
} on TimeoutException catch (e, st) {
result = Error<T>(
Failure.timeout(
message: 'Attempt timed out after $attemptTimeout',
stackTrace: st,
cause: e,
),
);
}
last = result;
if (result is Success<T>) return result;
final failure = (result as Error<T>).failure;
final isLastAttempt = attempt == maxAttempts;
final shouldRetry = retryIf?.call(failure) ?? true;
if (isLastAttempt || !shouldRetry) return result;
onRetry?.call(attempt, failure);
var waitMicros =
(delay.inMicroseconds * _pow(backoffFactor, attempt - 1)).round();
if (maxDelay != null && waitMicros > maxDelay.inMicroseconds) {
waitMicros = maxDelay.inMicroseconds;
}
if (jitter > 0) {
waitMicros = (waitMicros * (1 + rng.nextDouble() * jitter)).round();
}
await Future<void>.delayed(Duration(microseconds: waitMicros));
}
// Unreachable: the loop above always returns once it exits.
return last!;
}