Last active
September 9, 2025 13:41
-
-
Save eric-taix/071668a826358e60cfe3b5981dc4627e to your computer and use it in GitHub Desktop.
Generic retry strategy on TaskEither<L,R>
This file contains hidden or 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
| import 'dart:math'; | |
| import 'package:fpdart/fpdart.dart'; | |
| /// [RetryStrategy] is a strategy to retry a [TaskEither] when it fails. | |
| /// See each implementation documentation for more details. | |
| abstract interface class RetryStrategy { | |
| TaskEither<L, R> retry<L, R>(TaskEither<L, R> taskEither); | |
| } | |
| /// [LinearRetryStrategy] retries a [TaskEither] linearly with a fixed delay between retries | |
| /// and a maximum number of retries. | |
| class LinearRetryStrategy implements RetryStrategy { | |
| LinearRetryStrategy({required this.maxAttempts, required this.interval}); | |
| /// The total number of attempts to do the job. | |
| final int maxAttempts; | |
| /// The delay between retries. | |
| final Duration interval; | |
| int _attempts = 0; | |
| @override | |
| TaskEither<L, R> retry<L, R>(TaskEither<L, R> taskEither) { | |
| TaskEither<L, R> doRetry(L error) { | |
| _attempts++; | |
| return _attempts > maxAttempts ? TaskEither.left(error) : taskEither.delay(interval).orElse(doRetry); | |
| } | |
| return taskEither.orElse(doRetry); | |
| } | |
| } | |
| /// [ExponentialBackoffRetryStrategy] retries a [TaskEither] exponentially with a exponential delay between retries | |
| /// and a maximum number of retries. The interval between retries is capped by [maxInterval] to avoid very long wait times. | |
| /// See [FibonacciBackoffRetryStrategy] for a smoother retry strategy | |
| class ExponentialBackoffRetryStrategy implements RetryStrategy { | |
| ExponentialBackoffRetryStrategy({ | |
| required this.maxAttempts, | |
| required this.initialInterval, | |
| required this.multiplier, | |
| required this.maxInterval, | |
| }); | |
| /// The total maximum number of attemps to do the job | |
| final int maxAttempts; | |
| /// The initial delay between retries. | |
| final Duration initialInterval; | |
| /// The multiplier used to calculate the delay for each retry. | |
| final double multiplier; | |
| /// The maximum delay between retries. | |
| final Duration maxInterval; | |
| int _attempts = 1; | |
| @override | |
| TaskEither<L, R> retry<L, R>(TaskEither<L, R> taskEither) { | |
| TaskEither<L, R> doRetry(L error) { | |
| _attempts++; | |
| final currentInterval = initialInterval * (pow(multiplier, _attempts - 1)); | |
| final delay = currentInterval.compareTo(maxInterval) < 0 ? currentInterval : maxInterval; | |
| return _attempts > maxAttempts ? TaskEither.left(error) : taskEither.delay(delay).orElse(doRetry); | |
| } | |
| return taskEither.orElse(doRetry); | |
| } | |
| } | |
| /// [FibonacciBackoffRetryStrategy] retries a [TaskEither] with a delay between retries that follows the Fibonacci sequence | |
| /// and a maximum number of retries. The interval between retries is capped by [maxInterval] to avoid very long wait times. | |
| /// | |
| /// [FibonacciBackoffRetryStrategy] has smoother intervals than [ExponentialBackoffRetryStrategy] | |
| class FibonacciBackoffRetryStrategy implements RetryStrategy { | |
| FibonacciBackoffRetryStrategy({required this.maxAttempts, required this.initialInterval, required this.maxInterval}); | |
| final int maxAttempts; | |
| final Duration initialInterval; | |
| final Duration maxInterval; | |
| int _attempts = 1; | |
| int _fibonacci(int n) { | |
| if (n <= 1) return 1; | |
| return _fibonacci(n - 1) + _fibonacci(n - 2); | |
| } | |
| @override | |
| TaskEither<L, R> retry<L, R>(TaskEither<L, R> taskEither) { | |
| TaskEither<L, R> doRetry(L error) { | |
| _attempts++; | |
| final currentInterval = initialInterval * _fibonacci(_attempts - 1); | |
| final delay = currentInterval.compareTo(maxInterval) < 0 ? currentInterval : maxInterval; | |
| return _attempts > maxAttempts ? TaskEither.left(error) : taskEither.delay(delay).orElse(doRetry); | |
| } | |
| return taskEither.orElse(doRetry); | |
| } | |
| } | |
| extension RetryTaskEither<L, R> on TaskEither<L, R> { | |
| TaskEither<L, R> withRetry(RetryStrategy strategy) => strategy.retry(this); | |
| } | |
| void main() { | |
| final taskEither = TaskEither<String, Unit>(() async { | |
| print('Doing Job ${DateTime.now()}'); | |
| return const Left<String, Unit>('Error'); | |
| }); | |
| taskEither | |
| .withRetry( | |
| FibonacciBackoffRetryStrategy( | |
| maxAttempts: 5, | |
| initialInterval: const Duration(milliseconds: 300), | |
| maxInterval: const Duration(seconds: 3), | |
| ), | |
| ) | |
| .run(); | |
| taskEither | |
| .withRetry( | |
| ExponentialBackoffRetryStrategy( | |
| maxAttempts: 5, | |
| initialInterval: const Duration(milliseconds: 300), | |
| multiplier: 2.2, | |
| maxInterval: const Duration(seconds: 3), | |
| ), | |
| ) | |
| .run(); | |
| taskEither | |
| .withRetry( | |
| LinearRetryStrategy( | |
| maxAttempts: 5, | |
| interval: const Duration(milliseconds: 300), | |
| ), | |
| ) | |
| .run(); | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment