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
val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO {
httpClient.getCatGif()
}
// flakyRequest: IO[String] = Delay(thunk = <function0>)
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 = "3.1.0"
libraryDependencies += "com.github.cb372" %% "cats-retry" % 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._
val retryFiveTimes = RetryPolicies.limitRetries[IO](5)
// retryFiveTimes: RetryPolicy[IO] = RetryPolicy(
// decideNextRetry = retry.RetryPolicy$$$Lambda$11987/1347236281@5dcb8c0b
// )
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 cats.effect.IO
import scala.concurrent.duration.FiniteDuration
import retry._
import retry.RetryDetails._
val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()
val flakyRequest: IO[String] = IO {
httpClient.getCatGif()
}
// flakyRequest: IO[String] = Delay(thunk = <function0>)
val logMessages = collection.mutable.ArrayBuffer.empty[String]
// logMessages: collection.mutable.ArrayBuffer[String] = ArrayBuffer(
// "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."
// )
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")
}
}
// Now we have a retry policy and an error handler, we can wrap our `IO` inretries.
import cats.effect.unsafe.implicits.global
val flakyRequestWithRetry: IO[String] =
retryingOnAllErrors[String](
policy = RetryPolicies.limitRetries[IO](5),
onError = logError
)(flakyRequest)
// flakyRequestWithRetry: IO[String] = FlatMap(
// ioe = FlatMap(
// ioe = Attempt(ioa = Delay(thunk = <function0>)),
// f = retry.package$RetryingOnSomeErrorsPartiallyApplied$$Lambda$12035/340735956@59d52751
// ),
// f = cats.StackSafeMonad$$Lambda$11991/1077262642@58ff43b4
// )
// Let's see it in action.
flakyRequestWithRetry.unsafeRunSync()
// res2: String = "cute cat gets sleepy and falls asleep"
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:
- Learn about the other available combinators
- Learn about the MTL combinators
- Learn more about retry policies
- Learn about the
Sleep
type class