Skip to content

Commit 0b132fd

Browse files
committed
Use Cats Effect Random for randomness in fullJitter
Replace scala.util.Random with CE Random
1 parent 256e847 commit 0b132fd

File tree

3 files changed

+74
-48
lines changed

3 files changed

+74
-48
lines changed

build.sbt

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ inThisBuild(
4545
)
4646
)
4747

48-
val catsVersion = "2.12.0"
49-
val catsEffectVersion = "3.5.7"
50-
val catsMtlVersion = "1.5.0"
51-
val munitVersion = "1.0.0"
52-
val munitCatsEffectVersion = "2.0.0"
53-
val disciplineVersion = "2.0.0"
48+
val catsVersion = "2.12.0"
49+
val catsEffectVersion = "3.5.7"
50+
val catsMtlVersion = "1.5.0"
51+
val munitVersion = "1.0.0"
52+
val munitCatsEffectVersion = "2.0.0"
53+
val disciplineVersion = "2.0.0"
54+
val scalacheckEffectVersion = "1.0.4"
5455

5556
val core = crossProject(JVMPlatform, JSPlatform)
5657
.in(file("modules/core"))
@@ -60,10 +61,11 @@ val core = crossProject(JVMPlatform, JSPlatform)
6061
libraryDependencies ++= Seq(
6162
"org.typelevel" %%% "cats-core" % catsVersion,
6263
"org.typelevel" %%% "cats-effect" % catsEffectVersion,
63-
"org.scalameta" %%% "munit-scalacheck" % munitVersion % Test,
64-
"org.typelevel" %%% "munit-cats-effect" % munitCatsEffectVersion % Test,
65-
"org.typelevel" %%% "cats-laws" % catsVersion % Test,
66-
"org.typelevel" %%% "discipline-munit" % disciplineVersion % Test
64+
"org.scalameta" %%% "munit-scalacheck" % munitVersion % Test,
65+
"org.typelevel" %%% "munit-cats-effect" % munitCatsEffectVersion % Test,
66+
"org.typelevel" %%% "scalacheck-effect" % scalacheckEffectVersion % Test,
67+
"org.typelevel" %%% "cats-laws" % catsVersion % Test,
68+
"org.typelevel" %%% "discipline-munit" % disciplineVersion % Test
6769
),
6870
mimaPreviousArtifacts := Set.empty,
6971
Test / tpolecatExcludeOptions += ScalacOptions.warnNonUnitStatement

modules/core/shared/src/main/scala/retry/RetryPolicies.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package retry
33
import java.util.concurrent.TimeUnit
44

55
import cats.Applicative
6+
import cats.effect.std.Random
67
import cats.syntax.functor.*
78
import cats.syntax.show.*
89
import retry.PolicyDecision.*
910

1011
import scala.concurrent.duration.{Duration, FiniteDuration}
11-
import scala.util.Random
1212

