Implement `Retry` functionality
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:
- A confusion between
withMaxDelay,withCappedDelay,withMaxCumulativeDelay. Even though I provided the documentation with examples, these three methods are confusing. Can we find better names? - Is my implementation of the exponential backoff correct?
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'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.
Is there some plan to release this? I would love to have also too
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.
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?)