Skip to content
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

Exponential caped delay and with random factor #193

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions modules/core/shared/src/main/scala/retry/RetryPolicies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.util.concurrent.TimeUnit
import cats.Applicative
import cats.syntax.functor._
import cats.syntax.show._
import cats.instances.double._
import cats.instances.finiteDuration._
import cats.instances.int._
import retry.PolicyDecision._
Expand Down Expand Up @@ -60,6 +61,24 @@ object RetryPolicies {
DelayAndRetry(delay)
}, show"exponentialBackOff(baseDelay=$baseDelay)")

/**
* Each delay is twice as long as the previous one and caped by max delay. Never give up.
* Caped with max delay.
*/
def exponentialBackoffCaped[M[_]: Applicative](
minDelay: FiniteDuration,
maxDelay: FiniteDuration
): RetryPolicy[M] =
RetryPolicy.liftWithShow(
{ status =>
val delay =
safeMultiply(minDelay, Math.pow(2, status.retriesSoFar).toLong)
.min(maxDelay)
DelayAndRetry(delay)
},
show"exponentialBackOffCaped(minDelay=$minDelay, maxDelay=$maxDelay)"
)

/**
* Retry without delay, giving up after the given number of retries.
*/
Expand Down Expand Up @@ -158,4 +177,36 @@ object RetryPolicies {
show"limitRetriesByCumulativeDelay(threshold=$threshold, $policy)"
)
}

/**
* An analog for
* Akka restartable source https://doc.akka.io/api/akka/current/akka/stream/scaladsl/RestartSource$.html
* @param minDelay based delay for restarts
* @param maxDelay maximal (cap) delay for restarts
* @param randomFactor randomFactor for increasing delay
* @param maxRestarts maximal restarts count. pass -1 to infinite retries.
*/
def exponentialBackoffCapedRandom[M[_]: Applicative](
minDelay: FiniteDuration,
maxDelay: FiniteDuration,
randomFactor: Double,
maxRestarts: Int
): RetryPolicy[M] = {
RetryPolicy
.liftWithShow[M](
{ status =>
if (maxRestarts != -1 && status.retriesSoFar >= maxRestarts) {
GiveUp
} else {
val delayNanos =
(safeMultiply(minDelay, Math.pow(2, status.retriesSoFar).toLong) *
(1.0 + Random.nextDouble() * randomFactor)).min(maxDelay)
DelayAndRetry(
new FiniteDuration(delayNanos.toNanos, TimeUnit.NANOSECONDS)
)
}
},
show"exponentialRandomBackoffRandom(maxDelay=$minDelay, maxDelay=$maxDelay, randomFactor=$randomFactor, maxRestarts=$maxRestarts)"
)
}
}
77 changes: 77 additions & 0 deletions modules/core/shared/src/test/scala/retry/RetryPoliciesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ class RetryPoliciesSpec extends AnyFlatSpec with Checkers {
s"exponentialBackoff($baseDelay)"
)
),
for {
delay1 <- genFiniteDuration
delay2 <- genFiniteDuration
} yield {
val min = delay1 min delay2
val max = delay1 max delay2
LabelledRetryPolicy(
exponentialBackoffCaped[Id](min, max),
s"exponentialBackoffCaped($min, $max)"
)
},
for {
delay1 <- genFiniteDuration
delay2 <- genFiniteDuration
maxRetries <- Gen.posNum[Int]
randomFactor <- Gen.posNum[Double]
} yield {
val min = delay1 min delay2
val max = delay1 max delay2
LabelledRetryPolicy(
exponentialBackoffCapedRandom[Id](min, max, randomFactor, maxRetries),
s"exponentialBackoffCapedRandom($min, $max, $randomFactor, $maxRetries)"
)
},
Gen
.posNum[Int]
.map(maxRetries =>
Expand Down Expand Up @@ -102,6 +126,59 @@ class RetryPoliciesSpec extends AnyFlatSpec with Checkers {
test(3, 800.milliseconds)
}

behavior of "exponentialBackoffCaped"

it should "start with the base delay, double the delay after each iteration and caped by max delay" in {
val policy = exponentialBackoffCaped[Id](100.milliseconds, 500.milliseconds)
val arbitraryCumulativeDelay = 999.milliseconds
val arbitraryPreviousDelay = Some(999.milliseconds)

def test(retriesSoFar: Int, expectedDelay: FiniteDuration) = {
val status = RetryStatus(
retriesSoFar,
arbitraryCumulativeDelay,
arbitraryPreviousDelay
)
val verdict = policy.decideNextRetry(status)
assert(verdict == PolicyDecision.DelayAndRetry(expectedDelay))
}

test(0, 100.milliseconds)
test(1, 200.milliseconds)
test(2, 400.milliseconds)
test(3, 500.milliseconds)
test(4, 500.milliseconds)
}

behavior of "exponentialBackoffCapedRandom"

it should "start with the base delay, double the delay after each iteration and caped by max delay" in {
val policy = exponentialBackoffCapedRandom[Id](
100.milliseconds,
500.milliseconds,
0.0,
4
)
val arbitraryCumulativeDelay = 999.milliseconds
val arbitraryPreviousDelay = Some(999.milliseconds)

def test(retriesSoFar: Int, expectedDecision: PolicyDecision) = {
val status = RetryStatus(
retriesSoFar,
arbitraryCumulativeDelay,
arbitraryPreviousDelay
)
val verdict = policy.decideNextRetry(status)
assert(verdict == expectedDecision)
}

test(0, DelayAndRetry(100.milliseconds))
test(1, DelayAndRetry(200.milliseconds))
test(2, DelayAndRetry(400.milliseconds))
test(3, DelayAndRetry(500.milliseconds))
test(4, GiveUp)
}

behavior of "fibonacciBackoff"

it should "start with the base delay and increase the delay in a Fibonacci-y way" in {
Expand Down