-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: xsahil03x <xdsahil@gmail.com>
- Loading branch information
Showing
4 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
library rate_limiter; | ||
|
||
export 'src/backoff.dart'; | ||
export 'src/debounce.dart'; | ||
export 'src/throttle.dart'; | ||
export 'src/extension.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
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 response = await backOff( | ||
/// // 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 BackOff<T> { | ||
const BackOff( | ||
this.func, { | ||
this.delayFactor = const Duration(milliseconds: 200), | ||
this.randomizationFactor = 0.25, | ||
this.maxDelay = const Duration(seconds: 30), | ||
this.maxAttempts = 8, | ||
this.retryIf, | ||
}) : assert(maxAttempts >= 1, 'maxAttempts must be greater than 0'); | ||
|
||
/// The [Function] to execute. If the function throws an error, it will be | ||
/// retried [maxAttempts] times with an increasing delay between each attempt | ||
/// up to [maxDelay]. | ||
/// | ||
/// If [retryIf] is provided, the function will only be retried if the error | ||
/// matches the predicate. | ||
final FutureOr<T> Function() func; | ||
|
||
/// 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; | ||
|
||
/// Function to determine if a retry should be attempted. | ||
/// | ||
/// If `null` (default) all errors will be retried. | ||
final FutureOr<bool> Function(Object error, int attempt)? retryIf; | ||
|
||
// returns the sleep duration based on `attempt`. | ||
Duration _getSleepDuration(int attempt) { | ||
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; | ||
} | ||
|
||
Future<T> call() async { | ||
var attempt = 0; | ||
while (true) { | ||
attempt++; // first invocation is the first attempt. | ||
try { | ||
return await func(); | ||
} catch (error) { | ||
final attemptLimitReached = attempt >= maxAttempts; | ||
if (attemptLimitReached) rethrow; | ||
|
||
final shouldRetry = await retryIf?.call(error, attempt); | ||
if (shouldRetry == false) rethrow; | ||
} | ||
|
||
// sleep for a delay. | ||
await Future.delayed(_getSleepDuration(attempt)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import 'package:rate_limiter/rate_limiter.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
test('backOff (success)', () async { | ||
var count = 0; | ||
final f = backOff(() { | ||
count++; | ||
return true; | ||
}); | ||
expect(f, completion(isTrue)); | ||
expect(count, equals(1)); | ||
}); | ||
|
||
test('retry (unhandled exception)', () async { | ||
var count = 0; | ||
final f = backOff( | ||
() { | ||
count++; | ||
throw Exception('Retry will fail'); | ||
}, | ||
maxAttempts: 5, | ||
retryIf: (_, __) => false, | ||
); | ||
await expectLater(f, throwsA(isException)); | ||
expect(count, equals(1)); | ||
}); | ||
|
||
test('retry (retryIf, exhaust retries)', () async { | ||
var count = 0; | ||
final f = backOff( | ||
() { | ||
count++; | ||
throw FormatException('Retry will fail'); | ||
}, | ||
maxAttempts: 5, | ||
maxDelay: Duration(), | ||
retryIf: (e, _) => e is FormatException, | ||
); | ||
await expectLater(f, throwsA(isFormatException)); | ||
expect(count, equals(5)); | ||
}); | ||
|
||
test('retry (success after 2)', () async { | ||
var count = 0; | ||
final f = backOff( | ||
() { | ||
count++; | ||
if (count == 1) { | ||
throw FormatException('Retry will be okay'); | ||
} | ||
return true; | ||
}, | ||
maxAttempts: 5, | ||
maxDelay: Duration(), | ||
retryIf: (e, _) => e is FormatException, | ||
); | ||
await expectLater(f, completion(isTrue)); | ||
expect(count, equals(2)); | ||
}); | ||
|
||
test('retry (no retryIf)', () async { | ||
var count = 0; | ||
final f = backOff( | ||
() { | ||
count++; | ||
if (count == 1) { | ||
throw FormatException('Retry will be okay'); | ||
} | ||
return true; | ||
}, | ||
maxAttempts: 5, | ||
maxDelay: Duration(), | ||
); | ||
await expectLater(f, completion(isTrue)); | ||
expect(count, equals(2)); | ||
}); | ||
|
||
test('retry (unhandled on 2nd try)', () async { | ||
var count = 0; | ||
final f = backOff( | ||
() { | ||
count++; | ||
if (count == 1) { | ||
throw FormatException('Retry will be okay'); | ||
} | ||
throw Exception('unhandled thing'); | ||
}, | ||
maxAttempts: 5, | ||
maxDelay: Duration(), | ||
retryIf: (e, _) => e is FormatException, | ||
); | ||
await expectLater(f, throwsA(isException)); | ||
expect(count, equals(2)); | ||
}); | ||
} |