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. Functional
    Error&Retry Handling
    Hiroki Fujino
    Unipos GmbH

    View Slide

  2. About Me
    • Hiroki Fujino
    • Head of Engineering in Unipos
    • My interests: Scala, Architecture Design, Concurrency
    Programming, etc.

    View Slide

  3. •Error handling
    •Retry handling
    Agenda
    Functional
    Prerequisites:
    Experience with using Monad type class
    Sample Code:
    https://github.com/Hiroki6/SampleFunctionalErrorAndRetryHandling

    View Slide

  4. Error Handling
    Output log, return error message for the user, etc.

    View Slide

  5. Error Handling with Monad
    There are some Monads which can be used for error handling
    Task
    IO
    Future
    Either Try

    View Slide


  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"))
    }

    View Slide


  7. def fetch[F[_]](url: String): F[Value]
    Can we abstract over error handling?

    View Slide

  8. • Generic Method
    • Tagless Final Pattern

    UseCase of Abstract Error Handling

    View Slide

  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]

    View Slide

  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 ???
    }

    View Slide

  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")
    }
    }
    }

    View Slide


  12. Monad type class doesn’t have
    error handling functions

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  16. The other methods in MonadError
    • attempt
    • recover
    • onError
    • fromEither
    • ensure
    • …
    There are several useful methods for error handling.

    View Slide

  17. MonadError Instances in cats
    There are some instances of MonadError type class.
    Task
    IO
    Future
    Either Try
    Option

    View Slide

  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"))
    }

    View Slide

  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)

    View Slide


  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"))
    }
    }
    }

    View Slide


  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

    View Slide

  22. Functional Error Handling
    MonadError provides error handling without concrete Monad
    • Abstract error handling
    • Many useful methods

    View Slide


  23. If the error is temporary,
    the action might be successful with retry.

    View Slide


  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
    × × ✔

    View Slide


  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…

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide


  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

    View Slide

  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")
    }

    View Slide

  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
    }
    }

    View Slide

  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

    View Slide


  36. Thank you for listening!

    View Slide

  37. References
    • Practical FP in Scala: A hands-on approach
    • Refactoring with Monads
    • Error handling: Monad Error for the rest of us

    View Slide