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

The Futures are Calling and I Must Choose

The Futures are Calling and I Must Choose

Presented at Scale by the Bay 2017

Comparison of Scala Future, Twitter Future, and Java CompletableFuture, with a brief introduction to Monix Tasks too!

Chris Phelps

November 18, 2017
Tweet

More Decks by Chris Phelps

Other Decks in Programming

Transcript

  1. Copyright 2017 Tendril, Inc. All rights reserved. The Futures are

    Calling and I Must Choose SCALE BY THE BAY, SAN FRANCISCO, NOVEMBER 18, 2017
  2. Copyright 2017 Tendril, Inc. All rights reserved. Future History OVERVIEW

    • Java Future modeled async operations • Minimal ways to observe and react • Twitter and Akka • Scala SIP-14, Jan 2012 • Scala standard Future in 2.10 3
  3. Copyright 2017 Tendril, Inc. All rights reserved. Agenda OVERVIEW •

    Scala Future • Twitter Future • Java CompletableFuture • Tasks • Comparison and Recommendation 4 https://www.flickr.com/photos/3oheme/5141328136/in/photolist-8QjDJy
  4. Copyright 2017 Tendril, Inc. All rights reserved. What makes up

    a Future? SCALA FUTURES • 3 states • Unresolved • Resolved with Success and a value • Resolved with Failure and a Throwable • Immediately resolved instances • Promises • Rich set of combinators in class and companion object
  5. Copyright 2017 Tendril, Inc. All rights reserved. Obtaining a Future

    SCALA FUTURES • Call some library function • Future.apply() • Promise
  6. Copyright 2017 Tendril, Inc. All rights reserved. Creating a Future

    with apply() SCALA FUTURES 8 import scala.concurrent.ExecutionContext.Implicits.global val myFuture: Future[Int] = Future { calculateSlowly() } Creating a Future with Promise val myPromise = Promise[Int]() val myNewFuture = myPromise.future val res = calculateSlowly() myPromise.success(res)
  7. Copyright 2017 Tendril, Inc. All rights reserved. Promises SCALA FUTURES

    9 Promise Producer Consumer 1. return Future 2. register callback 3. complete() 4. callback executed
  8. Copyright 2017 Tendril, Inc. All rights reserved. Callbacks SCALA FUTURES

    10 val myFuture: Future[Int] = calculate() myFuture onComplete { case Success(res) => doSomethingWith(res) case Failure(t) => println(“An error occurred”) } // 2.11, deprecated in 2.12 myFuture foreach { case res => doSomethingWith(res) } myFuture onFailure { case t => println(“An error occurred”) } // 2.12 myFuture.foreach { res => doSomethingWith(res) } myFuture.failed.foreach{ t => println(“An error occurred”) }
  9. Copyright 2017 Tendril, Inc. All rights reserved. Future combinators SCALA

    FUTURES • map [S](f: (T) ⇒ S)(...): Future[S] • flatMap [S](f: (T) ⇒ Future[S])(...): Future[S] 11
  10. Copyright 2017 Tendril, Inc. All rights reserved. Map and FlatMap

    SCALA FUTURES 12 def calculate(): Future[Int] = ??? val myFuture = calculate() def scale(in: Int): Int = ??? val scaled = myFuture map { x => scale(x) } def recalculate(in: Int): Future[Int] = ??? val recalculated = mapped flatMap { x => recalculate(x) } val chained = for { a <- calculate() b <- recalculate(a) } yield scale(b)
  11. Copyright 2017 Tendril, Inc. All rights reserved. Future combinators SCALA

    FUTURES • map [S](f: (T) ⇒ S)(...): Future[S] • flatMap [S](f: (T) ⇒ Future[S])(...): Future[S] • foldLeft [T, R](futures: Iterable[Future[T]]) (zero: R)(op: (R, T) ⇒ R)(...): Future[R] • firstCompletedOf[T](futures:TraversableOnce[Future[T]]) (...): Future[T] • sequence[A, M[X] <: TraversableOnce[X]] (in: M[Future[A]])(...): Future[M[A]] 13
  12. Copyright 2017 Tendril, Inc. All rights reserved. Computation runs in

    an ExecutionContext SCALA FUTURES • Transform body is submitted to EC as a Callable • Global execution context – ForkJoin pool • Build from any Executor or ExecutionService • Why might you chose one over another? • Control over pool size and characteristics • ForkJoinPool is not ideal for long blocking operations • Certain work in a particular pool 14
  13. Copyright 2017 Tendril, Inc. All rights reserved. Twitter Futures have

    some differences TWITTER FUTURES • Combinators • Execution model • Cancellation
  14. Copyright 2017 Tendril, Inc. All rights reserved. Twitter supports many

    standard combinators TWITTER FUTURES • filter (p: (A) ⇒ Boolean): Future[A] • flatMap [B](f: (A) ⇒ Future[B]): Future[B] • flatten [B](implicit ev: <:<[A, Future[B]]): Future[B] • foreach (k: (A) ⇒ Unit): Future[A] • map [B](f: (A) ⇒ B): Future[B] • withFilter (p: (A) ⇒ Boolean): Future[A]
  15. Copyright 2017 Tendril, Inc. All rights reserved. Twitter adds many

    combinators TWITTER FUTURES • by (when: Time)(implicit timer: Timer): Future[A] • within (timeout: Duration)(implicit timer: Timer): Future[A] • parallel [A](n: Int)(f: ⇒ Future[A]): Seq[Future[A]] • select [A](fs: Seq[Future[A]]): Future[(Try[A], Seq[Future[A]])] • when [A](p: Boolean)(f: ⇒ Future[A]): Future[Unit] • whileDo [A](p: ⇒ Boolean)(f: ⇒ Future[A]): Future[Unit] Theme: ”Observing” or “Choreographing” other futures
  16. Copyright 2017 Tendril, Inc. All rights reserved. TWITTER FUTURES scala.util.Future

    com.twitter.util.Future • isCompleted: Boolean • value: Option[Try[T]] • recover [U >: T](pf: PartialFunction[Throwable, U]) : Future[U] • recoverWith [U >: T](pf: PartialFunction[Throwable, Future[U]]) : Future[U] • zip [U](that: Future[U]): Future[(T, U)] • zipWith [U, R](that: Future[U])(f: (T, U) ⇒ R): Future[R] • isDone (implicit ev: <:<[Future.this.type, Future[Unit]]): Boolean • poll: Option[Try[A]] • handle [B >: A](rescueException: PartialFunction[Throwable, B]): Future[B] • rescue [B >: A](rescueException: PartialFunction[Throwable, Future[B]]): Future[B] • join [B] (other: Future[B]): Future[(A, B)] • joinWith [B, C] (other: Future[B])(fn: (A, B) ⇒ C): Future[C] 19 Twitter supports many of the same combinators but with different names
  17. Copyright 2017 Tendril, Inc. All rights reserved. TWITTER FUTURES scala.util.Future

    com.twitter.util.Future • recover [U >: T](pf: PartialFunction[Throwable, U]) (implicit executor: ExecutionContext): Future[U] • recoverWith [U >: T](pf: PartialFunction[Throwable, Future[U]]) (implicit executor: ExecutionContext): Future[U] • zip [U](that: Future[U]): Future[(T, U)] • zipWith [U, R](that: Future[U])(f: (T, U) ⇒ R) (implicit executor: ExecutionContext): Future[R] • handle [B >: A](rescueException: PartialFunction[Throwable, B]): Future[B] • rescue [B >: A](rescueException: PartialFunction[Throwable, Future[B]]): Future[B] • join [B] (other: Future[B]): Future[(A, B)] • joinWith [B, C] (other: Future[B])(fn: (A, B) ⇒ C): Future[C] 20 Except.... No ExecutionContext?
  18. Copyright 2017 Tendril, Inc. All rights reserved. The Twitter execution

    model is different from Scala TWITTER FUTURES • Transform body is executed in the prior thread • Pipeline executed by same thread • Work can be forked to a new pool • Will these necessarily be in order? Depth first or something else? • If already resolved, new callback executes immediately in caller thread • Future.apply() runs body immediately in caller thread • New Future creation needs explicit FuturePool 21
  19. Copyright 2017 Tendril, Inc. All rights reserved. Creating new Twitter

    futures TWITTER FUTURES val pool = FuturePool.unboundedPool val notf = Future { // executes immediately in caller thread calculate() } val f = pool { // executes in the pool calculate() } 22
  20. Copyright 2017 Tendril, Inc. All rights reserved. Twitter Future supports

    cancellation TWITTER FUTURES • Promise raise() – marks promise as interrupted • Future is NOT resolved • Promise isInterrupted() • Running calculation is not stopped • Calculation can check isInterruped() and interrupt itself • raiseWithin() – NEW future which fails after the timeout 23
  21. Copyright 2017 Tendril, Inc. All rights reserved. Cancellation TWITTER FUTURES

    val pool = FuturePool.unboundedPool val promise = new Promise[Int]() pool { val r1 = expensiveCalculation() promise.isInterrupted match { case Some(t) => promise.setException(t) case None => promise.setValue(nextCalculation(r1)) } } promise.raise(new IllegalStateException(“I give up”)) 24
  22. Copyright 2017 Tendril, Inc. All rights reserved. Bijection TWITTER FUTURES

    import scala.concurrent.{Future => ScalaFuture} import com.twitter.util.{Future => TwitterFuture} import com.twitter.bijection.Conversion.asMethod import com.twitter.bijection.twitter_util.UtilBijections._ def doSomething: ScalaFuture[T] = { val response: TwitterFuture[T] = ??? response.as[ScalaFuture[T]] } 25
  23. Copyright 2017 Tendril, Inc. All rights reserved. Guava ListenableFuture GUAVA

    LISTENABLEFUTURE • Google Guava core library • Brought functional collections to Java • ListenableFuture provides Futures with callbacks • From ca 2011 • Before Scala Future • Before CompletableFuture
  24. Copyright 2017 Tendril, Inc. All rights reserved. ListenableFuture is unpleasant

    from Scala GUAVA LISTENABLEFUTURE • Not chainable • Requires type hint • Relies on GuavaFunction and AsyncFunction • transform() vs transformAsync() (in Guava 20.0) • ExecutionContext required (in Guava 21.0) • Consider adapters like Gilt Foundation Classes (gfc-guava) 28
  25. Copyright 2017 Tendril, Inc. All rights reserved. Gilt Foundation Classes

    GUAVA LISTENABLEFUTURE import com.gilt.gfc.guava.future.GuavaFutures implicit val es = Executors.newFixedThreadPool(3) val lf: ListenableFuture[String] = GuavaFutures.future { figureOutTheThing() } import com.gilt.gfc.guava.future.FutureConverters._ val alf: ListenableFuture[Int] = Future { findOtherThing() }.asListenableFuture import com.gilt.gfc.guava.future.GuavaFutures._ val res: ListenableFuture[Int] = lf.map(s => s.length) 29
  26. Copyright 2017 Tendril, Inc. All rights reserved. Java CompletableFuture JAVA

    COMPLETABLEFUTURE • Java8 • Future with callbacks • Acts as both Promise and Future • Creation • As Promise • In an executor via supplyAsync(Supplier) • In an executor via runAsync(Runnable)
  27. Copyright 2017 Tendril, Inc. All rights reserved. CompletableFuture example JAVA

    COMPLETABLEFUTURE val cf = new CompletableFuture[Int]() val x1: CompletableFuture[Int] = cf.thenApplyAsync( (i: Int) => scaleTransform(i)) // final stage – deal with the result x1.handle((r, t) => if (r != null) doHappyPath(r) else if (t != null) doExceptionPath(t)) // some time later cf.complete(41) 32
  28. Copyright 2017 Tendril, Inc. All rights reserved. Execution Model JAVA

    COMPLETABLEFUTURE • Different methods to do direct or async operations • apply(), applyAsync() • handle() handleAsync() • Regular version uses the same thread • Async versions use default executor or specified executor • Default executor is ForkJoinPool.commonPool() • Pool sized based on number of cores • New thread if commonPool is not configured for parallelism 33
  29. Copyright 2017 Tendril, Inc. All rights reserved. How do names

    and combinators compare? JAVA COMPLETABLEFUTURE • CompletableFuture operations provide variants: • Runnable / Supplier • Function / Consumer • No recoverWith, just recover (exceptionally) • Cannot replace a failure with an unresolved fallback • Combinators execute next step without intermediate Future • Scala zip() resolves with a tuple, map() to execute next step • foo.thenCombine(bar, (a,b) => doSomethingWith(a,b)) • Overall handler method taking nullable success and failure • handle(BiFunction<? super T,Throwable,? extends U> fn) 34
  30. Copyright 2017 Tendril, Inc. All rights reserved. Tasks can be

    thought of as lazy Futures MONIX TASKS • Scalaz, Monix, FS2 • General delayed effects see Scalaz 8 IO or Cats Effect • Explicitly run: attemptRun(), runAsync(), unsafeRunAsync() • No unwanted computations • No leaked computations • Memoization • Run every time by default • Memoize the result on demand • Trampolined • Cancelable
  31. Copyright 2017 Tendril, Inc. All rights reserved. Monix Task creation

    MONIX TASKS import monix.execution.Scheduler.Implicits.global import monix.execution.CancelableFuture import monix.eval.Task import scala.util.{Success, Failure} val task = Task { calculation() } val cancelable = task.runAsync { result => result match { case Success(value) => ??? case Failure(ex) => ??? }} val res = task.memoize 37
  32. Copyright 2017 Tendril, Inc. All rights reserved. Monix Task cancellation

    MONIX TASKS import monix.execution.Scheduler.Implicits.global import monix.execution.CancelableFuture import monix.eval.Task import scala.util.{Success, Failure} val handle = ??? val cancelable: CancelableFuture[Int] = task.runAsync { handle } cancelable.cancel() 38
  33. Copyright 2017 Tendril, Inc. All rights reserved. Execution Model -

    Trampoline MONIX TASKS • Controlled by Scheduler • BatchedExecution – Async boundary forced after a maximum size • AlwaysAsyncExecution • SynchronousExecution • Units of work are executed in a loop on a single thread • Work is forked to a new thread upon failure or async boundaries • Explicit fork to new Scheduler via Task.fork(s) • Task.fork(fa: Task[A]): Task[A] • mytask.asyncBoundary: Task[A] 39
  34. Copyright 2017 Tendril, Inc. All rights reserved. Monix Task async

    boundaries MONIX TASKS import monix.execution.Scheduler lazy val io = Scheduler.io(name="my-io") val source = Task(println(s"${Thread.currentThread.getName}")) val forked = Task.fork(source, io) val task = { source // executes on global .flatMap(_ => forked) // executes on io .asyncBoundary // switch back to global .doOnFinish(_ => onFinish) // executes on global .runAsync } 40
  35. Copyright 2017 Tendril, Inc. All rights reserved. Scala Twitter Guava

    Java Promise-like creation Promise Promise SettableFuture CompletableFuture Async creation Future.apply() myPool.apply() ListeningDecorator execSvc.submit() supplyAsync() Execution model Run in ExecutionContext Completing thread runs Completing thread or executor (method overload) Completing thread or executor (different method) Supports for- comprehension Yes Yes No (yes with gfc-guava) No (possible with conversions) Cancellation No Yes (cooperative) No Yes (no interrupt despite argument) Java/Scala interoperation Implement e.g. PartialFunction from Java Variants provided for interop Java fair, Scala bad Better with gfc-guava Java good, type inference issues Future comparison CONCLUSIONS 42
  36. Copyright 2017 Tendril, Inc. All rights reserved. What do we

    recommend? CONCLUSIONS • Use whatever your libraries use • If you aren’t introducing Twitter, stick with Scala unless you need cancellation or monitoring combinators • If you need to mix Scala and Twitter, use Bijection • If you need to interop with Java, prefer CompletableFuture • At this point, avoid Guava ListenableFuture • Watch this space – lots of progress with Monix, Scalaz8 IO, and Cats Effect 43
  37. Copyright 2017 Tendril, Inc. All rights reserved. Other abstractions to

    consider CONCLUSIONS • ReactiveX • Observable streams • Multiple languages supported • Streams • Scalaz, Fs2, Akka • Actors • Akka • Coroutines 44
  38. Copyright 2017 Tendril, Inc. All rights reserved. Chris Phelps @CJPhelps

    https://github.com/chrisphelps/futures-are-calling 45 Photo used under a Creative Commons license (Zach Dischner)