Getting started

Let’s start with a realistic example.

In order to provide business value to our stakeholders, we need to download a textual description of a cat gif.

Unfortunately we have to do this over a flaky network connection, so there’s a high probability it will fail.

We’ll be working with the cats-effect IO monad, but any monad will do.

import cats.effect.IO
// import cats.effect.IO

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()

val flakyRequest: IO[String] = IO {
  httpClient.getCatGif()
}
// flakyRequest: cats.effect.IO[String] = IO$1974131244

To improve the chance of successfully downloading the file, let’s wrap this with some retry logic.

We’ll add dependencies on the core and cats-effect modules:

val catsRetryVersion = "0.2.5"
libraryDependencies ++= Seq(
  "com.github.cb372" %% "cats-retry-core"        % catsRetryVersion,
  "com.github.cb372" %% "cats-retry-cats-effect" % catsRetryVersion
)

(Note: if you’re using Scala.js, you’ll need a %%% instead of %%.)

First we’ll need a retry policy. We’ll keep it simple: retry up to 5 times, with no delay between attempts. (See the retry policies page for information on more powerful policies).

import retry._
// import retry._

val retryFiveTimes = RetryPolicies.limitRetries[IO](5)
// retryFiveTimes: retry.RetryPolicy[cats.effect.IO] = RetryPolicy(retry.RetryPolicy$$$Lambda$7734/1971256031@6d137248)

We’ll also provide an error handler that does some logging before every retry. Note how this also happens within whatever monad you’re working in, in this case the IO monad.

import scala.concurrent.duration.FiniteDuration
// import scala.concurrent.duration.FiniteDuration

import retry.RetryDetails._
// import retry.RetryDetails._

val logMessages = collection.mutable.ArrayBuffer.empty[String]
// logMessages: scala.collection.mutable.ArrayBuffer[String] = ArrayBuffer()

def logError(err: Throwable, details: RetryDetails): IO[Unit] = details match {

  case WillDelayAndRetry(nextDelay: FiniteDuration,
                         retriesSoFar: Int,
                         cumulativeDelay: FiniteDuration) =>
    IO {
      logMessages.append(
        s"Failed to download. So far we have retried $retriesSoFar times.")
    }

  case GivingUp(totalRetries: Int, totalDelay: FiniteDuration) =>
    IO {
      logMessages.append(s"Giving up after $totalRetries retries")
    }

}
// logError: (err: Throwable, details: retry.RetryDetails)cats.effect.IO[Unit]

Now we have a retry policy and an error handler, we can wrap our IO in retries.

// We need an implicit cats.effect.Timer
import cats.effect.Timer
// import cats.effect.Timer

import scala.concurrent.ExecutionContext.global
// import scala.concurrent.ExecutionContext.global

implicit val timer: Timer[IO] = IO.timer(global)
// timer: cats.effect.Timer[cats.effect.IO] = cats.effect.internals.IOTimer@75acec40

// This is so we can use that Timer to perform delays between retries
import retry.CatsEffect._
// import retry.CatsEffect._

val flakyRequestWithRetry: IO[String] =
  retryingOnAllErrors[String](
    policy = retryFiveTimes,
    onError = logError
  )(flakyRequest)
// flakyRequestWithRetry: cats.effect.IO[String] = IO$40647647

Let’s see it in action.

scala> flakyRequestWithRetry.unsafeRunSync()
res3: String = cute cat gets sleepy and falls asleep

scala> logMessages.foreach(println)
Failed to download. So far we have retried 0 times.
Failed to download. So far we have retried 1 times.
Failed to download. So far we have retried 2 times.
Failed to download. So far we have retried 3 times.

Next steps: