Skip to content

Commit 8af8301

Browse files
committed
Retry: add MTL-specific tests
1 parent ff5bc74 commit 8af8301

4 files changed

Lines changed: 153 additions & 52 deletions

File tree

core/shared/src/main/scala/cats/effect/IO.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,14 +662,14 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
662662
* @param policy
663663
* the policy to use
664664
*
665-
* @param onRetry
665+
* @param onError
666666
* the effect to invoke on every retry decision
667667
*/
668668
def retry(
669669
policy: Retry[IO, Throwable],
670-
onRetry: (Retry.Status, Throwable, Retry.Decision) => IO[Unit]
670+
onError: (Retry.Status, Throwable, Retry.Decision) => IO[Unit]
671671
): IO[A] =
672-
Retry.retry(policy, onRetry)(this)
672+
Retry.retry(policy, onError)(this)
673673

674674
/**
675675
* Inverse of `attempt`

std/shared/src/main/scala/cats/effect/std/Retry.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ object Retry {
381381
* Creates an error matcher that matches all errors.
382382
*/
383383
def all[F[_]: Applicative, E]: ErrorMatcher[F, E] =
384-
new Impl[F, E]({ (_: E) => Applicative[F].pure(true) })
384+
new Impl[F, E]({ case _: E => Applicative[F].pure(true) })
385385

386386
/**
387387
* Creates an error matcher using the given `matcher` under the hood.
@@ -486,17 +486,17 @@ object Retry {
486486
* @param policy
487487
* the policy to use
488488
*
489-
* @param onRetry
489+
* @param onError
490490
* the effect to invoke on every retry decision
491491
*
492492
* @param fa
493493
* the effect
494494
*/
495495
def retry[F[_], A, E](
496496
policy: Retry[F, E],
497-
onRetry: (Status, E, Decision) => F[Unit]
497+
onError: (Status, E, Decision) => F[Unit]
498498
)(fa: F[A])(implicit F: GenTemporal[F, E]): F[A] =
499-
doRetry(policy, Some(onRetry))(fa)
499+
doRetry(policy, Some(onError))(fa)
500500

