├── .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 |
--------------------------------------------------------------------------------