Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Error&Retry Handling

fuzyco
February 12, 2020

Functional Error&Retry Handling

This presentation includes these following three things:
1. Error Handling with MonadError
2. Retry Handling with cats-retry

Sample Code: https://github.com/Hiroki6/SampleFunctionalErrorAndRetryHandling

fuzyco

February 12, 2020
Tweet

More Decks by fuzyco

Other Decks in Technology

Transcript

  1. About Me  • Hiroki Fujino • Head of Engineering

    in Unipos • My interests: Scala, Architecture Design, Concurrency Programming, etc.
  2. •Error handling •Retry handling Agenda  Functional Prerequisites: Experience with

    using Monad type class Sample Code: https://github.com/Hiroki6/SampleFunctionalErrorAndRetryHandling
  3. Error Handling with Monad  There are some Monads which

    can be used for error handling Task IO Future Either Try
  4.  Error Handling with Monad •IO •Either def fetchEither(url: String):

    Either[Throwable, Value] = { … if(success) Right(result) else Left(new Exception("fetch error")) } def fetchIO(url: URL): IO[Value] = { … if (success) IO(result) else IO.raiseError(new Exception("fetch error")) }
  5. Generic Method with Error Handling  def fetchIO(url: URL): IO[Value]

    def fetchTry(url: String): Try[Value] def fetchEither(url: String): Either[Throwable, Value] def fetch[F[_]](url: String): F[Value]
  6. Can we use Monad Type Class in this case? 

    def fetchEither(url: String): Either[Throwable, Value] = { … if(success) Right(result) else Left(new Exception("fetch error")) } def fetch[F[_]](url: String)(implicit M: Monad[F]): F[Value] = { … if(success) M.pure(result) else ??? }
  7. Tagless Final with Error Handling  import cats.instances.either._ implicit val

    eitherRepository = EitherRepository val eitherUseCase = new UseCase[EitherThrowable] eitherUseCase.update(in) Exception in thread "main" … PreconditionException: Not Found class UseCase[F[_]](implicit M: Monad[F], repository: Repository[F]) { def update(in: Input): F[Output] = { M.flatMap(repository.get(in)) { case Some(value) => repository.update(value) case None => throw PreconditionException("Not Found") } } }
  8. MonadError type class  trait MonadError[F[_], E] extends ApplicativeError[F, E]

    with Monad[F] { … } Type of Monad Type of Error Inherit Monad TypeClass Implemented in cats and the other functional libraries. Abstracts over error-handling monads.
  9. Major Methods in MonadError  • raiseError /** * Lift

    an error into the `F` context. * … */ def raiseError[A](e: E): F[A] def raiseError[B](e: A): Either[A, B] = Left(e) Ex. Either Instance
  10. Major Methods in MonadError  • handleErrorWith /** * Handle

    any error, potentially recovering from it, by mapping it to an * `F[A]` value. * .. */ def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] Similar to `recoverWith` method in Future and Try
  11. The other methods in MonadError  • attempt • recover

    • onError • fromEither • ensure • … There are several useful methods for error handling.
  12. MonadError Instances in cats  There are some instances of

    MonadError type class. Task IO Future Either Try Option
  13. Error Handling with MonadError  def fetch[F[_]](url: String)(implicit ME: MonadError[F,

    Throwable]): F[Value] = { … if(success) ME.pure(result) else ME.raiseError(new Exception("fetch error")) } def fetchEither(url: String): Either[Throwable, Value] = { … if(success) Right(result) else Left(new Exception("fetch error")) }
  14. Execution with MonadError  •IO •Either // `IO` implementing the

    algebra of `MonadError` fetch[IO](url) import cats.instances.either._ type EitherThrowable[A] = Either[Throwable, A] fetch[EitherThrowable](url)
  15.  Tagless Final with MonadError class UseCase[F[_]](implicit M: Monad[F], repository:

    Repository[F]) { def update(in: Input): F[Output] = { M.flatMap(repository.get(in)) { case Some(value) => repository.update(value) case None => throw PreconditionException("Not Found") } } } class UseCase[F[_]](implicit ME: MonadError[F, Throwable], repository: Repository[F]) { def update(in: Input): F[Output] = { ME.flatMap(repository.get(in)) { case Some(value) => repository.update(value) case None => ME.raiseError(PreconditionException("Not Found")) } } }
  16.  class UseCase[F[_]](implicit ME: MonadError[F, Throwable], repository: Repository[F]) { def

    update(in: Input): F[Output] = { ME.flatMap(repository.get(in)) { case Some(value) => repository.update(value) case None => ME.raiseError(PreconditionException("Not Found")) } } } import cats.instances.either._ implicit val eitherRepository = EitherRepository val eitherUseCase = new UseCase[EitherThrowable] eitherUseCase.update(in) Tagless Final with MonadError
  17. Functional Error Handling  MonadError provides error handling without concrete

    Monad • Abstract error handling • Many useful methods
  18.  Retry Handling Error has occurred on program, retry[counter=1] after

    1000 [ms] sleeping..., total delay was 0 [ms] so far Error has occurred on program, retry[counter=2] after 2000 [ms] sleeping..., total delay was 1000 [ms] so far Success! Retry Retry × × ✔
  19.  • How many times to retry • How long

    to wait between attempts • Logging when handle the error You have to consider the following: Retry handling tends to be complicated Let’s take a look at a basic retry handling…
  20. cats-retry  Functional retry handling with cats-retry in Scala This

    talk is based on the article below: Scala library for retrying actions that can failed. https://github.com/cb372/cats-retry
  21. The concept of cats-retry  Action Retry Policy Error Handling

    • How many times to retry • How long to wait between attempts • Logging when handle the error Action with Retry Mechanism Composition of retry policy and action
  22. The concept of cats-retry  Composition of retry policy and

    action class RetryingOnAllErrorsPartiallyApplied[A] { def apply[M[_], E]( policy: RetryPolicy[M], onError: (E, RetryDetails) => M[Unit] )( action: => M[A] )( implicit ME: MonadError[M, E], S: Sleep[M] ): M[A] Retry Policy Error Handling Action
  23. Retry Policy  import retry.RetryPolicies import cats.syntax.semigroup._ val policy =

    RetryPolicies.limitRetries[IO](3) |+| RetryPolicies.exponentialBackoff[IO](1.seconds) • ConstantDelay • FibonacciBackoff • FullJitter Delay Algorithm How many times to retry
  24. Composing Policies  Retry with a 100ms delay 5 times

    and then retry every minute import retry.RetryPolicies._ val retry5times100millis = limitRetries[IO](5) |+| constantDelay[IO](100.millis) val retry1mins = constantDelay[IO](1.minute) retry5times100millis.followedBy(retry1mins)
  25. Error Handling  def logError(action: String)(err: Throwable, details: RetryDetails): IO[Unit]

    = details match { case WillDelayAndRetry(nextDelay: FiniteDuration, retriesSoFar: Int, cumulativeDelay: FiniteDuration) => IO { logger.info( s”Error has occurred on $action, retry[counter=$retriesSoFar] after ${nextDelay.toMillis} [ms] sleeping...”) } case GivingUp(totalRetries: Int, totalDelay: FiniteDuration) => IO { logger.info( s”Giving up on $action after $totalRetries retries, finally total delay was ${totalDelay.toMillis} [ms]”) } } Logging or notification between attempts
  26.  def program(input: Input): IO[Output] = { retryingOnAllErrors[Output]( policy =

    policy, onError = logError("program") )(execute(input)) } Let’s take a look at a entire code… Retry Handling with cats-retry Action Retry Policy Error Handling
  27. RetryingOnSomeErrors  Retry on specific errors RetryingOnSomeErrorsPartiallyApplied[A] { def apply[M[_],

    E]( policy: RetryPolicy[M], isWorthRetrying: E => Boolean, onError: (E, RetryDetails) => M[Unit] ) (action: => M[A]) ( implicit ME: MonadError[M, E], S: Sleep[M] ) … def isWorthRetrying(err: Throwable): Boolean = { err.getMessage.contains("timeout") }
  28. Define your own retry policy  Use RetryPolicy.lift val onlyRetryOnTuesdays

    = RetryPolicy.lift[IO] { _ => if (LocalDate.now().getDayOfWeek() == DayOfWeek.TUESDAY) { PolicyDecision.DelayAndRetry(delay = 100.milliseconds) } else { PolicyDecision.GiveUp } }
  29. Functional Retry Handling  Cats-retry provide useful retry mechanism •

    Isolation between retry policy and action • Some useful delay algorithms • Composable retry policy • Can use Monad which has the instance of MonadError
  30. References  • Practical FP in Scala: A hands-on approach

    • Refactoring with Monads • Error handling: Monad Error for the rest of us