cats-effect icon indicating copy to clipboard operation
cats-effect copied to clipboard

Implement `Retry` functionality

Open iRevive opened this issue 1 year ago • 5 comments

The PR is influenced by #3135.

In my opinion, Retry must carry the error type. That way, we can implement retry functionality on top of the Handle from the cats-mtl.

I also decided to keep the name (perhaps the 'description' fits better?) around. That way, toString provides enough details to understand the retry strategy:

val policy = Retry
  .exponentialBackoff[IO, Throwable](1.second)
  .withCappedDelay(2.seconds)
  .withMaxRetries(10)
  
println(policy)
// MaxRetries(CapDelay(Backoff(baseDelay=1 second, multiplier=Const(2.0), randomizationFactor=0.5), cap=2 seconds), max=5)

Usage

Retry on all errors

val policy = Retry
  .exponentialBackoff[IO, Throwable](1.second)
  .withMaxRetries(10)

// retries 10 times at most using an exponential backoff strategy
IO.raiseError(new RuntimeException("oops")).retry(policy)

Retry on some errors (e.g. TimeoutException)

val policy = Retry
  .exponentialBackoff[IO, Throwable](1.second)
  .withMaxRetries(10)
  .withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[TimeoutException])

// retries 10 times at most using an exponential backoff strategy
IO.raiseError(new TimeoutException("timeout")).retry(policy)

// gives up immediately
IO.raiseError(new RuntimeException("oops")).retry(policy)

Retry on all errors except the TimeoutException

val policy = Retry
  .exponentialBackoff[IO, Throwable](1.second)
  .withMaxRetries(10)
  .withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[TimeoutException])

// retries 10 times at most using an exponential backoff strategy
IO.raiseError(new RuntimeException("oops")).retry(policy)

// gives up immediately
IO.raiseError(new TimeoutException("timeout")).retry(policy)

A few points to discuss:

  1. A confusion between withMaxDelay, withCappedDelay, withMaxCumulativeDelay. Even though I provided the documentation with examples, these three methods are confusing. Can we find better names?
  2. Is my implementation of the exponential backoff correct?

iRevive avatar Jul 26 '24 17:07 iRevive

I'd really love for this to land 🚀 - but I see there's been no comments for 4 months - @iRevive is this waiting on feedback or approvals, or is discussion ongoing somewhere off Github?

TobiasRoland avatar Nov 05 '24 12:11 TobiasRoland

I'd really love for this to land 🚀 - but I see there's been no comments for 4 months - @iRevive is this waiting on feedback or approvals, or is discussion ongoing somewhere off Github?

I'm still waiting for the feedback.

iRevive avatar Nov 05 '24 12:11 iRevive

Is there some plan to release this? I would love to have also too

albertoadami avatar Feb 28 '25 21:02 albertoadami

The current implementation has some flaws. For example, writing a retry policy for the following scenario is complicated: an effect is completed successfully, but the value is worth retrying (e.g., http.status_code = 400.

I will experiment with different API encoding this month.

iRevive avatar Mar 02 '25 09:03 iRevive

In https://github.com/biochimia/scala-retry, I've toyed with an approach where the retry policy (Retry.Strategy, in that project) boils down to a "factory" of retries. The retries themselves are represented as an Iterator[FiniteDuration]. Relying on an iterator makes it easy to implement and combine retry policies.

In that approach, the error conditions on which to retry are managed separately in a retryOn method. This is similar to the use of recover/recoverWith methods.

The actual implementation of the retry loop will be specific to the monad. In the project above, I added only an implementation for cats.Eval that uses Thread.sleep. With IO we'd use IO.sleep.

I don't intend to derail the current PR, but wanted to explore a bit the interface possibilities. (Is there a better place for this discussion outside the current PR?)

biochimia avatar Apr 14 '25 08:04 biochimia