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. 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 ≠
  2. 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 ≠
  3. 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 =
  4. 8.
  5. 10.
  6. 13.

    A story of interruption Sit down to work Remember what

    the task is, open project in editor
  7. 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
  8. 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
  9. 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
  10. 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
  11. 20.

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

    val program: IO[Int] We need to cancel an effect
  12. 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
  13. 26.

    Fiber model trait IO[A] { def start: IO[Fiber[A]] } trait

    Fiber[A] { } val program: IO[Int] = IO(...) program.start
  14. 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 }
  15. 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 }
  16. 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
  17. 31.

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

    We'll learn higher-level tools later!
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 41.

    Reacting to interruption - bracketCase val prepareToWork: IO[WorkContext] val developer

    = prepareToWork.bracketCase { (ctx: WorkContext) => performWork(ctx) } { case (ctx, ) case (ctx, ) case (ctx, ) }
  28. 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
  29. 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
  30. 47.

    By default: assume interruptibility between actions At every async boundary

    IO.sleep(1.seconds) *> another Every N flatMaps program.foreverM
  31. 55.

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

    on finalizers Interruptible blocking, futures* * some futures
  32. 56.

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

    on finalizers Interruptible blocking, futures* Supervision * some futures
  33. 57.

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

    on finalizers Interruptible blocking, futures* Supervision Coming in Cats Effect 3.0
  34. 58.

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

    on finalizers Interruptible blocking, futures* Supervision Under active discussion in CE
  35. 59.
  36. 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)
  37. 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
  38. 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) }
  39. 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 }
  40. 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
  41. 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
  42. 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
  43. 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] }
  44. 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] }
  45. 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] }
  46. 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] }
  47. 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] }