Retrying IO
It's very common to encounter calculations in the wild that can fail for any number or reasons. When making an HTTP call, for example, the operation could fail due to:
- Server is down
- Client timeout exceeded
- Request gave incorrect data
- Returned data has missing/unexpected JSON fields
- A cable being unplugged
This is only a few of the countless ways in which things can go sideways. Any
time you're interacting with the world outside your program such as a network
or file system, failure is an option. In certains circumstance, like in an
HTTP request as described above, it may be worthwhile to retry the operation
and hope that things go better the next time. Because this situation is so
common, Ribs provides a retry mechanism for IO
out of the box!
Flaky Operations
Let's define our flaky operation so that we can see how Ribs allows us to easily bake in retry capabilities:
IO<Json> flakyOp() => IO.delay(() => HttpClient()).bracket(
(client) => IO
.fromFutureF(() =>
client.getUrl(Uri.parse('http://api.flaky.org/account/123')))
.flatMap((req) => IO.fromFutureF(() => req.close()))
.flatMap((resp) => IO.fromFutureF(() => utf8.decodeStream(resp)))
.flatMap((bodyText) => IO.fromEither(Json.parse(bodyText))),
(client) => IO.exec(() => client.close()),
);
It's not very important that you immediately understand every single bit of what this code does, so long as you understand that it makes an HTTP request to our fake endpoint and attempts to parse the response as a JSON string, using the Ribs JSON library.
Declarative Retries
Now that our flaky operation is defined let's apply the simplest RetryPolicy
to it that we can:
final IO<Json> retry3Times = flakyOp().retrying(RetryPolicy.limitRetries(3));
And just like that, we've enhanced our original IO
to create a new IO
that
will automatically retry the operation if it fails, up to 3 more times.
Recognize that this capability is available on any IO<A>
type so it's
completely generic in terms of what the underlying operation is doing!
Retrying Customization
The IO.retrying
function provides additional ways to customize the retry
behavior of your operation. Here's an example:
final IO<Json> customRetry = flakyOp().retrying(
RetryPolicy.constantDelay(5.seconds),
wasSuccessful: (json) => json.isObject,
isWorthRetrying: (error) => error.message.toString().contains('oops'),
onError: (error, details) => IO.println('Attempt ${details.retriesSoFar}.'),
onFailure: (json, details) => IO.println('$json failed [$details]'),
);
Let's look at each argument to see what's available to you:
- policy: In this example,
RetryPolicy.contantDelay
is given, which will continually retry a failed operation after a specified delay. - wasSuccessful: Logic you can provide to inspect a successful compuation and force another retry attempt.
- isWorthRetrying: Logic you can provide to inspect the
RuntimeException
and determine if the opration should be retried again, overriding the policy. - onError: A side effect that is run every time the underlying
IO
encounters an error. In this case, the cumulative number or retries is printed to stdout. - onFailure: A side effect that is run every time the result from the
underlying
IO
fails thewasSuccessful
predicate.
Retry Policy Customization
You can also customize your retry policy to achieve the exact behavior you want by using combinators and/or combining any number of policies in a few different ways. To start off look at these examples:
// Exponential backoff with a maximum delay or 20 seconds
flakyOp().retrying(
RetryPolicy.exponentialBackoff(1.second).capDelay(20.seconds),
);
// Jitter backoff that will stop any retries after 1 minute
flakyOp().retrying(
RetryPolicy.fullJitter(2.seconds).giveUpAfterCumulativeDelay(1.minute),
);
// Retry every 2 seconds, giving up after 10 seconds, but then retry
// an additional 5 times
flakyOp().retrying(RetryPolicy.constantDelay(2.seconds)
.giveUpAfterCumulativeDelay(10.seconds)
.followedBy(RetryPolicy.limitRetries(5)));
// Join 2 policies, where retry is stopped when *either* policy wants to
// and the maximum delay is chosen between the two policies
flakyOp().retrying(
RetryPolicy.exponentialBackoff(1.second)
.giveUpAfterDelay(10.seconds)
.join(RetryPolicy.limitRetries(10)),
);
// Meet results in a policy that will retry until *both* policies want to
// give up and the minimum delay is chosen between the two policies
flakyOp().retrying(
RetryPolicy.exponentialBackoff(1.second)
.giveUpAfterDelay(10.seconds)
.meet(RetryPolicy.limitRetries(10)),
);
There's virtually no limit to what you can do with RetryPolicy
but to get a
full handle of what's possible, you should check out the API documentation.