├── .gitignore ├── README-CN.md ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── retry │ └── Retry.scala └── test └── scala └── RetrySpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | logs/ 3 | *.lock 4 | .DS_Store 5 | .history 6 | .idea 7 | .idea_modules 8 | /.classpath 9 | /.project 10 | /.settings -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # Play-Utils 介绍 2 | `Play-Utils` 是一个专门为 [Play Framework](https://www.playframework.com/) 开发的实用工具包模块,目前已实现如下功能: 3 | - `Retry` 自动请求重试 4 | 5 | # 1 Retry 6 | `Retry` 工具包可以帮助你设置不同的重试策略,自动重试失败的请求,最终返回成功的结果或者是最后一次重试结果。 7 | 8 | ## 1.1 基本用法 9 | 将以下依赖添加至`build.sbt`文件: 10 | ``` 11 | libraryDependencies += "cn.playscala" %% "play-utils" % "0.2.1" 12 | ``` 13 | 最简单的重试策略是固定时间重试,即每次重试的时间间隔相同。 在开始编码之前,你需要将`Retry`实例依赖注入到需要的地方: 14 | ``` 15 | class ExternalService @Inject()(retry: Retry) 16 | ``` 17 | 下面的代码使用固定时间重试策略,每秒重试一次,最多重试3次: 18 | ``` 19 | import scala.concurrent.duration._ 20 | 21 | retry.withFixedDelay[Int](3, 1 seconds) { () => 22 | Future.successful(0) 23 | }.stopWhen(_ == 10) 24 | ``` 25 | `stopWhen` 用于设置重试终止条件,即当 Future 结果为 10 时直接返回该Future。你也可以使用 `retryWhen` 设置重试条件: 26 | ``` 27 | import scala.concurrent.duration._ 28 | 29 | retry.withFixedDelay[Int](3, 1 seconds) { () => 30 | Future.successful(0) 31 | }.retryWhen(_ != 10) 32 | ``` 33 | 需要特别注意的是,如果在重试过程中发生异常,则会自动继续进行下一次重试。 34 | 35 | 除了采用依赖注入方式,你也可以直接使用单例对象`Retry`, 但是需要注意的是,选择单例对象方式需要在当前作用域内提供如下两个隐式对象: 36 | ``` 37 | implicit val ec: ExecutionContext = ... 38 | implicit val scheduler: Scheduler = ... 39 | 40 | Retry.withFixedDelay[Int](3, 1 seconds).apply { () => 41 | Future.successful(0) 42 | }.retryWhen(s => s != 10) 43 | ``` 44 | 45 | > 下文中如无特殊说明,默认为采用依赖注入方式,注入实例变量名为`retry`。 46 | 47 | 你可以通过 `withExecutionContext` 和 `withScheduler` 两个方法设置自定义的线程池和定时器: 48 | ``` 49 | import scala.concurrent.duration._ 50 | 51 | retry.withFixedDelay[Int](3, 1 seconds) { () => 52 | Future.successful(0) 53 | }.withExecutionContext(ec) 54 | .withScheduler(s) 55 | .retryWhen(_ != 10) 56 | ``` 57 | 58 | 可以通过`withTaskName`方法为重试任务起个名字,以增强日志的可读性。 日志功能默认是开启的,如果你不想记录日志,可以通过`withLoggingEnabled`方法关闭日志功能。 59 | 60 | ## 1.2 重试策略 61 | 某些场景下,固定时间重试可能会对远程服务造成冲击,因此`Retry`提供了多种策略供你选择。 62 | 63 | ### 1.2.1 BackoffRetry 64 | `BackoffRetry`包含两个参数,参数`delay`用于设置第一次延迟时间,参数`factor`是一个乘积因子,用于延长下一次的重试时间: 65 | ``` 66 | import scala.concurrent.duration._ 67 | 68 | retry.withBackoffDelay[Int](3, 1 seconds, 2.0) { () => 69 | Future.successful(0) 70 | }.retryWhen(_ != 10) 71 | ``` 72 | 重试的延迟时间依次为:`1 seconds`, `2 seconds` 和 `4 seconds`。 73 | 74 | ### 1.2.2 JitterRetry 75 | `JitterRetry`包含两个参数`minDelay`和`maxDelay`,用于控制延迟时间的上限和下限,真实的延迟时间会在这两个值之间波动: 76 | ``` 77 | import scala.concurrent.duration._ 78 | 79 | retry.withJitterDelay[Int](3, 1 seconds, 1 hours) { () => 80 | Future.successful(0) 81 | }.retryWhen(_ != 10) 82 | ``` 83 | 84 | ### 1.2.3 FibonacciRetry 85 | `FibonacciRetry`使用斐波纳契算法计算下一次的延迟时间: 86 | ``` 87 | import scala.concurrent.duration._ 88 | 89 | retry.withFibonacciDelay[Int](4, 1 seconds) { () => 90 | Future.successful(0) 91 | }.retryWhen(_ != 10) 92 | ``` 93 | 重试的延迟时间依次为:`0 seconds`, `1 seconds`, `1 seconds` 和 `2 seconds`。 94 | 95 | 需要注意的是,你可以设置`baseDelay`参数控制延迟的时间间隔: 96 | ``` 97 | import scala.concurrent.duration._ 98 | 99 | retry.withFibonacciDelay[Int](4, 2 seconds) { () => 100 | Future.successful(0) 101 | }.retryWhen(_ != 10) 102 | ``` 103 | 重试的延迟时间依次为:`0 seconds`, `2 seconds`, `2 seconds` 和 `4 seconds`。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | For chinese introduction, please refer to [README-CN.md](https://github.com/playcommunity/play-utils/blob/master/README-CN.md). 3 | 4 | # Introduction 5 | `Play-Utils` is a set of utilities for developing with [Play Framework](https://www.playframework.com/), including the following features: 6 | - `Retry` retry request automatically with different strategies 7 | 8 | # 1 Retry 9 | `Retry` utility is used to retry request automatically with different strategies, and finally return the success result or the last retried result. 10 | 11 | ## 1.1 Get started 12 | Add the following dependency to your `build.sbt`: 13 | ``` 14 | libraryDependencies += "cn.playscala" %% "play-utils" % "0.2.1" 15 | ``` 16 | FixedDelayRetry is the simplest retry strategy, it retries the next request with the same delay. Before coding, the instance of `Retry` should be injected where is needed: 17 | ``` 18 | class ExternalService @Inject()(retry: Retry) 19 | ``` 20 | The following codes retry every second and 3 times at most: 21 | ``` 22 | import scala.concurrent.duration._ 23 | 24 | retry.withFixedDelay[Int](3, 1 seconds) { () => 25 | Future.successful(0) 26 | }.stopWhen(_ == 10) 27 | ``` 28 | `stopWhen` is used to set the stop condition, that means, it will return a successful result when the result value is 10 otherwise continue to retry. You can also use `retryWhen` method to set retry condition: 29 | ``` 30 | import scala.concurrent.duration._ 31 | 32 | retry.withFixedDelay[Int](3, 1 seconds) { () => 33 | Future.successful(0) 34 | }.retryWhen(_ != 10) 35 | ``` 36 | Notice that, it will retry automatically when an exception is thrown. 37 | 38 | In addition to injected instance, you can also use the singleton object `Retry` directly with two implicit objects in current scope: 39 | ``` 40 | implicit val ec: ExecutionContext = ... 41 | implicit val scheduler: Scheduler = ... 42 | 43 | Retry.withFixedDelay[Int](3, 1 seconds).apply { () => 44 | Future.successful(0) 45 | }.retryWhen(s => s != 10) 46 | ``` 47 | > Unless stated, the following codes use the injected instance which is named `retry`. 48 | 49 | You can set the customized execution context and scheduler with `withExecutionContext` and `withScheduler` methods: 50 | ``` 51 | import scala.concurrent.duration._ 52 | 53 | retry.withFixedDelay[Int](3, 1 seconds) { () => 54 | Future.successful(0) 55 | }.withExecutionContext(ec) 56 | .withScheduler(s) 57 | .retryWhen(_ != 10) 58 | ``` 59 | You can set a name for this retry task for better logs with `withTaskName` method. The logging is enabled default, if you want you can disabled it with `withLoggingEnabled` method. 60 | 61 | ## 1.2 Retry Strategies 62 | In some scenarios, fixed-time retry may impact remote services. So there are several useful candidate strategies. 63 | 64 | ### 1.2.1 BackoffRetry 65 | `BackoffRetry` contains 2 parameters, `delay` parameter is for setting the initial delay, `factor` parameter is a product factor, used for adjusting the next delay time. 66 | ``` 67 | import scala.concurrent.duration._ 68 | 69 | retry.withBackoffDelay[Int](3, 1 seconds, 2.0) { () => 70 | Future.successful(0) 71 | }.retryWhen(_ != 10) 72 | ``` 73 | The retry delay times are: `1 seconds`, `2 seconds` and `4 seconds`. 74 | 75 | ### 1.2.2 JitterRetry 76 | `JitterRetry` contains 2 parameters, `minDelay` parameter sets the lower bound, and `maxDelay` parameter sets the upper bound. The retry delay time will fluctuate between these two values: 77 | ``` 78 | import scala.concurrent.duration._ 79 | 80 | retry.withJitterDelay[Int](3, 1 seconds, 1 hours) { () => 81 | Future.successful(0) 82 | }.retryWhen(_ != 10) 83 | ``` 84 | 85 | ### 1.2.3 FibonacciRetry 86 | `FibonacciRetry` calculates the delay time based on Fibonacci algorithm. 87 | ``` 88 | import scala.concurrent.duration._ 89 | 90 | retry.withFibonacciDelay[Int](4, 1 seconds) { () => 91 | Future.successful(0) 92 | }.retryWhen(_ != 10) 93 | ``` 94 | The retry delay times are: `0 seconds`, `1 seconds`, `1 seconds` and `2 seconds`。 95 | 96 | Notice that, you can adjust the `baseDelay` parameter to control the interval between each delay: 97 | ``` 98 | import scala.concurrent.duration._ 99 | 100 | retry.withFibonacciDelay[Int](4, 2 seconds) { () => 101 | Future.successful(0) 102 | }.retryWhen(_ != 10) 103 | ``` 104 | The retry delay times are: `0 seconds`, `2 seconds`, `2 seconds` and `4 seconds`. -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | name := "play-utils" 3 | 4 | version := "0.2.2" 5 | 6 | scalaVersion := "2.12.6" 7 | 8 | organization := "cn.playscala" 9 | 10 | organizationName := "cn.playscala" 11 | 12 | organizationHomepage := Some(url("https://github.com/playcommunity")) 13 | 14 | homepage := Some(url("https://github.com/playcommunity/play-utils")) 15 | 16 | playBuildRepoName in ThisBuild := "play-utils" 17 | 18 | version in ThisBuild := "0.2.2" 19 | 20 | val play = "com.typesafe.play" %% "play" % "2.6.18" 21 | val playScalaTest = "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % "test" 22 | 23 | val buildSettings = Seq( 24 | organization := "cn.playscala", 25 | organizationName := "cn.playscala", 26 | organizationHomepage := Some(url("https://github.com/playcommunity")), 27 | scalaVersion := "2.12.6", 28 | crossScalaVersions := Seq("2.11.7", "2.12.6"), 29 | //scalacOptions in Compile := scalacOptionsVersion(scalaVersion.value), 30 | //scalacOptions in Test := scalacOptionsTest, 31 | //scalacOptions in IntegrationTest := scalacOptionsTest, 32 | ) 33 | 34 | lazy val root = Project( 35 | id = "play-utils", 36 | base = file(".") 37 | ) 38 | .enablePlugins(PlayLibrary) 39 | .settings(buildSettings) 40 | .settings(libraryDependencies ++= Seq(play, playScalaTest)) 41 | .settings(publishSettings) 42 | 43 | lazy val publishSettings = Seq( 44 | publishTo := { 45 | val nexus = "https://oss.sonatype.org/" 46 | if (isSnapshot.value) 47 | Some("snapshots" at nexus + "content/repositories/snapshots") 48 | else 49 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 50 | }, 51 | publishMavenStyle := true, 52 | publishArtifact in Test := false, 53 | pomIncludeRepository := { _ => false }, 54 | publishConfiguration := publishConfiguration.value.withOverwrite(true), 55 | publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(true), 56 | pomExtra := ( 57 | 58 | git@github.com:playcommunity/play-utils.git 59 | scm:git:git@github.com:playcommunity/play-utils.git 60 | 61 | 62 | 63 | joymufeng 64 | Play Community 65 | 66 | 67 | ) 68 | ) 69 | 70 | lazy val noPublishing = Seq( 71 | publishTo := None 72 | ) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.3 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "interplay" % "2.0.3") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") -------------------------------------------------------------------------------- /src/main/scala/retry/Retry.scala: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import akka.actor.{ActorSystem, Scheduler} 4 | import akka.pattern.after 5 | import javax.inject.Inject 6 | import play.api.Logger 7 | 8 | import scala.annotation.tailrec 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.concurrent.duration._ 11 | import scala.util.{Failure, Random, Success} 12 | 13 | trait Retryable[T] { 14 | 15 | /** 16 | * Execute the block codes that produces a Future[T], returns a Future containing the result of T, unless an exception is thrown, 17 | * in which case the operation will be retried after delay time, if there are more possible retries, which is configured through 18 | * the retries parameter. If the operation does not succeed and there is no retries left, the resulting Future will contain the last failure. 19 | * @param block 20 | * @return 21 | */ 22 | def retryWhen(predicate: T => Boolean): Future[T] 23 | 24 | /** 25 | * Set the stop condition. 26 | * @param predicate 27 | * @return the successful Future[T] or the last retried result. 28 | */ 29 | def stopWhen(predicate: T => Boolean): Future[T] 30 | 31 | //TODO 32 | //def cancel() 33 | //def stopOnException 34 | } 35 | 36 | /** 37 | * The base abstract class for different retry strategies. 38 | * The original inspiration comes from https://gist.github.com/viktorklang/9414163, thanks to Viktor Klang and Chad Selph. 39 | * @param retries the max retry count. 40 | * @param baseDelay the initial delay for first retry. 41 | * @param ec execution context. 42 | * @param s scheduler. 43 | * @tparam T 44 | */ 45 | case class RetryTask[T](retries: Int, baseDelay: FiniteDuration, nextDelay: Int => FiniteDuration, block: () => Future[T] = null, predicate : T => Boolean = null, taskName: String = "default", enableLogging: Boolean = true, ec: ExecutionContext, scheduler: Scheduler) extends Retryable[T] { 46 | protected val logger = Logger("retry") 47 | /** 48 | * Set an block/operation that will produce a Future[T]. 49 | * @param block 50 | * @return current retryable instance. 51 | */ 52 | def apply(block: () => Future[T]): Retryable[T] = { 53 | this.copy(block = block) 54 | } 55 | 56 | /** 57 | * Set the retry condition. 58 | * @param predicate 59 | * @return the successful Future[T] or the last retried result. 60 | */ 61 | def retryWhen(predicate: T => Boolean): Future[T] = { 62 | val instance = this.copy(predicate = predicate) 63 | Future(instance.retry(0)(ec, scheduler))(ec).flatMap(f => f)(ec) 64 | } 65 | 66 | /** 67 | * Set the stop condition. 68 | * @param predicate 69 | * @return the successful Future[T] or the last retried result. 70 | */ 71 | def stopWhen(predicate: T => Boolean): Future[T] = { 72 | val instance = this.copy(predicate = predicate) 73 | Future(instance.retry(0)(ec, scheduler))(ec).flatMap(f => f)(ec) 74 | } 75 | 76 | /** 77 | * Retry and check the result with the predicate condition. Continue retrying if an exception is thrown. 78 | * @param retried the current retry number. 79 | * @return the successful Future[T] or the last retried result. 80 | */ 81 | private def retry(retried: Int)(implicit ec: ExecutionContext, scheduler: Scheduler): Future[T] = { 82 | val f = try { block() } catch { case t => Future.failed(t) } 83 | f transformWith { 84 | case Success(res) => 85 | val isSuccess = predicate(res) 86 | if (retried >= retries || isSuccess) { 87 | if (retried > retries && !isSuccess) error(s"Oops! retry finished with unexpected result: ${res}") 88 | if (retried > 0 && isSuccess) info(s"congratulations! retry finished with expected result: ${res}") 89 | f 90 | } else { 91 | val nextDelayTime = nextDelay(retried + 1) 92 | warn(s"invalid result ${res}, retry after ${nextDelayTime} for the ${retried} time.") 93 | after(nextDelayTime, scheduler)(retry(retried + 1)) 94 | } 95 | case Failure(t) => 96 | if (retried < retries) { 97 | val nextDelayTime = nextDelay(retried + 1) 98 | error(s"${t.getMessage} error occurred, retry after ${nextDelayTime} for the ${retried} time.", t) 99 | after(nextDelayTime, scheduler)(retry(retried + 1)) 100 | } else { 101 | error(s"Oops! retry finished with unexpected error: ${t.getMessage}", t) 102 | Future.failed(t) 103 | } 104 | } 105 | } 106 | 107 | protected def debug(msg: String): Unit = if (enableLogging) logger.debug(s"${taskName} - ${msg}") 108 | 109 | protected def info(msg: String): Unit = if (enableLogging) logger.info(s"${taskName} - ${msg}") 110 | 111 | protected def warn(msg: String): Unit = if (enableLogging) logger.warn(s"${taskName} - ${msg}") 112 | 113 | protected def error(msg: String): Unit = if (enableLogging) logger.error(s"${taskName} - ${msg}") 114 | 115 | protected def error(msg: String, t: Throwable): Unit = if (enableLogging) logger.error((s"${taskName} - ${msg}"), t) 116 | } 117 | 118 | /** 119 | * The entrance class for working with DI containers. 120 | * @param ec the injected execution context. 121 | * @param actorSystem the injected actorSystem. 122 | */ 123 | class Retry @Inject() (ec: ExecutionContext, actorSystem: ActorSystem) { 124 | 125 | /** 126 | * Retry with a fixed delay strategy. 127 | * @param retries the max retry count. 128 | * @param delay the fixed delay between each retry. 129 | * @param ec execution context. 130 | * @param s scheduler. 131 | * @tparam T 132 | */ 133 | def withFixedDelay[T](retries: Int, delay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true, executionContext: ExecutionContext = ec, scheduler: Scheduler = actorSystem.scheduler): RetryTask[T] = { 134 | /** 135 | * Calc the next delay based on the previous delay. 136 | * @param retries the current retry number. 137 | * @return the next delay. 138 | */ 139 | def nextDelay(retries: Int): FiniteDuration = delay 140 | RetryTask[T](retries, delay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 141 | } 142 | 143 | /** 144 | * Retry with a back-off delay strategy. 145 | * @param retries the max retry count. 146 | * @param baseDelay the initial delay for first retry. 147 | * @param factor the product factor for the calculation of next delay. 148 | * @param ec execution context. 149 | * @param scheduler 150 | * @tparam T 151 | */ 152 | def withBackoffDelay[T](retries: Int, baseDelay: FiniteDuration, factor: Double, taskName: String = "default", enableLogging: Boolean = true, executionContext: ExecutionContext = ec, scheduler: Scheduler = actorSystem.scheduler): RetryTask[T] = { 153 | def nextDelay(retried: Int): FiniteDuration = { 154 | if (retried == 1) { 155 | baseDelay 156 | } else { 157 | Duration((baseDelay.length * Math.pow(factor, retried - 1)).toLong, baseDelay.unit) 158 | } 159 | } 160 | RetryTask[T](retries, baseDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 161 | } 162 | 163 | /** 164 | * Retry with a jitter delay strategy. 165 | * @param retries the max retry count. 166 | * @param minDelay min delay. 167 | * @param maxDelay max delay. 168 | * @param ec execution context. 169 | * @param scheduler 170 | * @tparam T 171 | */ 172 | def withJitterDelay[T](retries: Int, minDelay: FiniteDuration, maxDelay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true, executionContext: ExecutionContext = ec, scheduler: Scheduler = actorSystem.scheduler): RetryTask[T] = { 173 | def nextDelay(retried: Int): FiniteDuration = { 174 | val interval = maxDelay - minDelay 175 | minDelay + Duration((interval.length * Random.nextDouble).toLong, interval.unit) 176 | } 177 | 178 | RetryTask[T](retries, minDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 179 | } 180 | 181 | /** 182 | * Retry with a fibonacci delay strategy. 183 | * @param retries the max retry count. 184 | * @param baseDelay the initial delay for first retry. 185 | * @param ec execution context. 186 | * @param scheduler 187 | * @tparam T 188 | */ 189 | def withFibonacciDelay[T](retries: Int, baseDelay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true, executionContext: ExecutionContext = ec, scheduler: Scheduler = actorSystem.scheduler): RetryTask[T] = { 190 | def nextDelay(retried: Int): FiniteDuration = { 191 | def fib(n: Int): Int = { 192 | def fib_tail(n: Int, a: Int, b: Int): Int = n match { 193 | case 0 => a 194 | case _ => fib_tail(n - 1, b, a + b) 195 | } 196 | return fib_tail(n, 0 , 1) 197 | } 198 | 199 | val next = baseDelay * fib(retries - retried) 200 | next 201 | } 202 | 203 | RetryTask[T](retries, baseDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 204 | } 205 | } 206 | 207 | /** 208 | * The entrance object for directly usage. There should be an implicit execution context and an implicit scheduler in scope. 209 | */ 210 | object Retry { 211 | 212 | /** 213 | * Retry with a fixed delay strategy. 214 | * @param retries the max retry count. 215 | * @param delay the fixed delay between each retry. 216 | * @param ec execution context. 217 | * @param s scheduler. 218 | * @tparam T 219 | */ 220 | def withFixedDelay[T](retries: Int, delay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true)(implicit executionContext: ExecutionContext, scheduler: Scheduler): RetryTask[T] = { 221 | /** 222 | * Calc the next delay based on the previous delay. 223 | * @param retries the current retry number. 224 | * @return the next delay. 225 | */ 226 | def nextDelay(retries: Int): FiniteDuration = delay 227 | RetryTask[T](retries, delay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 228 | } 229 | 230 | /** 231 | * Retry with a back-off delay strategy. 232 | * @param retries the max retry count. 233 | * @param baseDelay the initial delay for first retry. 234 | * @param factor the product factor for the calculation of next delay. 235 | * @param ec execution context. 236 | * @param scheduler 237 | * @tparam T 238 | */ 239 | def withBackoffDelay[T](retries: Int, baseDelay: FiniteDuration, factor: Double, taskName: String = "default", enableLogging: Boolean = true)(implicit executionContext: ExecutionContext, scheduler: Scheduler): RetryTask[T] = { 240 | def nextDelay(retried: Int): FiniteDuration = { 241 | if (retried == 1) { 242 | baseDelay 243 | } else { 244 | Duration((baseDelay.length * Math.pow(factor, retried - 1)).toLong, baseDelay.unit) 245 | } 246 | } 247 | RetryTask[T](retries, baseDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 248 | } 249 | 250 | /** 251 | * Retry with a jitter delay strategy. 252 | * @param retries the max retry count. 253 | * @param minDelay min delay. 254 | * @param maxDelay max delay. 255 | * @param ec execution context. 256 | * @param scheduler 257 | * @tparam T 258 | */ 259 | def withJitterDelay[T](retries: Int, minDelay: FiniteDuration, maxDelay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true)(implicit executionContext: ExecutionContext, scheduler: Scheduler): RetryTask[T] = { 260 | def nextDelay(retried: Int): FiniteDuration = { 261 | val interval = maxDelay - minDelay 262 | minDelay + Duration((interval.length * Random.nextDouble).toLong, interval.unit) 263 | } 264 | 265 | RetryTask[T](retries, minDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 266 | } 267 | 268 | /** 269 | * Retry with a fibonacci delay strategy. 270 | * @param retries the max retry count. 271 | * @param baseDelay the initial delay for first retry. 272 | * @param ec execution context. 273 | * @param scheduler 274 | * @tparam T 275 | */ 276 | def withFibonacciDelay[T](retries: Int, baseDelay: FiniteDuration, taskName: String = "default", enableLogging: Boolean = true)(implicit executionContext: ExecutionContext, scheduler: Scheduler): RetryTask[T] = { 277 | def nextDelay(retried: Int): FiniteDuration = { 278 | def fib(n: Int): Int = { 279 | @tailrec 280 | def fib_tail(n: Int, a: Int, b: Int): Int = n match { 281 | case 0 => a 282 | case _ => fib_tail(n - 1, b, a + b) 283 | } 284 | return fib_tail(n, 0 , 1) 285 | } 286 | 287 | val next = baseDelay * fib(retries - retried) 288 | next 289 | } 290 | 291 | RetryTask[T](retries, baseDelay, nextDelay, taskName = taskName, enableLogging = enableLogging, ec = executionContext, scheduler = scheduler) 292 | } 293 | } 294 | 295 | -------------------------------------------------------------------------------- /src/test/scala/RetrySpec.scala: -------------------------------------------------------------------------------- 1 | import java.util.concurrent.atomic.AtomicInteger 2 | 3 | import org.scalatestplus.play.PlaySpec 4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite 5 | import retry.Retry 6 | import scala.concurrent.{Await, Future} 7 | import scala.concurrent.duration._ 8 | import scala.language.postfixOps 9 | 10 | class RetrySpec extends PlaySpec with GuiceOneAppPerSuite { 11 | val retry = app.injector.instanceOf[Retry] 12 | 13 | "FixedDelayRetry" should { 14 | "return the last result when exceed the max retry count" in { 15 | val i = new AtomicInteger(0) 16 | val result = Await.result( 17 | retry.withFixedDelay[Int](3, 1.seconds){ () => 18 | Future.successful(i.addAndGet(1)) 19 | }.stopWhen(_ == 10) 20 | ,10.seconds) 21 | 22 | result mustBe 4 23 | } 24 | 25 | "continue retrying when an exception is thrown" in { 26 | val i = new AtomicInteger(0) 27 | val result = Await.result( 28 | retry.withFixedDelay[Int](3, 1.seconds){ () => 29 | i.addAndGet(1) 30 | if (i.get() % 2 == 1) { 31 | Future.failed(new Exception) 32 | } else { 33 | Future.successful(i.get()) 34 | } 35 | }.stopWhen(_ == 10) 36 | ,10 seconds) 37 | 38 | result mustBe 4 39 | } 40 | } 41 | 42 | "BackoffRetry" should { 43 | "stop retrying after 3 seconds" in { 44 | val startTime = System.currentTimeMillis() 45 | val result = Await.result( 46 | retry.withBackoffDelay[Int](2, 1 seconds, 2){ () => 47 | Future.successful(0) 48 | }.stopWhen(_ == 10) 49 | ,10 seconds) 50 | 51 | result mustBe 0 52 | (System.currentTimeMillis() - startTime)/1000 mustBe 3 53 | } 54 | } 55 | 56 | "JitterRetry" should { 57 | "stop retrying between 1 and 3 seconds" in { 58 | val startTime = System.currentTimeMillis() 59 | val result = Await.result( 60 | retry.withJitterDelay[Int](1, 1 seconds, 3 seconds){ () => 61 | Future.successful(0) 62 | }.stopWhen(_ == 10) 63 | ,10 seconds) 64 | 65 | result mustBe 0 66 | val secs = (System.currentTimeMillis() - startTime)/1000 67 | secs >= 1 mustBe true 68 | secs < 3 mustBe true 69 | } 70 | } 71 | 72 | "FibonacciRetry" should { 73 | "stop retrying after 3 seconds" in { 74 | val startTime = System.currentTimeMillis() 75 | val result = Await.result( 76 | retry.withFibonacciDelay[Int](4, 1 seconds){ () => 77 | Future.successful(0) 78 | }.stopWhen(_ == 10) 79 | ,10 seconds) 80 | 81 | result mustBe 0 82 | (System.currentTimeMillis() - startTime)/1000 mustBe 4 83 | } 84 | } 85 | } 86 | --------------------------------------------------------------------------------