$30 off During Our Annual Pro Sale. View Details »

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. Introduction to interruption
    in functional Scala
    Jakub Kozłowski

    View Slide

  2. I actually started in London...
    2015

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    =

    View Slide

  8. View Slide

  9. Examples use slightly simplified cats-effect model, naming, operators
    https://typelevel.org/cats-effect

    View Slide

  10. A story

    View Slide

  11. A story of interruption

    View Slide

  12. A story of interruption
    Sit down to work

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  18. Codifying the interruption story
    We need to cancel an effect

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  22. We need a running computation

    View Slide

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

    View Slide

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

    View Slide

  25. Fiber model
    val program: IO[Int] =
    IO.sleep(1.second) *>
    putStrLn("hello world") *>
    42.pure[IO]
    trait IO[A] {
    }

    View Slide

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

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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

    View Slide

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

    View Slide

  31. Fibers are very low level!
    It's easy to mess up.
    We'll learn higher-level tools later!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  44. Finalizers can never be interrupted!

    View Slide

  45. So what can be interrupted?

    View Slide

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

    View Slide

  47. By default: assume interruptibility between actions
    At every async boundary IO.sleep(1.seconds) *> another
    Every N flatMaps program.foreverM

    View Slide

  48. Cancelation safe spots
    acquire, release sections of bracket
    Sections marked uncancelable
    as of Dec 2019

    View Slide

  49. Implementation differences

    View Slide

  50. Implementation differences
    ...keep changing, just keep track of your favorite library's behavior

    View Slide

  51. Main differences

    View Slide

  52. Main differences
    Cancelable/uncancelable regions

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  58. Main differences
    Cancelable/uncancelable regions
    join on canceled fiber
    Semantic blocking on finalizers
    Interruptible blocking, futures*
    Supervision
    Under active discussion in CE

    View Slide

  59. Tips

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

  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
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. Thank you
    blog.kubukoz.com
    @kubukoz
    Slides: https://speakerdeck.com/kubukoz

    View Slide

  74. Thank you
    blog.kubukoz.com
    @kubukoz
    Slides: https://speakerdeck.com/kubukoz
    Find me on YouTube! (https://youtube.com/channel/UCBSRCuGz9laxVv0rAnn2O9Q)

    View Slide