Functional Error&Retry Handling

Ca4df28501e4c9cfbceb91f367afa784?s=47 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

Ca4df28501e4c9cfbceb91f367afa784?s=128

fuzyco

February 12, 2020
Tweet

Transcript

  1. Functional Error&Retry Handling Hiroki Fujino Unipos GmbH 

  2. About Me  • Hiroki Fujino • Head of Engineering

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

    using Monad type class Sample Code: https://github.com/Hiroki6/SampleFunctionalErrorAndRetryHandling
  4. Error Handling  Output log, return error message for the

    user, etc.
  5. Error Handling with Monad  There are some Monads which

    can be used for error handling Task IO Future Either Try
  6.  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")) }
  7.  def fetch[F[_]](url: String): F[Value] Can we abstract over error

    handling?
  8. • Generic Method • Tagless Final Pattern  UseCase of

    Abstract Error Handling
  9. 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]
  10. 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 ??? }
  11. 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") } } }
  12.  Monad type class doesn’t have error handling functions

  13. 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.
  14. 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
  15. 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
  16. The other methods in MonadError  • attempt • recover

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

    MonadError type class. Task IO Future Either Try Option
  18. 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")) }
  19. 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)
  20.  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")) } } }
  21.  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
  22. Functional Error Handling  MonadError provides error handling without concrete

    Monad • Abstract error handling • Many useful methods
  23.  If the error is temporary, the action might be

    successful with retry.
  24.  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 × × ✔
  25.  • 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…
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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)
  31. 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
  32.  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
  33. 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") }
  34. 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 } }
  35. 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
  36.  Thank you for listening!

  37. References  • Practical FP in Scala: A hands-on approach

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