Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions retry/lib/retry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,16 @@ class RetryOptions {
/// 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].
///
/// if retries is exhausted, the result of invoking the [orElse] function is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalize

///
/// If [orElse] is omitted, it defaults to throwing a [RetryExhaustedException]
/// when retries is exhausted.
Future<T> retry<T>(
FutureOr<T> Function() fn, {
FutureOr<bool> Function(Exception)? retryIf,
FutureOr<void> Function(Exception)? onRetry,
FutureOr<T> Function(Exception)? orElse,
}) async {
var attempt = 0;
// ignore: literal_only_boolean_expressions
Expand All @@ -130,8 +136,14 @@ class RetryOptions {
try {
return await fn();
} on Exception catch (e) {
if (attempt >= maxAttempts ||
(retryIf != null && !(await retryIf(e)))) {
if (attempt >= maxAttempts) {
if (orElse != null) {
return await orElse(e);
} else {
throw RetryExhaustedException(attempt, e);
}
}
if (retryIf != null && !(await retryIf(e))) {
rethrow;
}
if (onRetry != null) {
Expand Down Expand Up @@ -171,6 +183,11 @@ class RetryOptions {
/// 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].
///
/// if retries is exhausted, the result of invoking the [orElse] function is returned.
///
/// If [orElse] is omitted, it defaults to throwing a [RetryExhaustedException]
/// when retries is exhausted.
Future<T> retry<T>(
FutureOr<T> Function() fn, {
Duration delayFactor = const Duration(milliseconds: 200),
Expand All @@ -179,10 +196,27 @@ Future<T> retry<T>(
int maxAttempts = 8,
FutureOr<bool> Function(Exception)? retryIf,
FutureOr<void> Function(Exception)? onRetry,
FutureOr<T> Function(Exception)? orElse,
}) =>
RetryOptions(
delayFactor: delayFactor,
randomizationFactor: randomizationFactor,
maxDelay: maxDelay,
maxAttempts: maxAttempts,
).retry(fn, retryIf: retryIf, onRetry: onRetry);
).retry(fn, retryIf: retryIf, onRetry: onRetry, orElse: orElse);

/// throw [RetryExhaustedException] when retries is exhausted.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalize

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, even if orElse is omitted.

Is it really better to get a retries exhausted exception, rather than throwing the last exception that was caught?

We'd need to investigate of breaking this package would affect many users.

I think the reason you need RetryExhaustException is that when you need to do subsequent processing for a retry failure, you need to repeatedly use the logic in retryIf to determine if the exception is a failed retry.

Maybe not needing RetryExhaustException, orElse to handle only the retryIf exceptions is a better way?

But here the RetryExhaustException throwing should rely on retryIf, a little embarrassing😅.

class RetryExhaustedException implements Exception {
final int attempt;
final dynamic exception;

RetryExhaustedException(
this.attempt,
this.exception,
);

@override
String toString() {
return 'RetryExhaustedException{attempt: $attempt, exception: $exception}';
}
}
110 changes: 65 additions & 45 deletions retry/test/retry_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,59 +77,79 @@ void main() {
count++;
throw FormatException('Retry will fail');
}, retryIf: (e) => e is FormatException);
await expectLater(f, throwsA(isFormatException));
await expectLater(
f,
throwsA(predicate((e) =>
e is RetryExhaustedException &&
e.attempt == 5 &&
e.exception is FormatException)));
expect(count, equals(5));
});

test('retry (success after 2)', () async {
test('retry (orElse, exhaust retries)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
final v = await retry(
() async {
count++;
throw Exception();
},
retryIf: (e) => true,
orElse: (_) => 1,
maxAttempts: 3,
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
return true;
}, retryIf: (e) => e is FormatException);
await expectLater(f, completion(isTrue));
expect(count, equals(2));
expect(count, 3);
expect(v, 1);
});
});

test('retry (no retryIf)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
return true;
});
await expectLater(f, completion(isTrue));
expect(count, equals(2));
});
test('retry (success after 2)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
return true;
}, retryIf: (e) => e is FormatException);
await expectLater(f, completion(isTrue));
expect(count, equals(2));
});

test('retry (unhandled on 2nd try)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
throw Exception('unhandled thing');
}, retryIf: (e) => e is FormatException);
await expectLater(f, throwsA(isException));
expect(count, equals(2));
test('retry (no retryIf)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
return true;
});
await expectLater(f, completion(isTrue));
expect(count, equals(2));
});

test('retry (unhandled on 2nd try)', () async {
var count = 0;
final r = RetryOptions(
maxAttempts: 5,
maxDelay: Duration(),
);
final f = r.retry(() {
count++;
if (count == 1) {
throw FormatException('Retry will be okay');
}
throw Exception('unhandled thing');
}, retryIf: (e) => e is FormatException);
await expectLater(f, throwsA(isException));
expect(count, equals(2));
});
}