1717package cats .effect .std
1818
1919import 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
2123import cats .syntax .applicative ._
24+ import cats .syntax .flatMap ._
2225import cats .syntax .functor ._
2326import 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