Skip to main content

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 the wasSuccessful 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:

Modifying Retry Policies
// 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)));
Composing Retry Policies
// 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)),
);
tip

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.