Slide 1

Slide 1 text

Introduction to interruption in functional Scala Jakub Kozłowski

Slide 2

Slide 2 text

I actually started in London... 2015

Slide 3

Slide 3 text

I actually started in London... 2015 2019

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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 ≠

Slide 6

Slide 6 text

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 ≠

Slide 7

Slide 7 text

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 =

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

A story

Slide 11

Slide 11 text

A story of interruption

Slide 12

Slide 12 text

A story of interruption Sit down to work

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Codifying the interruption story We need to cancel an effect

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

We need a running computation

Slide 23

Slide 23 text

scala.concurrent.Future ? We need a running computation

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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 }

Slide 28

Slide 28 text

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 }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Finalizers can never be interrupted!

Slide 45

Slide 45 text

So what can be interrupted?

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Implementation differences

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Main differences

Slide 52

Slide 52 text

Main differences Cancelable/uncancelable regions

Slide 53

Slide 53 text

Main differences Cancelable/uncancelable regions join on canceled fiber

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Tips

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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)

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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 }

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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