Combinators

The library offers a few slightly different ways to wrap your operations with retries.

Cheat sheet

Combinator Context bound Handles
retryingOnFailures Monad Failures
retryingOnSomeErrors MonadError Errors
retryingOnAllErrors MonadError Errors
retryingOnFailuresAndSomeErrors MonadError Failures and errors
retryingOnFailuresAndAllErrors MonadError Failures and errors

More information on each combinator is provided below.

retryingOnFailures

To use retryingOnFailures, you pass in a predicate that decides whether you are happy with the result or you want to retry. It is useful when you are working in an arbitrary Monad that is not a MonadError. Your operation doesn’t throw errors, but you want to retry until it returns a value that you are happy with.

The API (modulo some type-inference trickery) looks like this:

def retryingOnFailures[M[_]: Monad: Sleep, A](policy: RetryPolicy[M],
                                              wasSuccessful: A => M[Boolean],
                                              onFailure: (A, RetryDetails) => M[Unit])
                                              (action: => M[A]): M[A]

You need to pass in:

  • a retry policy
  • a predicate that decides whether the operation was successful
  • a failure handler, often used for logging
  • the operation that you want to wrap with retries

For example, let’s keep rolling a die until we get a six, using IO.

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import retry._

import scala.concurrent.duration._

val policy = RetryPolicies.constantDelay[IO](10.milliseconds)
// policy: RetryPolicy[IO] = RetryPolicy(
//   decideNextRetry = retry.RetryPolicy$$$Lambda$11987/1347236281@5137ae40
// )

def onFailure(failedValue: Int, details: RetryDetails): IO[Unit] = {
  IO(println(s"Rolled a $failedValue, retrying ..."))
}

val loadedDie = util.LoadedDie(2, 5, 4, 1, 3, 2, 6)
// loadedDie: util.LoadedDie = LoadedDie(rolls = ArraySeq(2, 5, 4, 1, 3, 2, 6))

val io = retryingOnFailures(policy, (i: Int) => IO.pure(i == 6), onFailure){
  IO(loadedDie.roll())
}
// io: IO[Int] = FlatMap(
//   ioe = FlatMap(
//     ioe = Delay(thunk = <function0>),
//     f = retry.package$RetryingOnFailuresPartiallyApplied$$Lambda$11990/356075305@5f60302c
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@1561b23c
// )

io.unsafeRunSync()
// Rolled a 2, retrying ...
// Rolled a 5, retrying ...
// Rolled a 4, retrying ...
// Rolled a 1, retrying ...
// Rolled a 3, retrying ...
// Rolled a 2, retrying ...
// res0: Int = 6

retryingOnSomeErrors

This is useful when you are working with a MonadError[M, E] but you only want to retry on some errors.

To use retryingOnSomeErrors, you need to pass in a predicate that decides whether a given error is worth retrying.

The API (modulo some type-inference trickery) looks like this:

def retryingOnSomeErrors[M[_]: Sleep, A, E](policy: RetryPolicy[M],
                                            isWorthRetrying: E => M[Boolean],
                                            onError: (E, RetryDetails) => M[Unit])
                                           (action: => M[A])
                                           (implicit ME: MonadError[M, E]): M[A]

You need to pass in:

  • a retry policy
  • a predicate that decides whether a given error is worth retrying
  • an error handler, often used for logging
  • the operation that you want to wrap with retries

For example, let’s make a request for a cat gif using our flaky HTTP client, retrying only if we get an IOException.

import java.io.IOException

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO(httpClient.getCatGif())
// flakyRequest: IO[String] = Delay(thunk = <function0>)

def isIOException(e: Throwable): IO[Boolean] = e match {
  case _: IOException => IO.pure(true)
  case _ => IO.pure(false)
}

