Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Jan 14, 2020
1 parent ff79263 commit 10a8cb3
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions modules/docs/src/main/mdoc/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
205 changes: 205 additions & 0 deletions modules/docs/src/main/mdoc/docs/mtl-combinators.md
Original file line number Diff line number Diff line change
@@ -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)
```
2 changes: 2 additions & 0 deletions modules/docs/src/main/resources/microsite/data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 10a8cb3

Please sign in to comment.