├── .github
└── workflows
│ └── scala.yml
├── .gitignore
├── .scalafmt.conf
├── .travis.yml
├── LICENSE.txt
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
└── scala
│ └── util
│ └── retry
│ └── blocking
│ ├── Retry.scala
│ └── RetryStrategy.scala
└── test
└── scala
├── RetryDslSpec.scala
└── RetrySpec.scala
/.github/workflows/scala.yml:
--------------------------------------------------------------------------------
1 | name: Scala CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up JDK 1.8
17 | uses: actions/setup-java@v1
18 | with:
19 | java-version: 1.8
20 | - name: Run tests
21 | run: sbt test
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ass
2 | *.log
3 |
4 | # sbt specific
5 | .cache/
6 | .history/
7 | .lib/
8 | dist/*
9 | .DS_Store
10 | target/
11 | lib_managed/
12 | src_managed/
13 | project/boot/
14 | project/plugins/project/
15 |
16 | # Scala-IDE specific
17 | .scala_dependencies
18 | .worksheet
19 |
20 | # Idea specific
21 | .idea/
22 | *.ipr
23 | *.iws
24 | *.iml
25 | out/
26 | .idea_modules/
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = "2.5.2"
2 |
3 | #
4 | # See http://scalameta.org/scalafmt/#Configuration for details
5 | #
6 | project {
7 | includeFilters = [
8 | ".*.\\.scala$"
9 | ".*\\..sbt$"
10 | ]
11 | }
12 |
13 | maxColumn = 120
14 |
15 | # Vertical alignment, options: none, some, more
16 | #
17 | # This awkward, self-contradictory, configuration ensures that only
18 | # the common sbt tokens get aligned, and not "production" code.
19 | #
20 | align = none
21 | align {
22 | openParenCallSite = false
23 | openParenDefnSite = false
24 | tokens = ["%", ":=", "~=", "<-"]
25 | }
26 |
27 | # If true, the margin character | is aligned with the opening triple quote string literals
28 | assumeStandardLibraryStripMargin = true
29 |
30 | #From scalafmt website:
31 | #see: http://scalameta.org/scalafmt/#includeCurlyBraceInSelectChains
32 | includeCurlyBraceInSelectChains = false
33 |
34 | continuationIndent {
35 | callSite = 2
36 | defnSite = 2
37 | extendSite = 2
38 | }
39 |
40 | danglingParentheses = false
41 |
42 |
43 | newlines {
44 | alwaysBeforeTopLevelStatements = false
45 | sometimesBeforeColonInMethodReturnType = true
46 | penalizeSingleSelectMultiArgList = false
47 | alwaysBeforeElseAfterCurlyIf = false
48 | neverInResultType = false
49 | }
50 |
51 | spaces {
52 | afterKeywordBeforeParen = true
53 | }
54 |
55 | binPack {
56 | parentConstructors = true
57 | literalArgumentLists = true
58 | }
59 |
60 | optIn {
61 | breaksInsideChains = false
62 | breakChainOnFirstMethodDot = true
63 | configStyleArguments = true
64 | }
65 |
66 | runner {
67 | optimizer {
68 | # Set to -1 to disable. Number of characters needed to trigger "config-style" formatting
69 | # see: http://scalameta.org/scalafmt/#runner.optimizer.forceConfigStyleOnOffset
70 | forceConfigStyleOnOffset = 150
71 |
72 | # minimum number of func arguments before config-style (look at top of file) is enabled
73 | forceConfigStyleMinArgCount = 2
74 | }
75 | }
76 |
77 | rewrite {
78 | rules = [
79 | SortImports
80 | # if your for has more than one single <- then it gets transformed into a multit-line curly brace one
81 | # PreferCurlyFors
82 | ]
83 | }
84 |
85 | optIn {
86 | blankLineBeforeDocstring = true
87 | }
88 |
89 | lineEndings=preserve
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | sudo: false
3 | dist: trusty
4 | group: edge
5 |
6 | matrix:
7 | include:
8 | # Scala 2.11, JVM
9 | - jdk: oraclejdk8
10 | scala: 2.11.12
11 | env: COMMAND=ci-jvm-mima
12 | # Scala 2.11, JVM, JDK 11
13 | - jdk: openjdk11
14 | scala: 2.11.12
15 | env: COMMAND=ci-jvm
16 | # Scala 2.11, JavaScript
17 | - jdk: oraclejdk8
18 | scala: 2.11.12
19 | env: COMMAND=ci-js
20 | # Scala 2.12, JVM
21 | - jdk: oraclejdk8
22 | scala: 2.12.15
23 | env: COMMAND=ci-jvm-all
24 | # Scala 2.12, JVM, JDK 11
25 | - jdk: openjdk11
26 | scala: 2.12.15
27 | env: COMMAND=ci-jvm
28 | # Scala 2.12, JavaScript
29 | - jdk: oraclejdk8
30 | scala: 2.12.15
31 | env: COMMAND=ci-js
32 | # Scala 2.13, JVM
33 | - jdk: oraclejdk8
34 | scala: 2.13.1
35 | env: COMMAND=ci-jvm
36 | # Scala 2.13, JVM, JDK 11
37 | - jdk: openjdk11
38 | scala: 2.13.1
39 | env: COMMAND=ci-jvm
40 |
41 | jdk:
42 | - oraclejdk8
43 |
44 | script:
45 | - sbt clean test
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Scala Retry
2 | ===========
3 |
4 | [](https://travis-ci.org/hipjim/scala-retry)
5 |
6 | ###### Simple retry mechanism for arbitrary function calls in scala.
7 |
8 | We live in times where software systems are built by small services that are talking to each other.
9 | In such systems, transient errors can occur and interfere with the normal program execution flow.
10 | Transient faults are temporary abnormal conditions such as offline services, infrastructure faults, or network issues.
11 | It is common practice to retry transient faults using a retry loop mechanism.
12 | This minimalistic library tries to solve this problem by implementing the retry side effect
13 | and give the user different backoff strategies.
14 |
15 | ## Maven artifacts
16 |
17 | [](https://maven-badges.herokuapp.com/maven-central/com.github.hipjim/scala-retry_2.13)
18 |
19 | ```scala
20 | libraryDependencies += "com.github.hipjim" %% "scala-retry" % "0.4.0"
21 | ```
22 |
23 | ```groovy
24 | compile group: 'com.github.hipjim', name: 'scala-retry_2.13', version: '0.4.0'
25 | ```
26 |
27 |
28 | ```xml
29 |
30 | com.github.hipjim
31 | scala-retry_2.13
32 | 0.4.0
33 |
34 | ```
35 |
36 | Versions for ```Scala 2.12```, ```Scala 2.11``` and ```Scala 2.10``` are available.
37 |
38 | ## Tutorial
39 |
40 | ```scala
41 | import scala.concurrent.duration._
42 | import util.retry.blocking.{RetryStrategy, Failure, Retry, Success}
43 |
44 | // define the retry strategy
45 | implicit val retryStrategy =
46 | RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)
47 |
48 | // pattern match the result
49 | val r = Retry(1 / 1) match {
50 | case Success(x) => x
51 | case Failure(t) => log("I got 99 problems but you won't be one", t)
52 | }
53 |
54 | // recover in case of a failure
55 | val recover = Retry(1 / 0) recover {
56 | case NonFatal(t) => Int.MaxValue
57 | }
58 |
59 | // get or else in case of failure
60 | val result = Retry(1 / 0).getOrElse(1)
61 |
62 | // can be used in for comprehensions
63 | val result = for {
64 | x <- Retry(1 / 0) // fails with java.lang.ArithmeticException: / by zero
65 | y <- Retry(1 / 1) // success
66 | } yield x + y // result is Failure with java.lang.ArithmeticException: / by zero
67 |
68 | ```
69 | ## Retry strategies
70 |
71 | #### Fixed backoff
72 | ```scala
73 | val retryStrategy =
74 | RetryStrategy.fixedBackOff(retryDuration = 3.seconds, maxAttempts = 5)
75 | ```
76 |
77 | #### Fibonacci backoff
78 | ```scala
79 | val retryStrategy =
80 | RetryStrategy.fibonacciBackOff(initialWaitDuration = 3.seconds, maxAttempts = 5)
81 | ```
82 |
83 | #### Random backoff
84 | ```scala
85 | val retryStrategy =
86 | RetryStrategy.randomBackOff(
87 | minimumWaitDuration = 1.seconds,
88 | maximumWaitDuration = 10.seconds,
89 | maxAttempts = 10
90 | )
91 | ```
92 |
93 | #### No backoff
94 | ```scala
95 | val retryStrategy = RetryStrategy.noBackOff(maxAttempts = 10)
96 | ```
97 | #### No retry
98 | ```scala
99 | val retryStrategy = RetryStrategy.noRetry
100 | ```
101 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "scala-retry"
2 |
3 | organization := "com.github.hipjim"
4 |
5 | crossScalaVersions := Seq("2.10.6", "2.11.10", "2.12.15","2.13.15", "3.0.2")
6 |
7 | version := "0.5.0-SNAPSHOT"
8 |
9 | scalaVersion := "2.13.15"
10 |
11 | scalacOptions ++= Seq(
12 | // warnings
13 | "-unchecked", // able additional warnings where generated code depends on assumptions
14 | "-deprecation", // emit warning for usages of deprecated APIs
15 | "-feature", // emit warning usages of features that should be imported explicitly
16 | // Features enabled by default
17 | "-language:higherKinds",
18 | "-language:implicitConversions",
19 | "-language:experimental.macros"
20 | )
21 |
22 | // Targeting Java 6, but only for Scala <= 2.11
23 | javacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
24 | case Some((2, majorVersion)) if majorVersion <= 11 =>
25 | // generates code with the Java 6 class format
26 | Seq("-source", "1.6", "-target", "1.6")
27 | case _ =>
28 | // For 2.12 we are targeting the Java 8 class format
29 | Seq("-source", "1.8", "-target", "1.8")
30 | })
31 |
32 | // Targeting Java 6, but only for Scala <= 2.11
33 | scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
34 | case Some((2, majorVersion)) if majorVersion <= 11 =>
35 | // generates code with the Java 6 class format
36 | Seq("-target:jvm-1.6")
37 | case _ =>
38 | // For 2.12 we are targeting the Java 8 class format
39 | Seq.empty
40 | })
41 |
42 | libraryDependencies ++= Seq(
43 | "org.scalatest" %% "scalatest" % "3.2.9" % "test"
44 | )
45 |
46 | publishMavenStyle := true
47 |
48 | publishTo := {
49 | val nexus = "https://oss.sonatype.org/"
50 | if (isSnapshot.value)
51 | Some("snapshots" at nexus + "content/repositories/snapshots")
52 | else
53 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
54 | }
55 |
56 | publishArtifact in Test := false
57 | pomIncludeRepository := { _ => false }
58 |
59 | pomExtra :=
60 | https://github.com/hipjim/scala-retry
61 |
62 |
63 | Apache License, Version 2.0
64 | https://www.apache.org/licenses/LICENSE-2.0
65 | repo
66 |
67 |
68 |
69 | git@github.com:hipjim/scala-retry.git
70 | scm:git:git@github.com:hipjim/scala-retry.git
71 |
72 |
73 |
74 | hipjim
75 | Cristian Popovici
76 | https://cristipopovici.com
77 |
78 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.4.9
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.0")
2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
3 |
4 |
--------------------------------------------------------------------------------
/src/main/scala/util/retry/blocking/Retry.scala:
--------------------------------------------------------------------------------
1 | package util.retry.blocking
2 |
3 | import util.retry.blocking.RetryStrategy.RetryStrategyProducer
4 |
5 | import scala.concurrent.{ExecutionContext, Future}
6 | import scala.util.Try
7 | import scala.util.control.NonFatal
8 |
9 | /**
10 | * The `Retry` type represents a computation that is retrying itself in case of an exception. It uses a `RetryStrategy`
11 | * as a policy for the retry operation.
12 | *
13 | * The result may be successful consisting of the computation result value or a failure that
14 | * is wrapping the underlying exception. The type is similar to the scala [[scala.util.Try]] type.
15 | *
16 | * Example:
17 | * {{{
18 | * import scala.concurrent.duration._
19 | * import util.retry.blocking.{RetryStrategy, Failure, Retry, Success}
20 | *
21 | * implicit val retryStrategy =
22 | * RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)
23 | *
24 | * val r = Retry(1 / 1) match {
25 | * case Success(x) => x
26 | * case Failure(t) => log("Exception occurred", t)
27 | * }
28 | * }}}
29 | */
30 | sealed trait Retry[+T] {
31 | /**
32 | * Returns the result by mapping f to the current value of Success or a Failure if the result is Failure
33 | */
34 | def flatMap[S](f: T => Retry[S]): Retry[S]
35 |
36 | /**
37 | * Maps the given function to the value from this `Success` or returns this if this is a `Failure`.
38 | */
39 | def map[S](f: T => S)(implicit strategy: RetryStrategyProducer): Retry[S]
40 |
41 | /**
42 | * Returns `true` if the `Retry` is a `Failure` otherwise it returns `false`.
43 | */
44 | def isFailure: Boolean
45 |
46 | /**
47 | * Returns `true` if the `Retry` is a `Success` otherwise it returns `false` otherwise.
48 | */
49 | def isSuccess: Boolean
50 |
51 | /**
52 | * Returns the computation value in case of a `Success`.
53 | * In case of a `Failure` it throws the underlying exception.
54 | */
55 | def get: T
56 |
57 | /**
58 | * Returns the computation value in case of a `Success`. Otherwise it is returning the provided default.
59 | */
60 | def getOrElse[U >: T](default: => U): U = if (isSuccess) get else default
61 |
62 | /**
63 | * Applies the given function `f` if this is a `Success`, otherwise returns `Unit` if this is a `Failure`.
64 | */
65 | def foreach[X](f: T => X)
66 |
67 | /**
68 | * Applies the given function `f` if this is a `Failure`, otherwise returns this if this is a `Success`.
69 | */
70 | def recover[X >: T](f: PartialFunction[Throwable, X]): Retry[X]
71 |
72 | /**
73 | * Transforms the `Retry` value by applying a transformation function to its underlying value
74 | */
75 | def transform[X](f: T => X): X
76 | }
77 |
78 | final case class Success[+T](value: T) extends Retry[T] {
79 | override def isFailure: Boolean = false
80 | override def isSuccess: Boolean = true
81 | override def recover[X >: T](f: PartialFunction[Throwable, X]): Retry[X] =
82 | this
83 | override def get: T = value
84 | override def foreach[X](f: T => X): Unit = f(value)
85 | override def transform[X](f: T => X): X = f(value)
86 | override def flatMap[S](f: T => Retry[S]): Retry[S] = f(value)
87 | override def map[S](f: T => S)(implicit strategy: RetryStrategyProducer): Retry[S] = Retry(f(value))
88 | }
89 |
90 | final case class Failure[+T](exception: Throwable) extends Retry[T] {
91 | override def isFailure: Boolean = true
92 | override def isSuccess: Boolean = false
93 | override def recover[X >: T](f: PartialFunction[Throwable, X]): Retry[X] = {
94 | try {
95 | if (f.isDefinedAt(exception)) {
96 | Success(f(exception))
97 | } else this
98 | } catch {
99 | case NonFatal(e) => Failure(e)
100 | }
101 | }
102 |
103 | override def get: T = throw exception
104 | override def foreach[X](f: T => X): Unit = ()
105 | override def transform[X](f: T => X): X = throw exception
106 | override def flatMap[S](f: T => Retry[S]): Retry[S] = Failure(exception)
107 | override def map[S](f: T => S)(implicit strategy: () => RetryStrategy): Retry[S] = Failure(exception)
108 | }
109 |
110 | object Retry {
111 | def apply[T](fn: => T)(implicit strategy: () => RetryStrategy): Retry[T] = {
112 | def go(fn: => T)(strategy: RetryStrategy): Retry[T] = {
113 | Try(fn) match {
114 | case x: scala.util.Success[T] =>
115 | Success(x.value)
116 | case _ if strategy.shouldRetry() =>
117 | go(fn)(strategy.update())
118 | case f: scala.util.Failure[T] =>
119 | Failure(f.exception)
120 | }
121 | }
122 |
123 | go(fn)(strategy())
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/scala/util/retry/blocking/RetryStrategy.scala:
--------------------------------------------------------------------------------
1 | package util.retry.blocking
2 |
3 | import java.util.Random
4 |
5 | import scala.concurrent.duration.FiniteDuration
6 | import scala.concurrent.blocking
7 |
8 | /**
9 | * Interface defining a retry strategy
10 | */
11 | sealed trait RetryStrategy {
12 | /**
13 | * Returns `true` if the retry should be performed
14 | */
15 | def shouldRetry(): Boolean
16 |
17 | /**
18 | * Returns the new retry strategy state
19 | */
20 | def update(): RetryStrategy
21 | }
22 |
23 | /**
24 | * Simplest retry strategy that performs retry
25 | */
26 | object NoRetry extends RetryStrategy {
27 | override def shouldRetry(): Boolean = false
28 |
29 | override def update(): RetryStrategy = ???
30 | }
31 |
32 | class MaxNumberOfRetriesStrategy(val maxAttempts: Int) extends RetryStrategy {
33 | override def shouldRetry(): Boolean = maxAttempts > 0
34 |
35 | override def update(): RetryStrategy =
36 | new MaxNumberOfRetriesStrategy(maxAttempts = maxAttempts - 1)
37 | }
38 |
39 | class RetryForever(val waitTime: Int) extends RetryStrategy {
40 | override def shouldRetry(): Boolean = true
41 |
42 | override def update(): RetryStrategy =
43 | new RetryForever(waitTime)
44 | }
45 |
46 | class FixedWaitRetryStrategy(val millis: Long, override val maxAttempts: Int)
47 | extends MaxNumberOfRetriesStrategy(maxAttempts) with Sleep {
48 | override def update(): RetryStrategy = {
49 | sleep(millis)
50 | new FixedWaitRetryStrategy(millis, maxAttempts - 1)
51 | }
52 | }
53 |
54 | class RandomWaitRetryStrategy(val minimumWaitTime: Long, val maximumWaitTime: Long, override val maxAttempts: Int)
55 | extends MaxNumberOfRetriesStrategy(maxAttempts) with Sleep {
56 | private[this] final val random: Random = new Random()
57 |
58 | override def update(): RetryStrategy = {
59 | val millis: Long =
60 | math.abs(random.nextLong) % (maximumWaitTime - minimumWaitTime)
61 | sleep(millis)
62 | new RandomWaitRetryStrategy(
63 | minimumWaitTime,
64 | maximumWaitTime,
65 | maxAttempts - 1
66 | )
67 | }
68 | }
69 |
70 | class FibonacciBackOffStrategy(waitTime: Long, step: Long, override val maxAttempts: Int)
71 | extends MaxNumberOfRetriesStrategy(maxAttempts) with Sleep {
72 | def fibonacci(n: Long): Long = {
73 | n match {
74 | case x @ (0L | 1L) => x
75 | case _ =>
76 | var prevPrev: Long = 0L
77 | var prev: Long = 1L
78 | var result: Long = 0L
79 |
80 | for (_ <- 2L to n) {
81 | result = prev + prevPrev
82 | prevPrev = prev
83 | prev = result
84 | }
85 | result
86 | }
87 | }
88 |
89 | override def update(): RetryStrategy = {
90 | val millis: Long = fibonacci(step) * waitTime
91 | sleep(millis)
92 | new FibonacciBackOffStrategy(waitTime, step + 1, maxAttempts - 1)
93 | }
94 | }
95 |
96 | sealed trait Sleep {
97 | def sleep(millis: Long): Unit =
98 | try {
99 | blocking(Thread.sleep(millis))
100 | } catch {
101 | case e: InterruptedException =>
102 | Thread.currentThread().interrupt()
103 | throw e
104 | }
105 | }
106 |
107 | object RetryStrategy {
108 | type RetryStrategyProducer = () => RetryStrategy
109 |
110 | val noRetry: RetryStrategyProducer = () => NoRetry
111 |
112 | def noBackOff(maxAttempts: Int): RetryStrategyProducer =
113 | () => new MaxNumberOfRetriesStrategy(maxAttempts)
114 |
115 | def forever(waitTime: Int): RetryStrategyProducer =
116 | () => new RetryForever(waitTime)
117 |
118 | def fixedBackOff(retryDuration: FiniteDuration, maxAttempts: Int): RetryStrategyProducer =
119 | () => new FixedWaitRetryStrategy(retryDuration.toMillis, maxAttempts)
120 |
121 | def randomBackOff(
122 | minimumWaitDuration: FiniteDuration,
123 | maximumWaitDuration: FiniteDuration,
124 | maxAttempts: Int): RetryStrategyProducer =
125 | () => new RandomWaitRetryStrategy(minimumWaitDuration.toMillis, maximumWaitDuration.toMillis, maxAttempts)
126 |
127 | def fibonacciBackOff(initialWaitDuration: FiniteDuration, maxAttempts: Int): RetryStrategyProducer =
128 | () => new FibonacciBackOffStrategy(initialWaitDuration.toMillis, 1, maxAttempts)
129 | }
130 |
--------------------------------------------------------------------------------
/src/test/scala/RetryDslSpec.scala:
--------------------------------------------------------------------------------
1 |
2 | import util.retry.blocking.RetryStrategy.RetryStrategyProducer
3 | import util.retry.blocking._
4 |
5 | import scala.concurrent.{ExecutionContext, Future}
6 | import scala.concurrent.duration._
7 | import org.scalatest.flatspec.AnyFlatSpec
8 | import org.scalatest.matchers.should.Matchers
9 |
10 | /**
11 | * Created by dev on 11/03/16.
12 | */
13 | class RetryDslSpec extends AnyFlatSpec with Matchers {
14 |
15 | implicit val retryStrategy: RetryStrategyProducer =
16 | RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)
17 |
18 | "A `Retry` " should "be used in for comprehensions" in {
19 | val result = for {
20 | x <- Retry(1 / 1)
21 | y <- Retry(1 / 1)
22 | } yield x + y
23 |
24 | implicit val ec = ExecutionContext.global
25 | Retry(Future(1))
26 |
27 | result should be(Success(2))
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/scala/RetrySpec.scala:
--------------------------------------------------------------------------------
1 | import org.scalatest._
2 | import util.retry.blocking.RetryStrategy.RetryStrategyProducer
3 | import util.retry.blocking._
4 |
5 | import scala.concurrent.duration._
6 | import org.scalatest.flatspec.AnyFlatSpec
7 | import org.scalatest.matchers.should.Matchers
8 |
9 | /**
10 | * Created by dev on 7/2/14.
11 | */
12 | trait AbstractRetrySpec extends AnyFlatSpec with Matchers {
13 |
14 | implicit val retryStrategy: () => RetryStrategy
15 |
16 | "A `Retry` " should "return `Success` in case of a successful operation" in {
17 | Retry(1 / 1) should be(Success(1))
18 | }
19 |
20 | "A `Retry` " should "return `Failure` in case of a failed operation" in {
21 | Retry(1 / 0) match {
22 | case Failure(_) => () //OK
23 | case Success(_) => fail("Should return failure but it returned success")
24 | }
25 | }
26 |
27 | "A `Retry` " should "recover in case of a failure `Failure` " in {
28 | val result =
29 | Retry(1 / 0) recover {
30 | case _ => -1
31 | }
32 | result should be(Success(-1))
33 | }
34 |
35 | "A `Retry.get` " should " return the computed value" in {
36 | val result = Retry(1 / 1).get
37 | result should be(1)
38 | }
39 |
40 | "A `Retry.get` " should " throw an exception in case of a failure" in {
41 | try {
42 | Retry(1 / 0).get
43 | fail("should not get here")
44 | } catch {
45 | case _: ArithmeticException => ()
46 | }
47 | }
48 |
49 | "A `Retry.getOrElse` " should " should return a value even if an exception is thrown in the execution" in {
50 | val result = Retry(1 / 0).getOrElse(1)
51 | result should be(1)
52 | }
53 | }
54 |
55 | class NoRetrySpec extends AbstractRetrySpec {
56 | val retryStrategy:RetryStrategyProducer = RetryStrategy.noRetry
57 | }
58 |
59 | class NoBackOffRetrySpec extends AbstractRetrySpec {
60 | val retryStrategy: RetryStrategyProducer = RetryStrategy.noBackOff(maxAttempts = 3)
61 | }
62 |
63 | class FixedBackOffRetrySpec extends AbstractRetrySpec {
64 | val retryStrategy: RetryStrategyProducer =
65 | RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)
66 | }
67 |
68 | class FibonacciBackOffRetrySpec extends AbstractRetrySpec {
69 | val retryStrategy: RetryStrategyProducer = RetryStrategy.fibonacciBackOff(
70 | initialWaitDuration = 1.seconds, maxAttempts = 3)
71 | }
72 |
--------------------------------------------------------------------------------