From 8ef90610d7e086d8eb80e3201ebd162cd7a0fe3e Mon Sep 17 00:00:00 2001 From: Drown0315 Date: Sun, 24 Jul 2022 13:57:15 +0800 Subject: [PATCH] feat: add orElse callback and RetryExhaustedException --- retry/lib/retry.dart | 40 +++++++++++++- retry/test/retry_test.dart | 110 ++++++++++++++++++++++--------------- 2 files changed, 102 insertions(+), 48 deletions(-) diff --git a/retry/lib/retry.dart b/retry/lib/retry.dart index bde22c25..9626f1d3 100644 --- a/retry/lib/retry.dart +++ b/retry/lib/retry.dart @@ -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. + /// + /// If [orElse] is omitted, it defaults to throwing a [RetryExhaustedException] + /// when retries is exhausted. Future retry( FutureOr Function() fn, { FutureOr Function(Exception)? retryIf, FutureOr Function(Exception)? onRetry, + FutureOr Function(Exception)? orElse, }) async { var attempt = 0; // ignore: literal_only_boolean_expressions @@ -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) { @@ -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 retry( FutureOr Function() fn, { Duration delayFactor = const Duration(milliseconds: 200), @@ -179,10 +196,27 @@ Future retry( int maxAttempts = 8, FutureOr Function(Exception)? retryIf, FutureOr Function(Exception)? onRetry, + FutureOr 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. +class RetryExhaustedException implements Exception { + final int attempt; + final dynamic exception; + + RetryExhaustedException( + this.attempt, + this.exception, + ); + + @override + String toString() { + return 'RetryExhaustedException{attempt: $attempt, exception: $exception}'; + } +} diff --git a/retry/test/retry_test.dart b/retry/test/retry_test.dart index 75930633..1fd884f4 100644 --- a/retry/test/retry_test.dart +++ b/retry/test/retry_test.dart @@ -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)); }); }