501501
/**
502502
* The return policy that always gives up.

std/shared/src/main/scala/cats/effect/std/syntax/RetrySyntax.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ final class RetryOps[F[_], A] private[syntax] (private val fa: F[A]) extends Any
3131

3232
def retry[E](
3333
policy: Retry[F, E],
34-
onRetry: (Retry.Status, E, Retry.Decision) => F[Unit]
34+
onError: (Retry.Status, E, Retry.Decision) => F[Unit]
3535
)(implicit F: GenTemporal[F, E]): F[A] =
36-
Retry.retry(policy, onRetry)(fa)
36+
Retry.retry(policy, onError)(fa)
3737

3838
}

tests/shared/src/test/scala/cats/effect/std/RetrySpec.scala

Lines changed: 144 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package cats.effect.std
1818

1919
import cats.{Hash, Show}
20-
import cats.effect.{BaseSpec, IO, Ref}
20+
import cats.data.EitherT
21+
import cats.effect.{BaseSpec, IO, Ref, Temporal}
22+
import cats.mtl.Handle
2123
import cats.syntax.applicative._
24+
import cats.syntax.flatMap._
2225
import cats.syntax.functor._
2326
import cats.syntax.semigroup._
2427

@@ -72,11 +75,10 @@ class RetrySpec extends BaseSpec {
7275
val delay = 2.second
7376
val capDelay = 1.second
7477

75-
val policy =
76-
Retry
77-
.constantDelay[IO, Throwable](delay)
78-
.withCappedDelay(capDelay)
79-
.withMaxRetries(maxRetries)
78+
val policy = Retry
79+
.constantDelay[IO, Throwable](delay)
80+
.withCappedDelay(capDelay)
81+
.withMaxRetries(maxRetries)
8082

8183
val expected = {
8284
val retries = List.tabulate(maxRetries) { i =>
@@ -199,11 +201,10 @@ class RetrySpec extends BaseSpec {
199201
val delay = 1.second
200202

201203
val error = new Error1
202-
val policy =
203-
Retry
204-
.constantDelay[IO, Throwable](delay)
205-
.withMaxRetries(maxRetries)
206-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
204+
val policy = Retry
205+
.constantDelay[IO, Throwable](delay)
206+
.withMaxRetries(maxRetries)
207+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
207208

208209
val expected = List(
209210
RetryAttempt(Status(0, Duration.Zero), Decision.retry(delay), error),
@@ -218,11 +219,10 @@ class RetrySpec extends BaseSpec {
218219
val maxRetries = 5
219220
val delay = 1.second
220221

221-
val policy =
222-
Retry
223-
.constantDelay[IO, Throwable](delay)
224-
.withMaxRetries(maxRetries)
225-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
222+
val policy = Retry
223+
.constantDelay[IO, Throwable](delay)
224+
.withMaxRetries(maxRetries)
225+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
226226

227227
val expected = List(
228228
RetryAttempt(Status(0, Duration.Zero), Decision.giveUp)
@@ -235,12 +235,11 @@ class RetrySpec extends BaseSpec {
235235
val delay = 1.second
236236
val maxRetries = 1
237237

238-
val policy =
239-
Retry
240-
.constantDelay[IO, Throwable](delay)
241-
.withMaxRetries(maxRetries)
242-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
243-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error2])
238+
val policy = Retry
239+
.constantDelay[IO, Throwable](delay)
240+
.withMaxRetries(maxRetries)
241+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
242+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error2])
244243

245244
val error = new Error1
246245
val expected = List(
@@ -254,12 +253,11 @@ class RetrySpec extends BaseSpec {
254253
val delay = 1.second
255254
val maxRetries = 2
256255

257-
val policy =
258-
Retry
259-
.constantDelay[IO, Throwable](delay)
260-
.withMaxRetries(maxRetries)
261-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
262-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error2])
256+
val policy = Retry
257+
.constantDelay[IO, Throwable](delay)
258+
.withMaxRetries(maxRetries)
259+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error1])
260+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].only[Error2])
263261

264262
val error = new Error2
265263
val expected = List(
@@ -276,11 +274,10 @@ class RetrySpec extends BaseSpec {
276274
val delay = 1.second
277275

278276
val error = new Error1
279-
val policy =
280-
Retry
281-
.constantDelay[IO, Throwable](delay)
282-
.withMaxRetries(maxRetries)
283-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[Error1])
277+
val policy = Retry
278+
.constantDelay[IO, Throwable](delay)
279+
.withMaxRetries(maxRetries)
280+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[Error1])
284281

285282
val expected = List(
286283
RetryAttempt(Status(0, Duration.Zero), Decision.giveUp, error)
@@ -294,11 +291,10 @@ class RetrySpec extends BaseSpec {
294291
val delay = 1.second
295292

296293
val error = new Error1
297-
val policy =
298-
Retry
299-
.constantDelay[IO, Throwable](delay)
300-
.withMaxRetries(maxRetries)
301-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[RuntimeException])
294+
val policy = Retry
295+
.constantDelay[IO, Throwable](delay)
296+
.withMaxRetries(maxRetries)
297+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[RuntimeException])
302298

303299
val expected = List(
304300
RetryAttempt(Status(0, Duration.Zero), Decision.giveUp, error)
@@ -313,11 +309,10 @@ class RetrySpec extends BaseSpec {
313309
val delay = 1.second
314310

315311
val error = new Error2
316-
val policy =
317-
Retry
318-
.constantDelay[IO, Throwable](delay)
319-
.withMaxRetries(maxRetries)
320-
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[Error1])
312+
val policy = Retry
313+
.constantDelay[IO, Throwable](delay)
314+
.withMaxRetries(maxRetries)
315+
.withErrorMatcher(Retry.ErrorMatcher[IO, Throwable].except[Error1])
321316

322317
val expected = List(
323318
RetryAttempt(Status(0, Duration.Zero), Decision.retry(delay), error),
@@ -453,6 +448,112 @@ class RetrySpec extends BaseSpec {
453448

454449
}
455450

451+
"Retry MTL" should {
452+
453+
sealed trait Errors
454+
final class Error1 extends Errors
455+
final class Error2 extends Errors
456+
457+
type RetryAttempt = (Status, Decision, Errors)
458+
459+
def mtlRetry[F[_], E, A](
460+
action: F[A],
461+
policy: Retry[F, E],
462+
onRetry: (Status, E, Decision) => F[Unit]
463+
)(implicit F: Temporal[F], H: Handle[F, E]): F[A] =
464+
F.tailRecM(Status.initial) { status =>
465+
H.attempt(action).flatMap {
466+
case Left(error) =>
467+
policy
468+
.decide(status, error)
469+
.flatTap(decision => onRetry(status, error, decision))
470+
.flatMap {
471+
case retry: Decision.Retry =>
472+
F.delayBy(F.pure(Left(status.withRetry(retry.delay))), retry.delay)
473+
474+
case _: Decision.GiveUp =>
475+
H.raise(error)
476+
}
477+
478+
case Right(success) =>
479+
F.pure(Right(success))
480+
}
481+
}
482+
483+
implicit val outputHash: Hash[(Either[Errors, Unit], List[RetryAttempt])] =
484+
Hash.fromUniversalHashCode
485+
486+
implicit val outputShow: Show[(Either[Errors, Unit], List[RetryAttempt])] =
487+
Show.fromToString
488+
489+
"give up on mismatched errors" in ticked { implicit ticker =>
490+
type F[A] = EitherT[IO, Errors, A]
491+
492+
val maxRetries = 2
493+
val delay = 1.second
494+
495+
val error = new Error2
496+
val policy = Retry
497+
.constantDelay[F, Errors](delay)
498+
.withMaxRetries(maxRetries)
499+
.withErrorMatcher(Retry.ErrorMatcher[F, Errors].only[Error1])
500+
501+
val expected: List[RetryAttempt] = List(
502+
(Status(0, Duration.Zero), Decision.giveUp, error)
503+
)
504+
505+
val io: F[Unit] = Handle[F, Errors].raise[Errors, Unit](error)
506+
507+
val run =
508+
for {
509+
ref <- IO.ref(List.empty[RetryAttempt])
510+
result <- mtlRetry[F, Errors, Unit](
511+
io,
512+
policy,
513+
(s, e: Errors, d) => EitherT.liftF(ref.update(_ :+ (s, d, e)))
514+
).value
515+
attempts <- ref.get
516+
} yield (result, attempts)
517+
518+
run must completeAs((Left(error), expected))
519+
}
520+
521+
"retry only on matching errors" in ticked { implicit ticker =>
522+
type F[A] = EitherT[IO, Errors, A]
523+
524+
val maxRetries = 2
525+
val delay = 1.second
526+
527+
val error = new Error1
528+
val policy = Retry
529+
.constantDelay[F, Errors](delay)
530+
.withMaxRetries(maxRetries)
531+
.withErrorMatcher(Retry.ErrorMatcher[F, Errors].only[Error1])
532+
533+
val expected: List[RetryAttempt] = List(
534+
(Status(0, Duration.Zero), Decision.retry(delay), error),
535+
(Status(1, 1.second), Decision.retry(delay), error),
536+
(Status(2, 2.seconds), Decision.giveUp, error)
537+
)
538+
539+
val io: F[Unit] = Handle[F, Errors].raise[Errors, Unit](error)
540+
541+
val run =
542+
for {
543+
ref <- IO.ref(List.empty[RetryAttempt])
544+
result <- mtlRetry[F, Errors, Unit](
545+
io,
546+
policy,
547+
(s, e: Errors, d) => EitherT.liftF(ref.update(_ :+ (s, d, e)))
548+
).value
549+
attempts <- ref.get
550+
} yield (result, attempts)
551+
552+
run must completeAs((Left(error), expected))
553+
}
554+
555+
}
556+
456557
private def run[A](policy: Retry[IO, Throwable])(io: IO[A]): IO[List[RetryAttempt]] =
457558
for {
458559
ref <- IO.ref(List.empty[RetryAttempt])

0 commit comments

Comments
 (0)