From a0f4451cd1357fcf1df9af1de3ab839d6d70fc19 Mon Sep 17 00:00:00 2001 From: zkerriga Date: Sat, 27 Apr 2024 22:49:35 +0200 Subject: [PATCH 1/4] add ability to combine Schedules --- .../main/scala/ox/resilience/Schedule.scala | 24 +++++++++ .../ScheduleCombinationRetryTest.scala | 52 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala diff --git a/core/src/main/scala/ox/resilience/Schedule.scala b/core/src/main/scala/ox/resilience/Schedule.scala index 7da415ec..51f768b3 100644 --- a/core/src/main/scala/ox/resilience/Schedule.scala +++ b/core/src/main/scala/ox/resilience/Schedule.scala @@ -117,3 +117,27 @@ object Schedule: extends Infinite: override def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration = Backoff.nextDelay(attempt, initialDelay, maxDelay, jitter, lastDelay) + + private[resilience] sealed trait Combined extends Schedule: + val base: Finite + val fallback: Schedule + override def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration = + if base.maxRetries > attempt then base.nextDelay(attempt, lastDelay) + else fallback.nextDelay(attempt - base.maxRetries, lastDelay) + + /** A schedule that combines two schedules, using [[base]] first [[base.maxRetries]] number of times, and then using [[fallback]] + * [[fallback.maxRetries]] number of times. + */ + case class Combination(base: Finite, fallback: Finite) extends Combined, Finite: + override def maxRetries: Int = base.maxRetries + fallback.maxRetries + + object Combination: + /** A schedule that retries indefinitely, using [[base]] first [[base.maxRetries]] number of times, and then always using [[fallback]]. + */ + def forever(base: Finite, fallback: Infinite): Infinite = CombinationForever(base, fallback) + + case class CombinationForever private[resilience](base: Finite, fallback: Infinite) extends Combined, Infinite + + extension (schedule: Finite) + def fallbackTo(fallback: Finite): Finite = Combination(schedule, fallback) + def fallbackTo(fallback: Infinite): Infinite = Combination.forever(schedule, fallback) diff --git a/core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala b/core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala new file mode 100644 index 00000000..86db2dfa --- /dev/null +++ b/core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala @@ -0,0 +1,52 @@ +package ox.resilience + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import ox.ElapsedTime + +import scala.concurrent.duration.* + +class ScheduleCombinationRetryTest extends AnyFlatSpec with Matchers with ElapsedTime: + behavior of "retry with combination of schedules" + + it should "retry 3 times immediately and then 2 times with delay" in { + // given + var counter = 0 + val sleep = 100.millis + val immediateRetries = 3 + val delayedRetries = 2 + + def f = + counter += 1 + throw new RuntimeException("boom") + + val schedule = Schedule.Immediate(immediateRetries).fallbackTo(Schedule.Delay(delayedRetries, sleep)) + + // when + val (result, elapsedTime) = measure(the[RuntimeException] thrownBy retry(RetryPolicy(schedule))(f)) + + // then + result should have message "boom" + counter shouldBe immediateRetries + delayedRetries + 1 + elapsedTime.toMillis should be >= 2 * sleep.toMillis + } + + it should "retry forever" in { + // given + var counter = 0 + val retriesUntilSuccess = 1_000 + val successfulResult = 42 + + def f = + counter += 1 + if counter <= retriesUntilSuccess then throw new RuntimeException("boom") else successfulResult + + val schedule = Schedule.Immediate(100).fallbackTo(Schedule.Delay.forever(2.millis)) + + // when + val result = retry(RetryPolicy(schedule))(f) + + // then + result shouldBe successfulResult + counter shouldBe retriesUntilSuccess + 1 + } From c616b2faadaf4d8f1f0dc3384bb6d81cbc51dba5 Mon Sep 17 00:00:00 2001 From: zkerriga Date: Sun, 12 May 2024 19:30:10 +0200 Subject: [PATCH 2/4] make Combined values defs --- core/src/main/scala/ox/resilience/Schedule.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/ox/resilience/Schedule.scala b/core/src/main/scala/ox/resilience/Schedule.scala index 51f768b3..f3ccd401 100644 --- a/core/src/main/scala/ox/resilience/Schedule.scala +++ b/core/src/main/scala/ox/resilience/Schedule.scala @@ -119,8 +119,8 @@ object Schedule: Backoff.nextDelay(attempt, initialDelay, maxDelay, jitter, lastDelay) private[resilience] sealed trait Combined extends Schedule: - val base: Finite - val fallback: Schedule + def base: Finite + def fallback: Schedule override def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration = if base.maxRetries > attempt then base.nextDelay(attempt, lastDelay) else fallback.nextDelay(attempt - base.maxRetries, lastDelay) From 22ea44a65f91895e2bda78d20467575cd1e7b57e Mon Sep 17 00:00:00 2001 From: zkerriga Date: Sun, 12 May 2024 19:30:29 +0200 Subject: [PATCH 3/4] move extension methods to Finite trait --- core/src/main/scala/ox/resilience/Schedule.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/ox/resilience/Schedule.scala b/core/src/main/scala/ox/resilience/Schedule.scala index f3ccd401..1c13881c 100644 --- a/core/src/main/scala/ox/resilience/Schedule.scala +++ b/core/src/main/scala/ox/resilience/Schedule.scala @@ -10,6 +10,8 @@ object Schedule: private[resilience] sealed trait Finite extends Schedule: def maxRetries: Int + def fallbackTo(fallback: Finite): Finite = Combination(this, fallback) + def fallbackTo(fallback: Infinite): Infinite = Combination.forever(this, fallback) private[resilience] sealed trait Infinite extends Schedule @@ -137,7 +139,3 @@ object Schedule: def forever(base: Finite, fallback: Infinite): Infinite = CombinationForever(base, fallback) case class CombinationForever private[resilience](base: Finite, fallback: Infinite) extends Combined, Infinite - - extension (schedule: Finite) - def fallbackTo(fallback: Finite): Finite = Combination(schedule, fallback) - def fallbackTo(fallback: Infinite): Infinite = Combination.forever(schedule, fallback) From 7eb0a6fa2f606c91affc0131b9bcfb376ede29df Mon Sep 17 00:00:00 2001 From: zkerriga Date: Tue, 14 May 2024 22:14:24 +0200 Subject: [PATCH 4/4] clarify names of fallback classes --- core/src/main/scala/ox/resilience/Schedule.scala | 14 +++++++------- ...st.scala => ScheduleFallingBackRetryTest.scala} | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) rename core/src/test/scala/ox/resilience/{ScheduleCombinationRetryTest.scala => ScheduleFallingBackRetryTest.scala} (95%) diff --git a/core/src/main/scala/ox/resilience/Schedule.scala b/core/src/main/scala/ox/resilience/Schedule.scala index 1c13881c..50d405b8 100644 --- a/core/src/main/scala/ox/resilience/Schedule.scala +++ b/core/src/main/scala/ox/resilience/Schedule.scala @@ -10,8 +10,8 @@ object Schedule: private[resilience] sealed trait Finite extends Schedule: def maxRetries: Int - def fallbackTo(fallback: Finite): Finite = Combination(this, fallback) - def fallbackTo(fallback: Infinite): Infinite = Combination.forever(this, fallback) + def fallbackTo(fallback: Finite): Finite = FallingBack(this, fallback) + def fallbackTo(fallback: Infinite): Infinite = FallingBack.forever(this, fallback) private[resilience] sealed trait Infinite extends Schedule @@ -120,7 +120,7 @@ object Schedule: override def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration = Backoff.nextDelay(attempt, initialDelay, maxDelay, jitter, lastDelay) - private[resilience] sealed trait Combined extends Schedule: + private[resilience] sealed trait WithFallback extends Schedule: def base: Finite def fallback: Schedule override def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration = @@ -130,12 +130,12 @@ object Schedule: /** A schedule that combines two schedules, using [[base]] first [[base.maxRetries]] number of times, and then using [[fallback]] * [[fallback.maxRetries]] number of times. */ - case class Combination(base: Finite, fallback: Finite) extends Combined, Finite: + case class FallingBack(base: Finite, fallback: Finite) extends WithFallback, Finite: override def maxRetries: Int = base.maxRetries + fallback.maxRetries - object Combination: + object FallingBack: /** A schedule that retries indefinitely, using [[base]] first [[base.maxRetries]] number of times, and then always using [[fallback]]. */ - def forever(base: Finite, fallback: Infinite): Infinite = CombinationForever(base, fallback) + def forever(base: Finite, fallback: Infinite): Infinite = FallingBackForever(base, fallback) - case class CombinationForever private[resilience](base: Finite, fallback: Infinite) extends Combined, Infinite + case class FallingBackForever private[resilience] (base: Finite, fallback: Infinite) extends WithFallback, Infinite diff --git a/core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala b/core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala similarity index 95% rename from core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala rename to core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala index 86db2dfa..fa9f335d 100644 --- a/core/src/test/scala/ox/resilience/ScheduleCombinationRetryTest.scala +++ b/core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala @@ -6,7 +6,7 @@ import ox.ElapsedTime import scala.concurrent.duration.* -class ScheduleCombinationRetryTest extends AnyFlatSpec with Matchers with ElapsedTime: +class ScheduleFallingBackRetryTest extends AnyFlatSpec with Matchers with ElapsedTime: behavior of "retry with combination of schedules" it should "retry 3 times immediately and then 2 times with delay" in {