Slide 1

Slide 1 text

Functional Error&Retry Handling Hiroki Fujino Unipos GmbH 

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

• Generic Method • Tagless Final Pattern  UseCase of Abstract Error Handling

Slide 9

Slide 9 text

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]

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

 Monad type class doesn’t have error handling functions

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

 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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

 • 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…

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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)

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

 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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

 Thank you for listening!

Slide 37

Slide 37 text

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