1313
object RetryPolicies:
1414
private val LongMax: BigInt = BigInt(Long.MaxValue)
@@ -89,13 +89,15 @@ object RetryPolicies:
8989
/** "Full jitter" backoff algorithm. See
9090
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
9191
*/
92-
def fullJitter[F[_]: Applicative](baseDelay: FiniteDuration): RetryPolicy[F, Any] =
93-
RetryPolicy.liftWithShow(
92+
def fullJitter[F[_]: Applicative: Random](baseDelay: FiniteDuration): RetryPolicy[F, Any] =
93+
RetryPolicy.withShow(
9494
{ (_, status) =>
95-
val e = Math.pow(2.0, status.retriesSoFar.toDouble).toLong
96-
val maxDelay = safeMultiply(baseDelay, e)
97-
val delayNanos = (maxDelay.toNanos * Random.nextDouble()).toLong
98-
DelayAndRetry(new FiniteDuration(delayNanos, TimeUnit.NANOSECONDS))
95+
val e = Math.pow(2.0, status.retriesSoFar.toDouble).toLong
96+
val maxDelay = safeMultiply(baseDelay, e)
97+
Random[F].nextDouble.map { rnd =>
98+
val delayNanos = (maxDelay.toNanos * rnd).toLong
99+
DelayAndRetry(new FiniteDuration(delayNanos, TimeUnit.NANOSECONDS))
100+
}
99101
},
100102
show"fullJitter(baseDelay=$baseDelay)"
101103
)

modules/core/shared/src/test/scala/retry/RetryPoliciesSuite.scala

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@ package retry
22

33
import java.util.concurrent.TimeUnit
44

5-
import retry.RetryPolicies.*
65
import cats.Id
6+
import cats.effect.IO
7+
import cats.effect.std.Random
8+
import cats.syntax.all.*
79
import org.scalacheck.{Arbitrary, Gen}
810
import org.scalacheck.Prop.forAll
9-
import munit.ScalaCheckSuite
11+
import org.scalacheck.effect.PropF
12+
import munit.{CatsEffectSuite, ScalaCheckSuite}
1013
import retry.PolicyDecision.{DelayAndRetry, GiveUp}
14+
import retry.RetryPolicies.*
1115

1216
import scala.concurrent.duration.*
1317
import munit.Location
1418

15-
class RetryPoliciesSuite extends ScalaCheckSuite:
19+
class RetryPoliciesSuite extends CatsEffectSuite with ScalaCheckSuite:
1620

1721
given Arbitrary[RetryStatus] = Arbitrary {
1822
for
@@ -29,14 +33,14 @@ class RetryPoliciesSuite extends ScalaCheckSuite:
2933
val genFiniteDuration: Gen[FiniteDuration] =
3034
Gen.posNum[Long].map(FiniteDuration(_, TimeUnit.NANOSECONDS))
3135

32-
given Arbitrary[RetryPolicy[Id, Any]] = Arbitrary {
36+
given (using Random[IO]): Arbitrary[RetryPolicy[IO, Any]] = Arbitrary {
3337
Gen.oneOf(
34-
Gen.const(alwaysGiveUp[Id]),
35-
genFiniteDuration.map(delay => constantDelay[Id](delay)),
36-
genFiniteDuration.map(baseDelay => exponentialBackoff[Id](baseDelay)),
37-
Gen.posNum[Int].map(maxRetries => limitRetries[Id](maxRetries)),
38-
genFiniteDuration.map(baseDelay => fibonacciBackoff[Id](baseDelay)),
39-
genFiniteDuration.map(baseDelay => fullJitter[Id](baseDelay))
38+
Gen.const(alwaysGiveUp[IO]),
39+
genFiniteDuration.map(delay => constantDelay[IO](delay)),
40+
genFiniteDuration.map(baseDelay => exponentialBackoff[IO](baseDelay)),
41+
Gen.posNum[Int].map(maxRetries => limitRetries[IO](maxRetries)),
42+
genFiniteDuration.map(baseDelay => fibonacciBackoff[IO](baseDelay)),
43+
genFiniteDuration.map(baseDelay => fullJitter[IO](baseDelay))
4044
)
4145
}
4246

@@ -94,39 +98,57 @@ class RetryPoliciesSuite extends ScalaCheckSuite:
9498
}
9599

96100
test("fullJitter - implement the AWS Full Jitter backoff algorithm") {
97-
val policy = fullJitter[Id](100.milliseconds)
101+
val mkPolicy: IO[RetryPolicy[IO, Any]] = Random.scalaUtilRandom[IO].map { rnd =>
102+
given Random[IO] = rnd
103+
fullJitter[IO](100.milliseconds)
104+
}
98105
val arbitraryCumulativeDelay = 999.milliseconds
99106
val arbitraryPreviousDelay = Some(999.milliseconds)
100107

101-
def check(retriesSoFar: Int, expectedMaximumDelay: FiniteDuration): Unit =
108+
case class TestCase(retriesSoFar: Int, expectedMaximumDelay: FiniteDuration)
109+
110+
def check(testCase: TestCase): IO[Unit] =
102111
val status = RetryStatus(
103-
retriesSoFar,
112+
testCase.retriesSoFar,
104113
arbitraryCumulativeDelay,
105114
arbitraryPreviousDelay
106115
)
107-
for _ <- 1 to 1000 do
108-
val verdict = policy.decideNextRetry((), status)
109-
val delay = verdict.asInstanceOf[PolicyDecision.DelayAndRetry].delay
110-
assert(delay >= Duration.Zero)
111-
assert(delay < expectedMaximumDelay)
116+
(1 to 1000).toList.traverse_ { i =>
117+
for
118+
policy <- mkPolicy
119+
verdict <- policy.decideNextRetry((), status)
120+
yield
121+
val delay = verdict.asInstanceOf[PolicyDecision.DelayAndRetry].delay
122+
assert(clue(delay) >= Duration.Zero)
123+
assert(clue(delay) < clue(testCase.expectedMaximumDelay))
124+
}
112125

113-
check(0, 100.milliseconds)
114-
check(1, 200.milliseconds)
115-
check(2, 400.milliseconds)
116-
check(3, 800.milliseconds)
117-
check(4, 1600.milliseconds)
118-
check(5, 3200.milliseconds)
126+
val cases = List(
127+
TestCase(0, 100.milliseconds),
128+
TestCase(1, 200.milliseconds),
129+
TestCase(2, 400.milliseconds),
130+
TestCase(3, 800.milliseconds),
131+
TestCase(4, 1600.milliseconds),
132+
TestCase(5, 3200.milliseconds)
133+
)
134+
135+
cases.traverse_(check)
119136
}
120137

121-
property(
138+
test(
122139
"all built-in policies - never try to create a FiniteDuration of more than Long.MaxValue nanoseconds"
123140
) {
124-
forAll((policy: RetryPolicy[Id, Any], status: RetryStatus) =>
125-
policy.decideNextRetry((), status) match
126-
case PolicyDecision.DelayAndRetry(nextDelay) =>
127-
nextDelay.toNanos <= Long.MaxValue
128-
case PolicyDecision.GiveUp => true
129-
)
141+
Random.scalaUtilRandom[IO].map { rnd =>
142+
given Random[IO] = rnd
143+
PropF.forAllF((policy: RetryPolicy[IO, Any], status: RetryStatus) =>
144+
policy.decideNextRetry((), status).map {
145+
case PolicyDecision.DelayAndRetry(nextDelay) =>
146+
assert(nextDelay.toNanos <= Long.MaxValue)
147+
case PolicyDecision.GiveUp =>
148+
assert(true)
149+
}
150+
)
151+
}
130152
}
131153

132154
property("limitRetries - retry with no delay until the limit is reached") {

0 commit comments

Comments
 (0)