val io = retryingOnSomeErrors(
  isWorthRetrying = isIOException,
  policy = RetryPolicies.limitRetries[IO](5),
  onError = retry.noop[IO, Throwable]
)(flakyRequest)
// io: IO[String] = FlatMap(
//   ioe = FlatMap(
//     ioe = Attempt(ioa = Delay(thunk = <function0>)),
//     f = retry.package$RetryingOnSomeErrorsPartiallyApplied$$Lambda$12035/340735956@6d2fbfac
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@a494893
// )

io.unsafeRunSync()
// res1: String = "cute cat gets sleepy and falls asleep"

retryingOnAllErrors

This is useful when you are working with a MonadError[M, E] and you want to retry on all errors.

The API (modulo some type-inference trickery) looks like this:

def retryingOnAllErrors[M[_]: Sleep, A, E](policy: RetryPolicy[M],
                                           onError: (E, RetryDetails) => M[Unit])
                                          (action: => M[A])
                                          (implicit ME: MonadError[M, E]): M[A]

You need to pass in:

  • a retry policy
  • an error handler, often used for logging
  • the operation that you want to wrap with retries

For example, let’s make the same request for a cat gif, this time retrying on all errors.

import java.io.IOException

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO(httpClient.getCatGif())
// flakyRequest: IO[String] = Delay(thunk = <function0>)

val io = retryingOnAllErrors(
  policy = RetryPolicies.limitRetries[IO](5),
  onError = retry.noop[IO, Throwable]
)(flakyRequest)
// io: IO[String] = FlatMap(
//   ioe = FlatMap(
//     ioe = Attempt(ioa = Delay(thunk = <function0>)),
//     f = retry.package$RetryingOnSomeErrorsPartiallyApplied$$Lambda$12035/340735956@42ecd1e7
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@52aed4c9
// )

io.unsafeRunSync()
// res2: String = "cute cat gets sleepy and falls asleep"

retryingOnFailuresAndSomeErrors

This is a combination of retryingOnFailures and retryingOnSomeErrors. It allows you to specify failure conditions for both the results and errors that can occur.

To use retryingOnFailuresAndSomeErrors, you need to pass in predicates that decide whether a given error or result is worth retrying.

The API (modulo some type-inference trickery) looks like this:

def retryingOnFailuresAndSomeErrors[M[_]: Sleep, A, E](policy: RetryPolicy[M],
                                                       wasSuccessful: A => M[Boolean],
                                                       isWorthRetrying: E => M[Boolean],
                                                       onFailure: (A, RetryDetails) => M[Unit],
                                                       onError: (E, RetryDetails) => M[Unit])
                                                      (action: => M[A])
                                                      (implicit ME: MonadError[M, E]): M[A]

You need to pass in:

  • a retry policy
  • a predicate that decides whether the operation was successful
  • a predicate that decides whether a given error is worth retrying
  • a failure handler, often used for logging
  • an error handler, often used for logging
  • the operation that you want to wrap with retries

For example, let’s make a request to an API to retrieve details for a record, which we will only retry if:

  • A timeout exception occurs
  • The record’s details are incomplete pending future operations
import java.util.concurrent.TimeoutException

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO(httpClient.getRecordDetails("foo"))
// flakyRequest: IO[String] = Delay(thunk = <function0>)

def isTimeoutException(e: Throwable): IO[Boolean] = e match {
  case _: TimeoutException => IO.pure(true)
  case _ => IO.pure(false)
}

val io = retryingOnFailuresAndSomeErrors(
  wasSuccessful = (s: String) => IO.pure(s != "pending"),
  isWorthRetrying = isTimeoutException,
  policy = RetryPolicies.limitRetries[IO](5),
  onFailure = retry.noop[IO, String],
  onError = retry.noop[IO, Throwable]
)(flakyRequest)
// io: IO[String] = FlatMap(
//   ioe = FlatMap(
//     ioe = Attempt(ioa = Delay(thunk = <function0>)),
//     f = retry.package$RetryingOnFailuresAndSomeErrorsPartiallyApplied$$Lambda$12043/1215302332@12e313df
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@4270e427
// )

