From aede2cc79d8962d0e7bb1b201151eef4c0afbe12 Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Fri, 12 Feb 2016 10:59:49 +0200 Subject: [PATCH 1/2] Add an optional argument to Backoff policy - max delay. This caps the maximum wait time waited between retries. --- src/main/scala/Defaults.scala | 1 + src/main/scala/Policy.scala | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/scala/Defaults.scala b/src/main/scala/Defaults.scala index ae10415..c4de96c 100644 --- a/src/main/scala/Defaults.scala +++ b/src/main/scala/Defaults.scala @@ -5,5 +5,6 @@ import java.util.concurrent.TimeUnit object Defaults { val delay: FiniteDuration = Duration(500, TimeUnit.MILLISECONDS) + val maxDelay: Duration = Duration.Inf } diff --git a/src/main/scala/Policy.scala b/src/main/scala/Policy.scala index 4b93be2..7058519 100644 --- a/src/main/scala/Policy.scala +++ b/src/main/scala/Policy.scala @@ -77,9 +77,14 @@ object Pause { } object Backoff { + private def nextDelay(calculatedDelay: FiniteDuration, maxDelay: Duration): FiniteDuration = + maxDelay match { + case _: Duration.Infinite => calculatedDelay + case delay: FiniteDuration => List(calculatedDelay, delay).min + } /** Retry with exponential backoff forever */ - def forever(delay: FiniteDuration = Defaults.delay, base: Int = 2) + def forever(delay: FiniteDuration = Defaults.delay, base: Int = 2, maxDelay: Duration = Defaults.maxDelay) (implicit timer: Timer): Policy = new Policy { def apply[T] @@ -88,7 +93,7 @@ object Backoff { executor: ExecutionContext): Future[T] = { def run(delay: FiniteDuration): Future[T] = retry(promise, { () => Delay(delay) { - run(Duration(delay.length * base, delay.unit)) + run(nextDelay(delay * base, maxDelay)) }.future.flatMap(identity) }) run(delay) @@ -99,7 +104,8 @@ object Backoff { def apply( max: Int = 8, delay: FiniteDuration = Defaults.delay, - base: Int = 2) + base: Int = 2, + maxDelay: Duration = Defaults.maxDelay) (implicit timer: Timer): Policy = new CountingPolicy { def apply[T] @@ -109,7 +115,7 @@ object Backoff { def run(max: Int, delay: FiniteDuration): Future[T] = countdown( max, promise, count => Delay(delay) { - run(count, Duration(delay.length * base, delay.unit)) + run(count, nextDelay(delay * base, maxDelay)) }.future.flatMap(identity)) run(max, delay) } From c52e586e1b8f188d59e62a1dcae2a37abb9231ce Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Fri, 12 Feb 2016 11:04:33 +0200 Subject: [PATCH 2/2] Implement tests for Backoff policy with maxDelay. --- src/test/scala/PolicySpec.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/scala/PolicySpec.scala b/src/test/scala/PolicySpec.scala index 8b920c1..3cb15d8 100644 --- a/src/test/scala/PolicySpec.scala +++ b/src/test/scala/PolicySpec.scala @@ -116,6 +116,21 @@ class PolicySpec extends FunSpec with BeforeAndAfterAll { assert(took <= 110.millis === true, s"took more time than expected: $took") } + + it ("should wait up to a max time each time") { + implicit val success = Success[Int](_ == 2) + val tries = forwardCountingFutureStream().iterator + val policy = retry.Backoff(2, 30.millis, maxDelay = 50.millis) + val took = time { + val result = Await.result(policy(tries.next), + 80.millis + 20.millis) + assert(success.predicate(result) === true, "predicate failed") + } + assert(took >= 80.millis === true, + s"took less time than expected: $took") + assert(took <= 100.millis === true, + s"took more time than expected: $took") + } } describe("retry.When") {