From 10a8cb3af9806944b5686cb5a56997e94fffed62 Mon Sep 17 00:00:00 2001 From: Maksim Ochenashko Date: Tue, 14 Jan 2020 13:23:42 +0200 Subject: [PATCH] Add documentation --- build.sbt | 2 +- modules/docs/src/main/mdoc/docs/index.md | 1 + .../src/main/mdoc/docs/mtl-combinators.md | 205 ++++++++++++++++++ .../main/resources/microsite/data/menu.yml | 2 + 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 modules/docs/src/main/mdoc/docs/mtl-combinators.md diff --git a/build.sbt b/build.sbt index 057f294f..d8d72068 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ val mtlJS = mtlRetry.js val docs = project .in(file("modules/docs")) - .dependsOn(coreJVM, alleycatsJVM) + .dependsOn(coreJVM, alleycatsJVM, mtlJVM) .enablePlugins(MicrositesPlugin, BuildInfoPlugin) .settings(moduleSettings) .settings( diff --git a/modules/docs/src/main/mdoc/docs/index.md b/modules/docs/src/main/mdoc/docs/index.md index 70cda140..c450ce97 100644 --- a/modules/docs/src/main/mdoc/docs/index.md +++ b/modules/docs/src/main/mdoc/docs/index.md @@ -112,5 +112,6 @@ logMessages.foreach(println) Next steps: * Learn about the other available [combinators](combinators.html) +* Learn about the [MTL combinators](mtl-combinators.html) * Learn more about [retry policies](policies.html) * Learn about the [`Sleep` type class](sleep.html) diff --git a/modules/docs/src/main/mdoc/docs/mtl-combinators.md b/modules/docs/src/main/mdoc/docs/mtl-combinators.md new file mode 100644 index 00000000..e728dd56 --- /dev/null +++ b/modules/docs/src/main/mdoc/docs/mtl-combinators.md @@ -0,0 +1,205 @@ +--- +layout: docs +title: MTL Combinators +--- + +# MTL Combinators + +The `cats-retry-mtl` module provides two additional retry methods that operating with errors produced +by `ApplicativeHandle` from [cats-mtl](https://github.com/typelevel/cats-mtl). + +## Installation + +To use `cats-retry-mtl`, add the following dependency to your `build.sbt`: +```scala mdoc:passthrough +println( + s""" + |``` + |val catsRetryVersion = "${retry.BuildInfo.version.replaceFirst("\\+.*", "")}" + |libraryDependencies += "com.github.cb372" %% "cats-retry-mtl" % catsRetryVersion + |``` + |""".stripMargin.trim +) +``` + +## Interaction with MonadError retry + +MTL retry works independently from `retry.retryingOnSomeErrors`. The operations `retry.mtl.retryingOnAllErrors` and +`retry.mtl.retryingOnSomeErrors` evaluating retry exclusively on errors produced by `ApplicativeHandle`. +Thus errors produced by `MonadError` are not being taken into account and retry is not triggered. + +If you want to retry in case of any error, just chain the methods: +```scala +fa + .retryingOnAllErrors(policy, onError = retry.noop[F, Throwable]) + .retryingOnAllErrorsMtl[AppError](policy, onError = retry.noop[F, AppError]) +``` + +## `retryingOnSomeErrors` + +This is useful when you are working with an `ApplicativeHandle[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: + +```scala +def retryingOnSomeErrors[M[_]: Monad, A, E: ApplicativeHandle[M, *]]( + policy: RetryPolicy[M], + isWorthRetrying: E => Boolean, + onError: (E, RetryDetails) => M[Unit] +)(action: => M[A]): 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 + +Example: +```scala mdoc +import retry.{RetryDetails, RetryPolicies} +import cats.data.EitherT +import cats.effect.{Sync, IO, Timer} +import cats.mtl.ApplicativeHandle +import cats.mtl.instances.handle._ +import scala.concurrent.duration._ + +// We need an implicit cats.effect.Timer +implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + +type Effect[A] = EitherT[IO, AppError, A] + +case class AppError(reason: String) + +def failingOperation[F[_]: ApplicativeHandle[*[_], AppError]]: F[Unit] = + ApplicativeHandle[F, AppError].raise(AppError("Boom!")) + +def isWorthRetrying(error: AppError): Boolean = + error.reason.contains("Boom!") + +def logError[F[_]: Sync](error: AppError, details: RetryDetails): F[Unit] = + Sync[F].delay(println(s"Raised error $error. Details $details")) + +val policy = RetryPolicies.limitRetries[Effect](2) + +retry.mtl + .retryingOnSomeErrors(policy, isWorthRetrying, logError[Effect])(failingOperation[Effect]) + .value + .unsafeRunTimed(1.second) +``` + +## `retryingOnAllErrors` + +This is useful when you are working with a `ApplicatieHandle[M, E]` and you want to +retry on all errors. + +The API (modulo some type-inference trickery) looks like this: + +```scala +def retryingOnSomeErrors[M[_]: Monad, A, E: ApplicativeHandle[M, *]]( + policy: RetryPolicy[M], + onError: (E, RetryDetails) => M[Unit] +)(action: => M[A]): 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 + +Example: +```scala mdoc:reset +import retry.{RetryDetails, RetryPolicies} +import cats.data.EitherT +import cats.effect.{Sync, IO, Timer} +import cats.mtl.ApplicativeHandle +import cats.mtl.instances.handle._ +import scala.concurrent.duration._ + +// We need an implicit cats.effect.Timer +implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + +type Effect[A] = EitherT[IO, AppError, A] + +case class AppError(reason: String) + +def failingOperation[F[_]: ApplicativeHandle[*[_], AppError]]: F[Unit] = + ApplicativeHandle[F, AppError].raise(AppError("Boom!")) + +def logError[F[_]: Sync](error: AppError, details: RetryDetails): F[Unit] = + Sync[F].delay(println(s"Raised error $error. Details $details")) + +val policy = RetryPolicies.limitRetries[Effect](2) + +retry.mtl + .retryingOnAllErrors(policy, logError[Effect])(failingOperation[Effect]) + .value + .unsafeRunTimed(1.second) +``` + +## Syntactic sugar + +Cats-retry-mtl include some syntactic sugar in order to reduce boilerplate. + +```scala mdoc:reset +import retry._ +import cats.data.EitherT +import cats.effect.{Sync, IO, Timer} +import cats.syntax.functor._ +import cats.syntax.flatMap._ +import cats.mtl.ApplicativeHandle +import cats.mtl.instances.handle._ +import retry.mtl.syntax.all._ +import retry.syntax.all._ +import scala.concurrent.duration._ + +case class AppError(reason: String) + +class Service[F[_]: Timer](client: util.FlakyHttpClient)(implicit F: Sync[F], AH: ApplicativeHandle[F, AppError]) { + + // evaluates retry exclusively on errors produced by ApplicativeHandle. + def findCoolCatGifRetryMtl(policy: RetryPolicy[F]): F[String] = + findCoolCatGif.retryingOnAllErrorsMtl[AppError](policy, logMtlError) + + // evaluates retry on errors produced by MonadError and ApplicativeHandle + def findCoolCatGifRetryAll(policy: RetryPolicy[F]): F[String] = + findCoolCatGif + .retryingOnAllErrors(policy, logError) + .retryingOnAllErrorsMtl[AppError](policy, logMtlError) + + private def findCoolCatGif: F[String] = + for { + gif <- findCatGif + _ <- isCoolGif(gif) + } yield gif + + private def findCatGif: F[String] = + F.delay(client.getCatGif()) + + private def isCoolGif(string: String): F[Unit] = + if (string.contains("cool")) F.unit + else AH.raise(AppError("Gif is not cool")) + + private def logError(error: Throwable, details: RetryDetails): F[Unit] = + F.delay(println(s"Raised error $error. Details $details")) + + private def logMtlError(error: AppError, details: RetryDetails): F[Unit] = + F.delay(println(s"Raised MTL error $error. Details $details")) +} + +type Effect[A] = EitherT[IO, AppError, A] + +implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + +val policy = RetryPolicies.limitRetries[Effect](5) + +val service = new Service[Effect](util.FlakyHttpClient()) + +service.findCoolCatGifRetryMtl(policy).value.attempt.unsafeRunTimed(1.second) +service.findCoolCatGifRetryAll(policy).value.attempt.unsafeRunTimed(1.second) +``` diff --git a/modules/docs/src/main/resources/microsite/data/menu.yml b/modules/docs/src/main/resources/microsite/data/menu.yml index 0a78f4fd..c4921cd8 100644 --- a/modules/docs/src/main/resources/microsite/data/menu.yml +++ b/modules/docs/src/main/resources/microsite/data/menu.yml @@ -3,6 +3,8 @@ options: url: docs/index.html - title: Combinators url: docs/combinators.html + - title: MTL Combinators + url: docs/mtl-combinators.html - title: Retry policies url: docs/policies.html - title: Sleep