io.unsafeRunSync()
// res3: String = "got some sweet details"

retryingOnFailuresAndAllErrors

This is a combination of retryingOnFailures and retryingOnAllErrors. It allows you to specify failure conditions for your results as well as retry an error that occurs

To use retryingOnFailuresAndAllErrors, you need to pass in a predicate that decides whether a given result is worth retrying.

The API (modulo some type-inference trickery) looks like this:

def retryingOnFailuresAndAllErrors[M[_]: Sleep, A, E](policy: RetryPolicy[M],
                                                      wasSuccessful: A => M[Boolean],
                                                      onFailure: (A, RetryDetails) => M[Unit],
                                                      onError: (E, RetryDetails) => M[Unit])
                                                     (action: => M[A])
                                                     (implicit ME: MonadError[M, E]): M[A]

You need to pass in:

  • a retry policy
  • a predicate that decides whether the operation was successful
  • a failure handler, often used for logging
  • an error handler, often used for logging
  • the operation that you want to wrap with retries

For example, let’s make a request to an API to retrieve details for a record, which we will only retry if:

  • Any exception occurs
  • The record’s details are incomplete pending future operations
import java.util.concurrent.TimeoutException

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO(httpClient.getRecordDetails("foo"))
// flakyRequest: IO[String] = Delay(thunk = <function0>)

val io = retryingOnFailuresAndAllErrors(
  wasSuccessful = (s: String) => IO.pure(s != "pending"),
  policy = RetryPolicies.limitRetries[IO](5),
  onFailure = retry.noop[IO, String],
  onError = retry.noop[IO, Throwable]
)(flakyRequest)
// io: IO[String] = FlatMap(
//   ioe = FlatMap(
//     ioe = Attempt(ioa = Delay(thunk = <function0>)),
//     f = retry.package$RetryingOnFailuresAndSomeErrorsPartiallyApplied$$Lambda$12043/1215302332@66377667
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@429bfef4
// )

io.unsafeRunSync()
// res4: String = "got some sweet details"

Syntactic sugar

Cats-retry includes some syntactic sugar in order to reduce boilerplate.

Instead of calling the combinators and passing in your action, you can call them as extension methods.

import retry.syntax.all._

// To retry until you get a value you like
IO(loadedDie.roll()).retryingOnFailures(
  policy = RetryPolicies.limitRetries[IO](2),
  wasSuccessful = (i: Int) => IO.pure(i == 6),
  onFailure = retry.noop[IO, Int]
)

val httpClient = util.FlakyHttpClient()

// To retry only on errors that are worth retrying
IO(httpClient.getCatGif()).retryingOnSomeErrors(
  isWorthRetrying = isIOException,
  policy = RetryPolicies.limitRetries[IO](2),
  onError = retry.noop[IO, Throwable]
)

// To retry on all errors
IO(httpClient.getCatGif()).retryingOnAllErrors(
  policy = RetryPolicies.limitRetries[IO](2),
  onError = retry.noop[IO, Throwable]
)

// To retry only on errors and results that are worth retrying
IO(httpClient.getRecordDetails("foo")).retryingOnFailuresAndSomeErrors(
  wasSuccessful = (s: String) => IO.pure(s != "pending"),
  isWorthRetrying = isTimeoutException,
  policy = RetryPolicies.limitRetries[IO](2),
  onFailure = retry.noop[IO, String],
  onError = retry.noop[IO, Throwable]
)

// To retry all errors and results that are worth retrying
IO(httpClient.getRecordDetails("foo")).retryingOnFailuresAndAllErrors(
  wasSuccessful = (s: String) => IO.pure(s != "pending"),
  policy = RetryPolicies.limitRetries[IO](2),
  onFailure = retry.noop[IO, String],
  onError = retry.noop[IO, Throwable]
)