-
Notifications
You must be signed in to change notification settings - Fork 529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Retry functionality #3135
base: series/3.x
Are you sure you want to change the base?
Retry functionality #3135
Conversation
The core module depends on |
baseDelay: FiniteDuration | ||
): Retry[F] = | ||
Retry.lift[F] { status => | ||
val delay = safeMultiply(baseDelay, Math.pow(2, status.retriesSoFar).toLong) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make the base (2
) configurable? It could still be the default as many people will want to use it as their base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can probably add it as a param with a default, yeah
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An overload is better for bincompat 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or a builder? Overloads tend to lead to combinatorial explosion
By the way, I'm explicitly seeking feedback on the interaction between |
would you mind showing an example of this? |
For example exponentialBackoff(1.ms)
.selectError {
case NullPointerException => true
case _ => false
}.capDelay(500.ms) // with the old implementation of capDelay the resulting policy will retry all errors, not just |
def mapK[G[_]: Monad](f: F ~> G): Retry[G] | ||
} | ||
object Retry { | ||
final case class Status( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Case classes tend to be fiddly from a bincompat perspective - could this be package-private and abstracted behind a sealed trait?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General thoughts…
I like where this is headed. It does feel slightly odd to me that we have all these combinators on Retry
itself, but at the same time the only way to make a stateful decision chain is to do so using Retry.lift
(which should be renamed but that's another topic). It feels like the set of combinators is incomplete somehow. Maybe the decision calculus is what's incomplete? I haven't given it too much thought.
@@ -0,0 +1,666 @@ | |||
package cats.effect.std | |||
package retry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming OldRetry
is only here for comparison purposes?
def liftF[F[_]: Monad]( | ||
nextRetry: Retry.Status => F[Retry.Decision] | ||
): Retry[F] = | ||
Retry((status, _) => nextRetry(status)) | ||
|
||
def lift[F[_]: Monad]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like an odd use of the lift
nomenclature. I probably would have gone with apply
or something like that, though I'm not sure what to do about the non-functor variant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe def byStatus
?
/** | ||
* Each delay is twice as long as the previous one. Never give up. | ||
*/ | ||
def exponentialBackoff[F[_]: Monad]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL people actually use exponential backoff without jitter. I had no idea that was even a useful thing to do…
* "Full jitter" backoff algorithm. See | ||
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ | ||
*/ | ||
def fullJitter[F[_]: Monad](baseDelay: FiniteDuration): Retry[F] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continuing on my thought from above… It feels like this should be the algorithm that we're encouraging people to reach for first, but the name definitely does not hint in that direction. I had to actually look up the AWS Engineering blog to figure out that this is actually the algorithm that I generally identify with "exponential backoff".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels a little odd to me that these are methods rather than some kind of Strategy
that encapsulates the (previousBackoff: FiniteDuration) => (nextBackoff: F[FiniteDuration])
If they were structured in that way, we could provide easier guidance by having the strategies be available on object Strategy
with def default
being this recommended one
|
||
val noRetriesYet = Retry.Status(0, Duration.Zero, None) | ||
|
||
def retry[F[_]: Temporal, A](r: Retry[F], action: F[A]): F[A] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this not a member of Retry
?
def retry(r: Retry[F])(implicit F: Temporal[F]): F[A] = | ||
Retry.retry(r, wrapped) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also add some convenience syntax for exponential backoff?
Retry.lift[F] { status => | ||
val e = Math.pow(2, status.retriesSoFar.toDouble).toLong | ||
val maxDelay = safeMultiply(baseDelay, e) | ||
val delayNanos = (maxDelay.toNanos * Random.nextDouble()).toLong |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use cats.effect.std.Random
?
Fixes #1459
I had originally started with a direct port of cats-retry, but since then I've changed the design significantly enough to warrant a proper review of the api before I port tests and docs.
Everything is in
Retry.scala
, here are some of the relevant decisions:X => F[Y]
.ensure
instead.TODO:
I'm afraid usage withIO
would require a syntax import since this lives instd
.