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

Introduction to interruption

Introduction to interruption

Modern functional effect systems give us great power, but with that power comes interruptibility.
It's important to know if and when your effects can be interrupted, and how to handle that interruption well.

In this talk, I'll provide a concise introduction to the way interruption works in functional IO, as well as some best practices for making our code interruption-safe.

Jakub Kozłowski

December 12, 2019
Tweet

More Decks by Jakub Kozłowski

Other Decks in Technology

Transcript

  1. Quick recap: functional effects Side effect: breaking referential transparency def

    sayHello() = { println("hello") 42 } val x = sayHello() + sayHello() val result = sayHello() val x = result + result ≠
  2. Quick recap: functional effects Side effect: breaking referential transparency def

    sayHello() = { println("hello") 42 } val x = sayHello() + sayHello() val result = sayHello() val x = result + result ≠
  3. Quick recap: functional effects Effects can be purely functional! val

    sayHello = IO { println("hello") }.map(_ => 42) val x = for { a <- sayHello b <- sayHello } yield a + b val result = sayHello val x = for { a <- result b <- result } yield a + b =
  4. A story of interruption Sit down to work Remember what

    the task is, open project in editor
  5. A story of interruption Sit down to work Remember what

    the task is, open project in editor Gather context on the task in head
  6. A story of interruption Sit down to work Remember what

    the task is, open project in editor Gather context on the task in head Work for 5 minutes
  7. A story of interruption Sit down to work Remember what

    the task is, open project in editor Gather context on the task in head Work for 5 minutes hey do you have a sec
  8. A story of interruption Sit down to work Remember what

    the task is, open project in editor Gather context on the task in head Work for 5 minutes hey do you have a sec Clear your head to think about the colleague's problem
  9. Codifying the interruption story How do we even cancel this...

    val program: IO[Int] We need to cancel an effect
  10. Codifying the interruption story How do we even cancel this...

    val program: IO[Int] ...if it's just a description? We need to cancel an effect
  11. Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { } val program: IO[Int] = IO(...) program.start
  12. Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { def join: IO[A] } val program: IO[Int] = IO(...) program.start.flatMap { fiber => fiber.join }
  13. Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { def join: IO[A] def cancel: IO[Unit] } val program: IO[Int] = IO(...) program.start.flatMap { fiber => fiber.cancel }
  14. Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { def join: IO[A] def cancel: IO[Unit] } val program: IO[Int] = IO(...) program.start.flatMap { fiber => fiber.cancel } Fabio Labella - How do fibers work? https://youtube.com/watch?v=x5_MmZVLiSM
  15. Fibers are very low level! It's easy to mess up.

    We'll learn higher-level tools later!
  16. Codifying the interruption story Sit down to work Remember what

    the task is, open project in editor Gather context on the task in head Work for 5 minutes hey do you have a sec Clean your head, think about the colleague's problem
  17. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work for 5 minutes hey do you have a sec Clean your head, think about the colleague's problem val developer = prepareToWork Codifying the interruption story
  18. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec Clean your head, think about the colleague's problem val developer = prepareToWork.bracket( use = performWork ) for { workerProcess <- developer.start } Codifying the interruption story
  19. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec Clean your head, think about the colleague's problem val developer = prepareToWork.bracket( use = performWork ) for { workerProcess <- developer.start _ <- IO.sleep(5.minutes) } Codifying the interruption story
  20. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec Clear your head, think about the colleague's problem val developer = prepareToWork.bracket( use = performWork ) for { workerProcess <- developer.start _ <- IO.sleep(5.minutes) _ <- workerProcess.cancel } yield () Codifying the interruption story
  21. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec val developer = prepareToWork.bracket( use = performWork )(release = stopThinkingAboutTask) for { workerProcess <- developer.start _ <- IO.sleep(5.minutes) _ <- workerProcess.cancel } yield () Codifying the interruption story Concurrently with developer
  22. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec Clear your head to think about the colleague's problem val developer = prepareToWork.bracket( use = performWork )(release = stopThinkingAboutTask) for { workerProcess <- developer.start _ <- IO.sleep(5.minutes) _ <- workerProcess.cancel } yield () Codifying the interruption story
  23. Sit down to work Remember what the task is, open

    project in editor Gather context on the task in head Work... for 5 minutes hey do you have a sec Clear your head to think about the colleague's problem val developer = prepareToWork.bracket( use = performWork )(release = stopThinkingAboutTask) for { workerProcess <- developer.start _ <- IO.sleep(5.minutes) _ <- workerProcess.cancel } yield () Codifying the interruption story interruption, finalizer
  24. Reacting to interruption - bracket val prepareToWork: IO[WorkContext] val developer

    = prepareToWork.bracket { use = (ctx: WorkContext) => performWork(ctx) } { release = ctx => stopThinkingAboutTask(ctx) } try-with-resources for async, functional effects
  25. Reacting to interruption - bracketCase val prepareToWork: IO[WorkContext] val developer

    = prepareToWork.bracketCase { (ctx: WorkContext) => performWork(ctx) } { case (ctx, ) case (ctx, ) case (ctx, ) }
  26. Reacting to interruption - bracketCase val prepareToWork: IO[WorkContext] val developer

    = prepareToWork.bracketCase { (ctx: WorkContext) => performWork(ctx) } { case (ctx, ExitCase.Completed) => finishWork case (ctx, ExitCase.Error(e)) => revertWork(e) case (ctx, ExitCase.Canceled) => stopThinkingAboutTask(ctx) } See also - guarantee, onCancel
  27. Building cancelable tasks def asyncProviderToIO(provider: AsyncProvider): IO[Int] = IO.cancelable {

    cb => val cancel = provider.getOne( onSuccess = success => cb(Right(success)), onFailure = failure => cb(Left(failure)) ) IO(cancel()) } Convert callback-based interface to IO
  28. By default: assume interruptibility between actions At every async boundary

    IO.sleep(1.seconds) *> another Every N flatMaps program.foreverM
  29. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

    on finalizers Interruptible blocking, futures* * some futures
  30. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

    on finalizers Interruptible blocking, futures* Supervision * some futures
  31. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

    on finalizers Interruptible blocking, futures* Supervision Coming in Cats Effect 3.0
  32. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

    on finalizers Interruptible blocking, futures* Supervision Under active discussion in CE
  33. Tip 1 Avoid start / fork // Don't do this!

    def par2[A, B](ioa: IO[A], iob: IO[B]): IO[(A, B)] = for { fa <- ioa.start fb <- iob.start a <- fa.join b <- fb.join } yield (a, b)
  34. Tip 2 Use built-in parallel operators // Much safer! def

    par2[A, B](ioa: IO[A], iob: IO[B]): IO[(A, B)] = (ioa, iob).parTupled Operators from cats.Parallel type class instance for cats.effect.IO
  35. Tip 3 Use race, Deferred (Promise) def runUntilKeyPress[A](program: IO[A]): IO[Option[A]]

    = { val keyPress = IO(StdIn.readLine()) (keyPress race program).map(_.toOption) }
  36. Tip 3 Use race, Deferred (Promise) def firstCompletedButAwaitAll[A](as: List[IO[A]]): IO[A]

    = Deferred[IO, A].flatMap { promise => as.parTraverse { _.flatMap { promise.complete(_).attempt } } *> promise.get }
  37. Tip 3 Use race, Deferred (Promise) // - Run both

    in parallel // - Cancel right if left completes first // - Wait for left // - Cancel both if result is canceled def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { Deferred[IO, Unit].flatMap { leftCompleted => (left <* leftCompleted.complete(())) <& (right race leftCompleted.get) } } From https://www.youtube.com/watch?v=fZO2lV2xjEo
  38. Tip 4 Use background (in next Cats Effect release) def

    parBothUsingBackground[A, B]( longProcess: IO[A], anotherProcess: IO[B] ): IO[(A, B)] = longProcess.background.use { await: IO[A] => (anotherProcess, await).tupled } Fork a process as a fiber + ensure cancelation when use completes
  39. Tip 5 Use fs2.Stream, fs2.Queue, Ref + Deferred for really

    complex problems trait Background[F[_]] { def managedStart[A](fa: F[A]): F[Unit] } object Background { def bounded[F[_]: Concurrent: Timer]( maxSize: Int ): Resource[F, Background[F]] = Resource .liftF(fs2.concurrent.Queue.bounded[F, F[Unit]](maxSize)) .flatMap { q => val process = q.dequeue .mapAsync(maxSize)(_.attempt) .compile .drain val bg = new Background[F] { def managedStart[A](fa: F[A]): F[Unit] = q.enqueue1(fa.void) } process.background.as(bg) } } https://vimeo.com/366191463 https://youtu.be/oluPEFlXumw
  40. Design tips Small, high-level, compositional abstractions trait Background[F[_]] { def

    managedStart[A](fa: F[A]): F[Unit] } trait SimpleCache[K, V] { def clear(k: K): IO[Unit] def getOrFetch(k: K): IO[V] } trait JobScheduler[F[_]] { def schedule(at: Instant, job: F[Unit]): F[Unit] } trait RateLimiter[F[_]] { def limited[A](action: F[A]): F[A] }
  41. Design tips Small, high-level, compositional abstractions Concurrency details away from

    domain trait Background[F[_]] { def managedStart[A](fa: F[A]): F[Unit] } trait SimpleCache[K, V] { def clear(k: K): IO[Unit] def getOrFetch(k: K): IO[V] } trait JobScheduler[F[_]] { def schedule(at: Instant, job: F[Unit]): F[Unit] } trait RateLimiter[F[_]] { def limited[A](action: F[A]): F[A] }
  42. Design tips Small, high-level, compositional abstractions Concurrency details away from

    domain Trust the laws, nothing else trait Background[F[_]] { def managedStart[A](fa: F[A]): F[Unit] } trait SimpleCache[K, V] { def clear(k: K): IO[Unit] def getOrFetch(k: K): IO[V] } trait JobScheduler[F[_]] { def schedule(at: Instant, job: F[Unit]): F[Unit] } trait RateLimiter[F[_]] { def limited[A](action: F[A]): F[A] }
  43. Design tips Small, high-level, compositional abstractions Concurrency details away from

    domain Trust the laws, nothing else Embrace differences between effects in concrete code trait Background[F[_]] { def managedStart[A](fa: F[A]): F[Unit] } trait SimpleCache[K, V] { def clear(k: K): IO[Unit] def getOrFetch(k: K): IO[V] } trait JobScheduler[F[_]] { def schedule(at: Instant, job: F[Unit]): F[Unit] } trait RateLimiter[F[_]] { def limited[A](action: F[A]): F[A] }
  44. Design tips Small, high-level, compositional abstractions Concurrency details away from

    domain Trust the laws, nothing else Embrace differences between effects in concrete code Test the hell out of edge cases trait Background[F[_]] { def managedStart[A](fa: F[A]): F[Unit] } trait SimpleCache[K, V] { def clear(k: K): IO[Unit] def getOrFetch(k: K): IO[V] } trait JobScheduler[F[_]] { def schedule(at: Instant, job: F[Unit]): F[Unit] } trait RateLimiter[F[_]] { def limited[A](action: F[A]): F[A] }