├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── core ├── build.sbt └── src │ ├── main │ └── scala │ │ └── rebind │ │ ├── Count.scala │ │ ├── Rebind.scala │ │ ├── RetryPolicy.scala │ │ ├── std │ │ └── FutureAction.scala │ │ └── syntax │ │ ├── All.scala │ │ ├── Count.scala │ │ ├── Kleisli.scala │ │ ├── package.scala │ │ └── std │ │ ├── All.scala │ │ ├── FutureOps.scala │ │ └── package.scala │ └── test │ └── scala │ └── rebind │ ├── Error.scala │ ├── Positive.scala │ ├── RetryPolicySpec.scala │ ├── TestAction.scala │ └── std │ └── FutureActionSpec.scala └── project ├── build.properties └── plugins.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | .cache 15 | .classpath 16 | .project 17 | .worksheet/ 18 | bin/ 19 | .settings/ 20 | 21 | # OS X 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Adelbert Chang 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Adelbert Chang nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rebind 2 | 3 | [![Join the chat at https://gitter.im/adelbertc/rebind](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adelbertc/rebind?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Rebind is a Scala port/remake of the Haskell [retry](https://hackage.haskell.org/package/retry) library. One 6 | of the main differences is it is designed to work with `DisjunctionT`'s instead of `MonadIO` things. 7 | 8 | ## Getting Started 9 | Rebind is cross-built/published against Scala 2.10 and 2.11 with 10 | [Scalaz](https://github.com/scalaz/scalaz) 7.1 - Scalaz is (currently) its only dependency. 11 | 12 | To use it in your project, add the following to your SBT build definition: 13 | 14 | ``` 15 | resolvers += "adelbertc" at "http://dl.bintray.com/adelbertc/maven" 16 | 17 | libraryDependencies += "com.adelbertc" %% "rebind-core" % "0.2.0" 18 | ``` 19 | 20 | Despite the small bit of code that it is, there may well be breaking changes in the following versions. 21 | 22 | ### Usage 23 | Example usage can be found in the 24 | [tests](https://github.com/adelbertc/rebind/tree/master/core/src/test/scala/rebind). 25 | 26 | Because Rebind abstracts out the `F[_] : Monad` used in the `DisjunctionT`, it should (hopefully) be pretty easy 27 | to make it work with libraries like [Doobie](https://github.com/tpolecat/doobie) or 28 | [Dispatch](http://dispatch.databinder.net/Dispatch.html). The general idea is to get your "action" (probably 29 | in the form of `scalaz.concurrent.Task` or `scalaz.effect.IO`) and then use Rebind to specify how you want to 30 | retry in the case of failure to get a new retrying action back. The operations are stack stafe so long as `F[_]` 31 | is - common examples are `Task` and `IO`. 32 | 33 | ## License 34 | Code is provided under the BSD 3-Clause license available at http://opensource.org/licenses/BSD-3-Clause, as 35 | well as in the LICENSE file. This is the same license used as the retry library. 36 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "com.adelbertc" 2 | 3 | version in ThisBuild := "0.2.0" 4 | 5 | licenses in ThisBuild += ("BSD New", url("http://opensource.org/licenses/BSD-3-Clause")) 6 | 7 | scalaVersion in ThisBuild := "2.11.6" 8 | 9 | crossScalaVersions in ThisBuild := List("2.10.5", scalaVersion.value) 10 | 11 | scalacOptions in ThisBuild ++= Seq( 12 | "-deprecation", 13 | "-encoding", "UTF-8", 14 | "-feature", 15 | "-language:existentials", 16 | "-language:experimental.macros", 17 | "-language:higherKinds", 18 | "-language:implicitConversions", 19 | "-unchecked", 20 | "-Xfatal-warnings", 21 | "-Xlint", 22 | "-Xlog-reflective-calls", 23 | "-Yno-adapted-args", 24 | "-Ywarn-dead-code", 25 | "-Ywarn-numeric-widen", 26 | "-Ywarn-value-discard" 27 | ) 28 | 29 | lazy val core = project.in(file("core")) 30 | -------------------------------------------------------------------------------- /core/build.sbt: -------------------------------------------------------------------------------- 1 | name := "rebind-core" 2 | 3 | organization := "com.adelbertc" 4 | 5 | resolvers ++= Seq( 6 | "bintray/non" at "http://dl.bintray.com/non/maven", 7 | "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" 8 | ) 9 | 10 | val scalazVersion = "7.1.2" 11 | 12 | val specs2Version = "3.6.1" 13 | 14 | libraryDependencies ++= Seq( 15 | compilerPlugin("org.spire-math" %% "kind-projector" % "0.5.4"), 16 | 17 | "org.scalaz" %% "scalaz-core" % scalazVersion, 18 | "org.scalacheck" %% "scalacheck" % "1.12.2" % "test", 19 | "org.scalaz" %% "scalaz-scalacheck-binding" % scalazVersion % "test", 20 | "org.specs2" %% "specs2-core" % specs2Version % "test", 21 | "org.specs2" %% "specs2-scalacheck" % specs2Version % "test" 22 | ) 23 | 24 | seq(bintraySettings:_*) 25 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/Count.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | sealed abstract class Count { 4 | import Count._ 5 | 6 | def <(m: Int): Boolean = 7 | this match { 8 | case Finite(n) => n < m 9 | case Infinite => false 10 | } 11 | 12 | def ===(m: Int): Boolean = 13 | this match { 14 | case Finite(n) => n == m 15 | case Infinite => false 16 | } 17 | 18 | def <=(m: Int): Boolean = <(m) || ===(m) 19 | 20 | def >(m: Int): Boolean = !(<=(m)) 21 | 22 | def >=(m: Int): Boolean = !(<(m)) 23 | } 24 | 25 | object Count { 26 | final case class Finite(n: Int) extends Count 27 | final case object Infinite extends Count 28 | 29 | def finite(n: Int): Count = Count.Finite(n) 30 | val infinite: Count = Count.Infinite 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/Rebind.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import rebind.syntax.AllSyntax 4 | 5 | object Rebind 6 | extends RetryPolicyFunctions 7 | with AllSyntax 8 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/RetryPolicy.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import scala.concurrent.duration._ 4 | import scala.util.Random 5 | 6 | import scalaz.{Apply, Disjunction, DisjunctionT, DLeft, DRight, Equal, Foldable, IList, Monad, Semigroup, StateT, Zipper} 7 | import scalaz.std.option._ 8 | import scalaz.syntax.apply._ 9 | 10 | sealed abstract class RetryPolicy { outer => 11 | private[rebind] type S 12 | 13 | private[rebind] def initialState: S 14 | 15 | private[rebind] def transition: StateT[Option, S, FiniteDuration] 16 | 17 | /** Wait for a maximum of the specified time before trying again */ 18 | def capDelay(limit: FiniteDuration): RetryPolicy = 19 | new RetryPolicy { 20 | type S = outer.S 21 | 22 | def initialState = outer.initialState 23 | 24 | def transition: StateT[Option, S, FiniteDuration] = outer.transition.map(_.min(limit)) 25 | } 26 | 27 | /** Combine this policy with another. 28 | * 29 | * If either policy decides to stop retrying, then so will the resultant one. 30 | * 31 | * If both policies want to retry, the one with the greater delay will be used. 32 | * 33 | * Example: 34 | * {{{ 35 | * // Exponential backoff starting with 1 second, up to 5 times 36 | * RetryPolicy.exponentialBackoff(1.second) && RetryPolicy.limitRetries(5) 37 | * }}} 38 | */ 39 | def &&(other: RetryPolicy): RetryPolicy = 40 | new RetryPolicy { 41 | type S = (outer.S, other.S) 42 | 43 | def initialState = (outer.initialState, other.initialState) 44 | 45 | def transition: StateT[Option, (outer.S, other.S), FiniteDuration] = 46 | StateT { 47 | case (outerState, otherState) => 48 | Apply[Option].apply2(outer.transition(outerState), other.transition(otherState)) { 49 | case ((outerNext, outerVal), (otherNext, otherVal)) => 50 | ((outerNext, otherNext), outerVal.max(otherVal)) 51 | } 52 | } 53 | } 54 | 55 | /** Alias for `&&` */ 56 | def and(other: RetryPolicy): RetryPolicy = this && other 57 | 58 | /** Keep trying to recover until success or the policy is exhausted. */ 59 | def recover[F[_] : Monad, E, A](action: DisjunctionT[F, E, A])(handler: E => DisjunctionT[F, E, A]): DisjunctionT[F, E, A] = 60 | unfold(action.run, ())(e => handler(e).run, (_, _) => Option(())) 61 | 62 | /** Keep trying to recover until success or the policy is exhausted. 63 | * 64 | * Can retry with a different action on certain errors - unspecified errors will retry same action. 65 | */ 66 | def recoverWith[F[_] : Monad, E, A](action: DisjunctionT[F, E, A])( 67 | handler: PartialFunction[E, DisjunctionT[F, E, A]]): DisjunctionT[F, E, A] = 68 | recover(action)(e => if (handler.isDefinedAt(e)) handler(e) else action) 69 | 70 | /** Retry with error-specific limits, or when policy is exhausted. 71 | * 72 | * Limits are compared against the total number of times the error has occured so far, 73 | * regardless of when they occured (e.g. occured non-consecutively). 74 | */ 75 | def retry[F[_] : Monad, E : Equal, A](action: DisjunctionT[F, E, A])(limits: E => Count): DisjunctionT[F, E, A] = { 76 | def checkError(error: E, history: IList[(E, Int)]): Option[IList[(E, Int)]] = 77 | limits(error) match { 78 | case Count.Finite(0) => None 79 | case _ => 80 | val zipper = history.toZipper.flatMap { z => 81 | if (Equal[E].equal(z.focus._1, error)) Option(z) 82 | else z.findNext(p => Equal[E].equal(p._1, error)) 83 | } 84 | 85 | zipper match { 86 | case None => Option((error, 1) :: history) 87 | case Some(z) => 88 | val newCount = z.focus._2 + 1 89 | if (limits(error) >= newCount) { 90 | val updatedZipper = z.modify { case (e, _) => (e, newCount) } 91 | Option(Foldable[Zipper].foldRight(updatedZipper, IList.empty[(E, Int)])(_ :: _)) 92 | } else None 93 | } 94 | } 95 | 96 | val unwrapped = action.run 97 | 98 | unfold(unwrapped, IList.empty[(E, Int)])(Function.const(unwrapped), checkError) 99 | } 100 | 101 | /** Keep retrying on all errors until the policy is exhausted. */ 102 | def retryAll[F[_] : Monad, E, A](action: DisjunctionT[F, E, A]): DisjunctionT[F, E, A] = 103 | recover(action)(Function.const(action)) 104 | 105 | /** Retry with error-specific limits on consecutive errors, or when policy is exhausted. 106 | * 107 | * Limits are compared against consecutive occurences. For instance, if a particular error 108 | * is mapped to `5.times` and so far it has failed consecutively 4 times but then fails with 109 | * a different error, the count is reset. 110 | */ 111 | def retryConsecutive[F[_] : Monad, E : Equal, A](action: DisjunctionT[F, E, A])(limits: E => Count): DisjunctionT[F, E, A] = { 112 | def checkError(error: E, count: Option[(E, Int)]): Option[(Option[(E, Int)])] = 113 | count match { 114 | // first iteration 115 | case None => 116 | if (limits(error) > 0) Option(Option((error, 1))) 117 | else None 118 | 119 | // same error as last iteration 120 | case Some((e, n)) if Equal[E].equal(e, error) => 121 | val newCount = n + 1 122 | if (limits(error) >= newCount) Option(Option((e, newCount))) 123 | else None 124 | 125 | // different error as last iteration 126 | case Some((e, _)) => 127 | if (limits(error) > 0) Option(Option((error, 1))) 128 | else None 129 | } 130 | 131 | val unwrapped = action.run 132 | 133 | unfold(unwrapped, (Option.empty[(E, Int)]))(Function.const(unwrapped), checkError) 134 | } 135 | 136 | /** Retry certain errors on consecutive errors, or when policy is exhausted. 137 | * 138 | * Limits are compared against consecutive occurences. For instance, if a particular error 139 | * is mapped to `5.times` and so far it has failed consecutively 4 times but then fails with 140 | * a different error, the count is reset. 141 | */ 142 | def retryConsecutiveWith[F[_] : Monad, E : Equal, A](action: DisjunctionT[F, E, A])(limits: PartialFunction[E, Count]): DisjunctionT[F, E, A] = { 143 | val lifted = limits.lift 144 | retryConsecutive(action) { e => 145 | lifted(e).fold(Count.finite(0))(identity) 146 | } 147 | } 148 | 149 | /** Retry certain errors up to a limit, or when policy is exhausted. 150 | * 151 | * Limits are compared against the total number of times the error has occured so far, 152 | * regardless of when they occured (e.g. occured non-consecutively). 153 | */ 154 | def retryWith[F[_] : Monad, E : Equal, A](action: DisjunctionT[F, E, A])(limits: PartialFunction[E, Count]): DisjunctionT[F, E, A] = { 155 | val lifted = limits.lift 156 | retry(action) { e => 157 | lifted(e).fold(Count.finite(0))(identity) 158 | } 159 | } 160 | 161 | private def unfold[F[_] : Monad, E, A, T](currentAction: F[Disjunction[E, A]], initialTest: T)( 162 | next: E => F[Disjunction[E, A]], 163 | test: (E, T) => Option[T]): DisjunctionT[F, E, A] = { 164 | def go(action: F[Disjunction[E, A]], nextState: S, nextTest: T): F[Disjunction[E, A]] = 165 | Monad[F].bind(action) { d => 166 | val pointed = Monad[F].point(d) 167 | 168 | d match { 169 | case DLeft(e) => 170 | Apply[Option].tuple2(transition(nextState), test(e, nextTest)).fold(pointed) { 171 | case ((anotherState, delay), anotherTest) => 172 | Monad[F].point(DRight(Thread.sleep(delay.toMillis))) *> go(next(e), anotherState, anotherTest) 173 | } 174 | case DRight(_) => pointed 175 | } 176 | } 177 | 178 | DisjunctionT(go(currentAction, initialState, initialTest)) 179 | } 180 | 181 | } 182 | 183 | object RetryPolicy extends RetryPolicyInstances with RetryPolicyFunctions { 184 | /** Create a retry policy with a state transition function. 185 | * 186 | * Iterates with `next` starting with `initial`. `next` should return a `Some` of 187 | * a pair of `S` (the next state) and `FiniteDuration` (minimum time to wait before 188 | * next retry) if you want to retry again, or a `None` if you want to give up. 189 | */ 190 | def apply[S](initial: S)(next: S => Option[(S, FiniteDuration)]): RetryPolicy = 191 | stateT(initial)(StateT(next)) 192 | 193 | /** Create a retry policy with a state transition function. 194 | * 195 | * Iterates with `next` starting with `initial`. `next` should return a `Some` of 196 | * a pair of `S` (the next state) and `FiniteDuration` (minimum time to wait before 197 | * next retry) if you want to retry again, or a `None` if you want to give up. 198 | */ 199 | def stateT[T](initial: T)(next: StateT[Option, T, FiniteDuration]): RetryPolicy = 200 | new RetryPolicy { 201 | type S = T 202 | 203 | def initialState = initial 204 | 205 | def transition = next 206 | } 207 | } 208 | 209 | trait RetryPolicyInstances { 210 | implicit val retryPolicyInstance: Semigroup[RetryPolicy] = 211 | Semigroup.instance(_ && _) 212 | } 213 | 214 | trait RetryPolicyFunctions { 215 | /** Constantly retry, pausing a fixed amount in between */ 216 | def constantDelay(delay: FiniteDuration): RetryPolicy = 217 | RetryPolicy(())(Function.const(Option(((), delay)))) 218 | 219 | /** Exponential backoff, iterating indefinitely with a seed duration */ 220 | def exponentialBackoff(base: FiniteDuration): RetryPolicy = 221 | RetryPolicy(1L)(n => Option((2 * n, base * n))) 222 | 223 | /** Fibonacci backoff, iterating indefinitely with a seed duration */ 224 | def fibonacciBackoff(base: FiniteDuration): RetryPolicy = 225 | RetryPolicy((base, base)) { 226 | case (next, after) => 227 | val nextState = (after, next + after) 228 | Option((nextState, next)) 229 | } 230 | 231 | /** Constantly retry immediately */ 232 | def immediate: RetryPolicy = constantDelay(Duration.Zero) 233 | 234 | /** Constantly retry, starting at the specified base and iterating */ 235 | def iterateDelay(base: FiniteDuration)(f: FiniteDuration => FiniteDuration): RetryPolicy = 236 | RetryPolicy(base)(fd => Option((f(fd), fd))) 237 | 238 | /** Immediately retry the specified number of times */ 239 | def limitRetries(i: Int): RetryPolicy = 240 | RetryPolicy(0)(n => if (n < i) Option((n + 1, Duration.Zero)) else None) 241 | 242 | /** Constantly retry, pausing for pivot +/- epsilon. */ 243 | def random(pivot: FiniteDuration, epsilon: FiniteDuration): RetryPolicy = { 244 | def longMod(a: Long, n: Long): Long = 245 | a - (n * (a / n)) 246 | 247 | def random(): FiniteDuration = { 248 | val randomLong = math.abs(Random.nextLong()) 249 | val newEpsilon = longMod(randomLong, epsilon.toNanos) 250 | val randomDuration = newEpsilon.nanoseconds 251 | if (Random.nextBoolean()) pivot + randomDuration else pivot - randomDuration 252 | } 253 | 254 | iterateDelay(random())(_ => random()) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/std/FutureAction.scala: -------------------------------------------------------------------------------- 1 | package rebind.std 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | 5 | import scalaz.{Disjunction, DisjunctionT, Kleisli, Monad} 6 | import scalaz.std.scalaFuture._ 7 | 8 | /** Suspended Future that "always succeeds" - the contained value indicates success/failure via scalaz.Disjunction. */ 9 | final class FutureAction[A] private[std] (private val suspended: Kleisli[Future, Unit, A])(implicit ec: ExecutionContext) { 10 | def flatMap[B](f: A => FutureAction[B]): FutureAction[B] = 11 | new FutureAction(suspended.flatMap(a => f(a).suspended)) 12 | 13 | def map[B](f: A => B): FutureAction[B] = 14 | new FutureAction(suspended.map(f)) 15 | 16 | def unsafeRun(): Future[A] = suspended.run(()) 17 | } 18 | 19 | object FutureAction { companionObject => 20 | /** Convert a standard library Future into a DisjunctionT[FutureAction, Throwable, A]. 21 | * 22 | * This allows you to use Futures with this library. 23 | * 24 | * Note a DisjunctionT[FutureAction, Throwable, A] is similar to a 25 | * Future[Disjunction[Throwable, A]]. A FutureAction can be seen as a suspended Future 26 | * that can be retried. Moreover, while the standard library Future 27 | * has error-handling "built in", the transformed "Future" returned by this function 28 | * will always "succeed." The value contained within the Future will be either 29 | * a Disjunction left with a Throwable signaling an error (what would normally 30 | * be seen as a Failure), or a Disjunction right with the value we want (what 31 | * would normally be seen as a Success). 32 | */ 33 | def apply[A](future: => Future[A])(implicit ec: ExecutionContext): DisjunctionT[FutureAction, Throwable, A] = { 34 | val suspended = 35 | Kleisli.kleisli[Future, Unit, Disjunction[Throwable, A]] { _ => 36 | future.map(Disjunction.right[Throwable, A]).recover { case t => Disjunction.left(t) } 37 | } 38 | 39 | DisjunctionT(new FutureAction(suspended)) 40 | } 41 | 42 | def point[A](a: => A)(implicit ec: ExecutionContext): FutureAction[A] = 43 | new FutureAction(Monad[Kleisli[Future, Unit, ?]].point(a)) 44 | 45 | implicit def futureActionMonad(implicit ec: ExecutionContext): Monad[FutureAction] = 46 | new Monad[FutureAction] { 47 | def bind[A, B](fa: FutureAction[A])(f: A => FutureAction[B]): FutureAction[B] = 48 | fa.flatMap(f) 49 | 50 | def point[A](a: => A): FutureAction[A] = companionObject.point(a) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/All.scala: -------------------------------------------------------------------------------- 1 | package rebind.syntax 2 | 3 | trait AllSyntax 4 | extends CountSyntax 5 | with KleisliSyntax 6 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/Count.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | package syntax 3 | 4 | trait CountSyntax { 5 | implicit def countSyntax(i: Int): CountOps = new CountOps(i) 6 | 7 | val Infinite = Count.Infinite 8 | } 9 | 10 | class CountOps(val i: Int) extends AnyVal { 11 | def time: Count = times 12 | 13 | def times: Count = Count.Finite(i) 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/Kleisli.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | package syntax 3 | 4 | import scalaz.{DisjunctionT, Equal, Kleisli, Monad} 5 | 6 | trait KleisliSyntax { 7 | implicit def kleisliSyntax[F[_], E, A](action: DisjunctionT[F, E, A]): KleisliOps[F, E, A] = 8 | new KleisliOps(action) 9 | } 10 | 11 | class KleisliOps[F[_], E, A](val action: DisjunctionT[F, E, A]) extends AnyVal { 12 | import KleisliOps.KleisliAction 13 | 14 | private def lift(f: RetryPolicy => DisjunctionT[F, E, A]): KleisliAction[F, E, A] = 15 | Kleisli[DisjunctionT[F, E, ?], RetryPolicy, A](f) 16 | 17 | def recover(handler: E => DisjunctionT[F, E, A])(implicit F: Monad[F]): KleisliAction[F, E, A] = 18 | lift(_.recover(action)(handler)) 19 | 20 | def recoverWith(handler: PartialFunction[E, DisjunctionT[F, E, A]])(implicit F: Monad[F]): KleisliAction[F, E, A] = 21 | lift(_.recoverWith(action)(handler)) 22 | 23 | def retry(limits: E => Count)(implicit E: Equal[E], F: Monad[F]): KleisliAction[F, E, A] = 24 | lift(_.retry(action)(limits)) 25 | 26 | def retryAll(implicit F: Monad[F]): KleisliAction[F, E, A] = 27 | lift(_.retryAll(action)) 28 | 29 | def retryConsecutive(limits: E => Count)(implicit E: Equal[E], F: Monad[F]): KleisliAction[F, E, A] = 30 | lift(_.retryConsecutive(action)(limits)) 31 | 32 | def retryConsecutiveWith(limits: PartialFunction[E, Count])(implicit E: Equal[E], F: Monad[F]): KleisliAction[F, E, A] = 33 | lift(_.retryConsecutiveWith(action)(limits)) 34 | 35 | def retryWith(limits: PartialFunction[E, Count])(implicit E: Equal[E], F: Monad[F]): KleisliAction[F, E, A] = 36 | lift(_.retryWith(action)(limits)) 37 | } 38 | 39 | object KleisliOps { 40 | type KleisliAction[F[_], E, A] = Kleisli[DisjunctionT[F, E, ?], RetryPolicy, A] 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/package.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | package object syntax { 4 | object all extends AllSyntax 5 | object count extends CountSyntax 6 | object kleisli extends KleisliSyntax 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/std/All.scala: -------------------------------------------------------------------------------- 1 | package rebind.syntax.std 2 | 3 | trait AllStdSyntax 4 | extends FutureSyntax 5 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/std/FutureOps.scala: -------------------------------------------------------------------------------- 1 | package rebind.syntax.std 2 | 3 | import rebind.std.FutureAction 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | import scalaz.DisjunctionT 8 | 9 | trait FutureSyntax { 10 | implicit def futureSyntax[A](future: => Future[A])(implicit ec: ExecutionContext): FutureOps[A] = 11 | new FutureOps(future) 12 | } 13 | 14 | class FutureOps[A](future: => Future[A])(implicit ec: ExecutionContext) { 15 | def action: DisjunctionT[FutureAction, Throwable, A] = FutureAction(future) 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/rebind/syntax/std/package.scala: -------------------------------------------------------------------------------- 1 | package rebind.syntax 2 | 3 | package object std { 4 | object all extends AllStdSyntax 5 | object future extends FutureSyntax 6 | } 7 | -------------------------------------------------------------------------------- /core/src/test/scala/rebind/Error.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import org.scalacheck._ 4 | import org.scalacheck.Arbitrary._ 5 | 6 | import scalaz.Equal 7 | 8 | final case object Oops { 9 | implicit val oopsEqual: Equal[Oops.type] = Equal.equalA 10 | } 11 | 12 | sealed abstract class UhOh 13 | final case object Uh extends UhOh 14 | final case object Oh extends UhOh 15 | 16 | object UhOh { 17 | implicit val uhOhEqual: Equal[UhOh] = Equal.equalA 18 | 19 | implicit val uhOhArbitrary: Arbitrary[UhOh] = 20 | Arbitrary(Gen.oneOf(List[UhOh](Uh, Oh))) 21 | } 22 | -------------------------------------------------------------------------------- /core/src/test/scala/rebind/Positive.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import org.scalacheck._ 4 | import org.scalacheck.Arbitrary._ 5 | 6 | final class PositiveByte private(val byte: Byte) extends AnyVal { 7 | def int: Int = byte.toInt 8 | } 9 | 10 | object PositiveByte { 11 | implicit val positiveByteArbitrary: Arbitrary[PositiveByte] = 12 | Arbitrary(Gen.chooseNum[Byte](1, Byte.MaxValue).map(b => new PositiveByte(b))) 13 | } 14 | 15 | final class PositiveInt private(val int: Int) extends AnyVal 16 | 17 | object PositiveInt { 18 | implicit val positiveIntArbitrary: Arbitrary[PositiveInt] = 19 | Arbitrary(Gen.chooseNum[Int](1, Int.MaxValue).map(i => new PositiveInt(i))) 20 | } 21 | -------------------------------------------------------------------------------- /core/src/test/scala/rebind/RetryPolicySpec.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import org.scalacheck._ 4 | import org.scalacheck.Arbitrary._ 5 | 6 | import org.specs2._ 7 | import org.specs2.time.NoTimeConversions 8 | 9 | import scala.concurrent.duration._ 10 | 11 | import scalaz.{Disjunction, DisjunctionT, Equal, Monad, Name, StateT} 12 | import scalaz.scalacheck.ScalazProperties.semigroup 13 | import scalaz.scalacheck.ScalazArbitrary.indexedStateTArb 14 | import scalaz.std.AllInstances._ 15 | 16 | class RetryPolicySpec extends Specification with ScalaCheck with RetryPolicySpecInstances { 17 | def is = 18 | s2""" 19 | capDelay ${capDelay} 20 | limitRetries ${limitRetries} 21 | iterateDelay ${iterateDelay} 22 | iterateDelay memoizes ${iterateDelayMemoize} 23 | constantDelay ${constantDelay} 24 | immediate ${immediate} 25 | exponentialBackoff ${exponentialBackoff} 26 | fibonaciBackoff ${fibonaciBackoff} 27 | 28 | law checking 29 | semigroup ${semigroup.laws[RetryPolicy]} 30 | 31 | recover 32 | uses handler ${recoverUsesHandler} 33 | retries until success ${recoverUntilSuccess} 34 | exhausts policy ${recoverExhaustPolicy} 35 | 36 | recoverWith 37 | retries on unhandled error ${recoverWithRetriesUnhandled} 38 | 39 | retry 40 | retries until success ${retryUntilSuccess} 41 | is error-specific (success) ${retryErrorSpecificSuccess} 42 | is error-specific (failure) ${retryErrorSpecificFailure} 43 | obeys limits ${retryObeyLimit} 44 | exhausts policy ${retryExhaustPolicy} 45 | 46 | retryAll 47 | retries until success ${retryAllUntilSuccess} 48 | exhausts policy ${retryAllExhaustPolicy} 49 | 50 | retryConsecutive 51 | retries until success ${retryConsecutiveUntilSuccess} 52 | is error-specific (success) ${retryConsecutiveErrorSpecificSuccess} 53 | is error-specific (failure) ${retryConsecutiveErrorSpecificFailure} 54 | obeys limits ${retryConsecutiveObeyLimit} 55 | exhausts policy ${retryConsecutiveExhaustPolicy} 56 | 57 | retryConsecutiveWith 58 | doesn't retry unhandled error ${retryConsecutiveWithUnhandled} 59 | 60 | retryWith 61 | doesn't retry unhandled error ${retryWithUnhandled} 62 | """ 63 | 64 | val failingAction = DisjunctionT.left[Name, Oops.type, Unit](Name(Oops)) 65 | 66 | val rightUnit = Disjunction.right(()) 67 | 68 | val toEval = 100 69 | 70 | def evalPolicyMany(n: Int)(policy: RetryPolicy): Option[List[FiniteDuration]] = 71 | Monad[StateT[Option, policy.S, ?]].replicateM(n, policy.transition).eval(policy.initialState) 72 | 73 | def evalPolicyAll[A](n: Int, policy: RetryPolicy, a: A) = 74 | evalPolicyMany(n)(policy) must beSome((fds: List[FiniteDuration]) => fds must contain(beEqualTo(a)).forall) 75 | 76 | def evalPolicyExpected(n: Int, policy: RetryPolicy, expected: List[FiniteDuration]) = 77 | evalPolicyMany(n)(policy) must beSome((fds: List[FiniteDuration]) => fds mustEqual expected) 78 | 79 | type PolicyFunction[E] = RetryPolicy => DisjunctionT[Name, E, Unit] => (E => Count) => DisjunctionT[Name, E, Unit] 80 | 81 | def untilSuccess(policy: PolicyFunction[Oops.type]) = 82 | prop { (pb: PositiveByte) => 83 | val action = new TestAction(pb.int, Oops, ()) 84 | val retriedAction = policy(RetryPolicy.immediate)(action.run())(_ => Count.Infinite) 85 | retriedAction.run.value mustEqual rightUnit 86 | } 87 | 88 | def errorSpecificSuccess(policy: PolicyFunction[UhOh]) = 89 | prop { (pb: PositiveByte) => 90 | val positive = pb.int 91 | 92 | val action = new TestAction[UhOh, Unit](positive, Uh, ()) 93 | 94 | val retriedAction = 95 | policy(RetryPolicy.immediate)(action.run()) { 96 | case Uh => Count.Finite(positive) 97 | case Oh => Count.Infinite 98 | } 99 | 100 | retriedAction.run.value mustEqual rightUnit 101 | } 102 | 103 | def errorSpecificFailure(policy: PolicyFunction[UhOh]) = 104 | prop { (ipb: PositiveByte, jpb: PositiveByte) => (ipb != jpb) ==> { 105 | val i = ipb.int 106 | val j = jpb.int 107 | 108 | val lower = i.min(j) 109 | val higher = i.max(j) 110 | 111 | val action = new TestAction[UhOh, Unit](higher, Uh, ()) 112 | 113 | val retriedAction = 114 | policy(RetryPolicy.immediate)(action.run()) { 115 | case Uh => Count.Finite(lower) 116 | case Oh => Count.Infinite 117 | } 118 | 119 | retriedAction.run.value mustEqual Disjunction.left(Uh) 120 | }} 121 | 122 | def exhaustPolicy(policy: PolicyFunction[Oops.type]) = 123 | prop { (pb: PositiveByte) => 124 | val positive = pb.int 125 | 126 | val policy = RetryPolicy.limitRetries(positive) 127 | 128 | val retriedAction = policy.retry(failingAction)(_ => Count.Infinite) 129 | retriedAction.run.value mustEqual failingAction.run.value 130 | } 131 | 132 | /* Tests */ 133 | 134 | def capDelay = 135 | prop { (i: FiniteDuration, j: FiniteDuration) => 136 | val lower = i.min(j) 137 | val higher = i.max(j) 138 | val policy = RetryPolicy.constantDelay(higher).capDelay(lower) 139 | 140 | evalPolicyAll(toEval, policy, lower) 141 | } 142 | 143 | def limitRetries = 144 | prop { (pb: PositiveByte) => 145 | val i = pb.int 146 | val policy = RetryPolicy.limitRetries(i) 147 | 148 | val at = evalPolicyMany(i + 1)(policy) 149 | 150 | evalPolicyAll(i, policy, Duration.Zero) and (at must beNone) 151 | } 152 | 153 | def iterateDelay = { 154 | val policy = RetryPolicy.iterateDelay(1.second)(_ * 10) 155 | val expected = List(1.second, 10.seconds, 100.seconds, 1000.seconds, 10000.seconds) 156 | 157 | evalPolicyExpected(expected.size, policy, expected) 158 | } 159 | 160 | def iterateDelayMemoize = 161 | prop { (pb: PositiveByte, fd: FiniteDuration) => 162 | val i = pb.int 163 | var counter = 0 164 | val policy = RetryPolicy.iterateDelay(fd) { fd => counter += 1; fd } 165 | val expected = List.fill(i)(fd) 166 | 167 | evalPolicyExpected(expected.size, policy, expected) and (counter mustEqual i) 168 | } 169 | 170 | def constantDelay = 171 | prop { (delay: FiniteDuration) => 172 | val policy = RetryPolicy.constantDelay(delay) 173 | 174 | evalPolicyAll(toEval, policy, delay) 175 | } 176 | 177 | def immediate = evalPolicyAll(toEval, RetryPolicy.immediate, Duration.Zero) 178 | 179 | def exponentialBackoff = { 180 | val policy = RetryPolicy.exponentialBackoff(1.second) 181 | val expected = List(1.second, 2.seconds, 4.seconds, 8.seconds, 16.seconds) 182 | 183 | evalPolicyExpected(expected.size, policy, expected) 184 | } 185 | 186 | def fibonaciBackoff = { 187 | val policy = RetryPolicy.fibonacciBackoff(1.second) 188 | val expected = List(1.second, 1.second, 2.seconds, 3.seconds, 5.seconds) 189 | 190 | evalPolicyExpected(expected.size, policy, expected) 191 | } 192 | 193 | /* RetryPolicy#recover */ 194 | 195 | def recoverUsesHandler = { 196 | val failWithUhAction = DisjunctionT.left[Name, UhOh, String](Name(Uh)) 197 | 198 | val recoverString = "recovered" 199 | val recoveringAction = DisjunctionT.right[Name, UhOh, String](Name(recoverString)) 200 | 201 | val shouldNotBeString = "should not happen" 202 | val shouldNotBeAction = DisjunctionT.right[Name, UhOh, String](Name(shouldNotBeString)) 203 | 204 | val retriedAction = 205 | RetryPolicy.immediate.recover(failWithUhAction) { 206 | case Uh => recoveringAction 207 | case Oh => shouldNotBeAction 208 | } 209 | 210 | retriedAction.run.value mustEqual recoveringAction.run.value 211 | } 212 | 213 | def recoverUntilSuccess = 214 | prop { (pb: PositiveByte) => 215 | val positive = pb.int 216 | 217 | val action = new TestAction(positive, Oops, ()) 218 | 219 | var counter = 0 220 | val retriedAction = RetryPolicy.immediate.recover(action.run()) { _ => counter += 1; action.run() } 221 | (retriedAction.run.value mustEqual rightUnit) and (counter mustEqual positive) 222 | } 223 | 224 | def recoverExhaustPolicy = { 225 | val function: PolicyFunction[Oops.type] = policy => action => m => 226 | policy.recover(action)(_ => action) 227 | 228 | exhaustPolicy(function) 229 | } 230 | 231 | /* RetryPolicy#recoverWith */ 232 | 233 | def recoverWithRetriesUnhandled = { 234 | val failWithUhAction = DisjunctionT.left[Name, UhOh, String](Name(Uh)) 235 | 236 | val shouldNotBeString = "should not happen" 237 | val shouldNotBeAction = DisjunctionT.right[Name, UhOh, String](Name(shouldNotBeString)) 238 | 239 | val retriedAction = 240 | RetryPolicy.limitRetries(3).recoverWith(failWithUhAction) { 241 | case Oh => shouldNotBeAction 242 | } 243 | 244 | retriedAction.run.value mustEqual failWithUhAction.run.value 245 | } 246 | 247 | /* RetryPolicy#retry */ 248 | 249 | def retryUntilSuccess = untilSuccess(_.retry) 250 | 251 | def retryErrorSpecificSuccess = errorSpecificSuccess(_.retry) 252 | 253 | def retryErrorSpecificFailure = errorSpecificFailure(_.retry) 254 | 255 | def retryObeyLimit = 256 | prop { (es: List[UhOh]) => 257 | val limited = es.take(Byte.MaxValue) 258 | val numberOfOhs = limited.count(Equal[UhOh].equal(Uh, _)) + 1 259 | val stream = limited.toStream ++ Stream(Uh) ++ Stream.continually(Oh) 260 | 261 | val action = new TestAction(stream, ()) 262 | val retriedAction = 263 | RetryPolicy.immediate.retry(action.run()) { 264 | case Uh => Count.Finite(numberOfOhs - 1) 265 | case Oh => Count.Infinite 266 | } 267 | 268 | retriedAction.run.value mustEqual Disjunction.left(Uh) 269 | } 270 | 271 | def retryExhaustPolicy = exhaustPolicy(_.retry) 272 | 273 | /* RetryPolicy#retryAll */ 274 | 275 | def retryAllUntilSuccess = 276 | prop { (pb: PositiveByte) => 277 | val positive = pb.int 278 | 279 | val action = new TestAction(positive, Oops, ()) 280 | val retriedAction = RetryPolicy.immediate.retryAll(action.run()) 281 | retriedAction.run.value mustEqual rightUnit 282 | } 283 | 284 | def retryAllExhaustPolicy = { 285 | val function: PolicyFunction[Oops.type] = policy => action => m => policy.retryAll(action) 286 | exhaustPolicy(function) 287 | } 288 | 289 | /* RetryPolicy#retryConsecutive */ 290 | 291 | def retryConsecutiveUntilSuccess = untilSuccess(_.retryConsecutive) 292 | 293 | def retryConsecutiveErrorSpecificSuccess = errorSpecificSuccess(_.retryConsecutive) 294 | 295 | def retryConsecutiveErrorSpecificFailure = errorSpecificFailure(_.retryConsecutive) 296 | 297 | def retryConsecutiveObeyLimit = 298 | prop { (pb: PositiveByte) => 299 | val i = pb.int 300 | val first = i - 1 301 | val last = i 302 | val errors: Stream[UhOh] = Stream.fill(first)(Uh) ++ Stream(Oh) ++ Stream.fill(last + 1)(Uh) 303 | val action = new TestAction(errors, ()) 304 | val retriedAction = 305 | RetryPolicy.immediate.retryConsecutive(action.run()) { 306 | case Uh => Count.Finite(last) 307 | case Oh => Count.Infinite 308 | } 309 | 310 | (retriedAction.run.value mustEqual Disjunction.left(Uh)) and (action.run().run.value mustEqual rightUnit) 311 | } 312 | 313 | def retryConsecutiveExhaustPolicy = exhaustPolicy(_.retryConsecutive) 314 | 315 | /* RetryPolicy#retryConsecutiveWith */ 316 | 317 | def retryConsecutiveWithUnhandled = { 318 | val failWithUhAction = DisjunctionT.left[Name, UhOh, String](Name(Uh)) 319 | 320 | val retriedAction = 321 | RetryPolicy.limitRetries(3).retryConsecutiveWith(failWithUhAction) { 322 | case Oh => Count.Infinite 323 | } 324 | 325 | retriedAction.run.value mustEqual failWithUhAction.run.value 326 | } 327 | 328 | /* RetryPolicy#retryWith */ 329 | 330 | def retryWithUnhandled = { 331 | val failWithUhAction = DisjunctionT.left[Name, UhOh, String](Name(Uh)) 332 | 333 | val retriedAction = 334 | RetryPolicy.limitRetries(3).retryWith(failWithUhAction) { 335 | case Oh => Count.Infinite 336 | } 337 | 338 | retriedAction.run.value mustEqual failWithUhAction.run.value 339 | } 340 | } 341 | 342 | trait RetryPolicySpecInstances extends OrphanInstances { 343 | implicit val retryPolicyEqual: Equal[RetryPolicy] = 344 | Equal.equalBy { rp => 345 | Monad[StateT[Option, rp.S, ?]].replicateM(100, rp.transition).eval(rp.initialState) 346 | } 347 | 348 | implicit val retryPolicyArbitrary: Arbitrary[RetryPolicy] = 349 | Arbitrary(arbitrary[(Int, StateT[Option, Int, FiniteDuration])].map { 350 | case (initialState, transition) => RetryPolicy.stateT(initialState)(transition) 351 | }) 352 | } 353 | 354 | trait OrphanInstances { 355 | implicit val finiteDurationArbitrary: Arbitrary[FiniteDuration] = { 356 | val bound = math.pow(2, 63).toLong - 1 357 | 358 | Arbitrary(Gen.chooseNum(0L, bound).map(_.nanoseconds)) 359 | } 360 | 361 | implicit val finiteDurationEqual: Equal[FiniteDuration] = Equal.equalA 362 | } 363 | -------------------------------------------------------------------------------- /core/src/test/scala/rebind/TestAction.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | 3 | import scalaz.{Disjunction, DisjunctionT, Name} 4 | 5 | final class TestAction[E, A](private var errorStream: Stream[E], private val success: A) { 6 | def this(n: Int, error: E, success: A) = this(Stream.fill(n)(error), success) 7 | 8 | def run(): DisjunctionT[Name, E, A] = 9 | DisjunctionT { 10 | Name { 11 | errorStream match { 12 | case Stream.Empty => Disjunction.right(success) 13 | case e #:: es => 14 | errorStream = es 15 | Disjunction.left(e) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/scala/rebind/std/FutureActionSpec.scala: -------------------------------------------------------------------------------- 1 | package rebind 2 | package std 3 | 4 | import org.specs2.Specification 5 | 6 | import scala.concurrent.{Await, Future} 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.concurrent.duration.Duration 9 | 10 | import scalaz.{Disjunction, DisjunctionT} 11 | 12 | class FutureActionSpec extends Specification { 13 | def is = 14 | s2""" 15 | Success ${futureSuccess} 16 | Failure ${futureFailure} 17 | Retry ${futureRetry} 18 | """ 19 | 20 | def testFuture[A](have: DisjunctionT[FutureAction, Throwable, A], expected: Disjunction[Throwable, A]) = 21 | Await.result(have.run.unsafeRun(), Duration.Inf) mustEqual expected 22 | 23 | def futureSuccess = { 24 | val r = "future" 25 | val future = FutureAction(Future.successful(r)) 26 | val retried = RetryPolicy.immediate.retryAll(future) 27 | testFuture(retried, Disjunction.right(r)) 28 | } 29 | 30 | def futureFailure = { 31 | val r = new Exception("oops") 32 | val future = FutureAction[String](Future.failed(r)) 33 | val retried = RetryPolicy.limitRetries(5).retryAll(future) 34 | testFuture(retried, Disjunction.left(r)) 35 | } 36 | 37 | 38 | final case object FutureException extends Exception 39 | def futureRetry = { 40 | val failingFuture = FutureAction[String](Future.failed(FutureException)) 41 | val r = "future" 42 | val successfulFuture = FutureAction(Future.successful(r)) 43 | val retried = RetryPolicy.immediate.recoverWith(failingFuture) { case FutureException => successfulFuture } 44 | testFuture(retried, Disjunction.right(r)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.7 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url( 2 | "bintray-sbt-plugin-releases", 3 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases"))( 4 | Resolver.ivyStylePatterns) 5 | 6 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.2.0") 7 | --------------------------------------------------------------------------------