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.

08f642741fba006656cb86fb61c160b3?s=128

Jakub Kozłowski

December 12, 2019
Tweet

Transcript

  1. Introduction to interruption in functional Scala Jakub Kozłowski

  2. I actually started in London... 2015

  3. I actually started in London... 2015 2019

  4. Agenda Everyday story of interruption Causing and reacting to interruption

    Best practices
  5. 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 ≠
  6. 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 ≠
  7. 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 =
  8. None
  9. Examples use slightly simplified cats-effect model, naming, operators https://typelevel.org/cats-effect

  10. A story

  11. A story of interruption

  12. A story of interruption Sit down to work

  13. A story of interruption Sit down to work Remember what

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

    the task is, open project in editor Gather context on the task in head
  15. 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
  16. 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
  17. 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
  18. Codifying the interruption story We need to cancel an effect

  19. Codifying the interruption story How do we even cancel this...

    We need to cancel an effect
  20. Codifying the interruption story How do we even cancel this...

    val program: IO[Int] We need to cancel an effect
  21. 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
  22. We need a running computation

  23. scala.concurrent.Future ? We need a running computation

  24. scala.concurrent.Future ? We need a running computation no

  25. Fiber model val program: IO[Int] = IO.sleep(1.second) *> putStrLn("hello world")

    *> 42.pure[IO] trait IO[A] { }
  26. Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { } val program: IO[Int] = IO(...) program.start
  27. 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 }
  28. 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 }
  29. 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
  30. Fibers are very low level! It's easy to mess up.

  31. Fibers are very low level! It's easy to mess up.

    We'll learn higher-level tools later!
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. Reacting to interruption - bracketCase val prepareToWork: IO[WorkContext] val developer

    = prepareToWork.bracketCase { (ctx: WorkContext) => performWork(ctx) } { case (ctx, ) case (ctx, ) case (ctx, ) }
  42. 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
  43. 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
  44. Finalizers can never be interrupted!

  45. So what can be interrupted?

  46. So what can be interrupted? A: pretty much anything these

    days!
  47. By default: assume interruptibility between actions At every async boundary

    IO.sleep(1.seconds) *> another Every N flatMaps program.foreverM
  48. Cancelation safe spots acquire, release sections of bracket Sections marked

    uncancelable as of Dec 2019
  49. Implementation differences

  50. Implementation differences ...keep changing, just keep track of your favorite

    library's behavior
  51. Main differences

  52. Main differences Cancelable/uncancelable regions

  53. Main differences Cancelable/uncancelable regions join on canceled fiber

  54. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

    on finalizers
  55. Main differences Cancelable/uncancelable regions join on canceled fiber Semantic blocking

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

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

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

    on finalizers Interruptible blocking, futures* Supervision Under active discussion in CE
  59. Tips

  60. Tip 0 Avoid concurrency like the plague it is https://github.com/alexandru/scala-best-practices/blob/master/sections/4-concurrency-parallelism.md

  61. 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)
  62. 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
  63. 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) }
  64. 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 }
  65. 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
  66. 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
  67. 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
  68. 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] }
  69. 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] }
  70. 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] }
  71. 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] }
  72. 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] }
  73. Thank you blog.kubukoz.com @kubukoz Slides: https://speakerdeck.com/kubukoz

  74. Thank you blog.kubukoz.com @kubukoz Slides: https://speakerdeck.com/kubukoz Find me on YouTube!

    (https://youtube.com/channel/UCBSRCuGz9laxVv0rAnn2O9Q)