Андрей Ершов — Future в Java & Scala

Андрей Ершов — Future в Java & Scala

С ростом количества запросов к сервисам, набирает популярность асинхронный стиль написания приложений. В связи с этим, возникла идея подробно посмотреть на различные варианты реализации Future в языках Java и Scala и сравнить их между собой.

Начнем мы с Java и посмотрим на эволюцию Future в этом языке:

- В Java 5 появился класс Future, но он предоставлял только блокирующий API.
- Сторонние библиотеки, такие как Guava, предоставляли свои абстракции Future, которые однако тоже обладали недостатками.
- Текущее состояние мира Java — класс CompletableFuture, который действительно позволяет писать код в асинхронном стиле. Есть ли у него какие-то недостатки?

В ходе доклада, мы сравним CompletableFuture с Future в Scala и посмотрим, кто одержит победу.

После сравнения, мы поговорим об обработке ошибок в асинхронных вычислениях, в том числе рассмотрим альтернативный исключениям подход к обработке ошибок — монады и монад-трансформеры, который предлагает Scala, являясь функциональным языком.

Если останется время, мы так же поговорим о классе Task в scalaz и monix.

3fc5b5eb32bd3b48d7810fd67b37f9a1?s=128

Moscow JUG

August 31, 2017
Tweet

Transcript

  1. Future Evolution Java and Scala Extended version Andrey Ershov @andrershov

    andrershov@gmail.com
  2. About me • Working at Dino Systems • 8+ year

    of Java • Desktop, Android, Enterprise, Distributed Systems • Lead in video-conferencing project • Enjoy reading distributed systems papers a lot 2
  3. Agenda • Future, ListenableFuture, CompletableFuture in Java • Future в

    Scala • Comparison • Logging in async applications • Exception handling in async applications 3
  4. What is the Future? “A Future represents the result of

    an asynchronous computation” / JavaDoc “A future represents a value, detached from time” / Victor Klang 4
  5. Java 5 Future 5

  6. Java 5 Future is blocking The result can only be

    retrieved using method get when the computation has completed, blocking if necessary until it is ready. / JavaDoc 6
  7. But it can be useful Future<Result> fetchUrl(String url) { return

    executor.submit(()->http.get(url)); } List<Result> fetchResults (List<String> urls){ return urls.stream() .map(url -> WS.fetchUrl(url)) .map(future -> future.get()) .collect(Collectors.toList()); } 7
  8. Analysis 1) Each call to web-service consumes a thread. Not

    compatible with non-blocking IO. 2) Each call to Future.get() is blocking, but it’s ok when all results are needed 8
  9. What if we need the fastest reply only? pool =

    Executors.newFixedThreadPool(5); completionService = new ExecutorCompletionService<>(pool); void fetchUrl(String url) { completionService.submit(()->http.get(url)); } Result firstResult (List<String> urls){ urls.forEach(url -> WS.fetchUrl(url)); return completionService.take().get(); } 9
  10. ExecutorCompletionService 10

  11. FutureTask 11 Future submit(Runnable r) { t = newTaskFor(r); this.execute(t);

    return t; }
  12. QueuingFuture 12

  13. Async Servlets (server-side IO) void doGet(HttpRequest request) { AsyncContext ctx

    = request.startAsync(); Future<Result> res = service.doSmth(); ctx.complete(res.get()); } Servlet-container thread is still blocked! 13 HttpResponse doGet(HttpRequest request) { return service.doSmth(); }
  14. ListenableFuture 14 Executor

  15. ListenableFuture creation Future future = executor.submit(task); ListenableFuture listenableFuture = JdkFutureAdapters.listenInPoolThread(future);

    Each future requires a thread to listen it! 15
  16. ListenableFuture creation - the right way pool = Executors.newFixedThreadPool(5); executor

    = MoreExecutors.listenableDecorator(pool); ListenableFuture future = executor.submit(task); 16
  17. Servlet 3.0 with ListenableFuture pool = Executors.newFixedThreadPool(5); executor = MoreExecutors.listenableDecorator(pool);

    ListenableFuture<Result> doSmth() { return executor.submit(()->...); } void doGet(HttpRequest request) { AsyncContext ctx = request.startAsync(); ListenableFuture<Result> future = service.doSmth(); future.addListener(res -> ctx.complete(res), MoreExecutors.sameThreadExecutor()); } 17
  18. Callbacks? void doSmth(callback) { step1().addListener(res1 -> step2(res1).addListener(res2 -> step3(res2).addListener(res3 ->

    callback(res3); } } } } 18 CPS Callback hell
  19. ListenableFuture composition. Futures class 19 import static com.google.common.util.concurrent.Futures.*; ListenableFuture doSmth()

    { return transformAsync( transformAsync(step1(), res1 -> step2(res1)), res2 -> step3(res2)); } No CPS Still looks like callback hell
  20. Fluent API 20 ListenableFuture doSmth() { step1() .transformAsync(res1 -> step2(res1))

    .transformAsync(res2 -> step3(res2)); } Not in Guava
  21. SettableFuture • SettableFuture is ListenableFuture • set() and setException() methods

    • Useful to convert callback API to Future API 21
  22. Callback to SettableFuture void get(URL url, Consumer<HttpResponse> cb) 22 =>

    ListenableFuture<HttpResponse> get(URL url) { SettableFuture<HttpResponse> future = new SettableFuture<>(); get(url, res -> fut.set(res)); return future; }
  23. Java 8 CompletableFuture • Appeared in Java 8 • CompletionStage

    (interface) similar to ListenableFuture • CompletableFuture (class) similar to SettableFuture • A part of standard library • Fluent API for Future composition 23
  24. CompletableFuture composition CompletionStage doSmth() { step1() .thenCompose(res1 -> step2(res1)) .thenCompose(res2

    -> step3(res2)); } 24
  25. CompletableFuture exception handling • Methods thenCompose, thenApply, etc. do not

    handle exceptions, but throw deeper in the chain 25 Throws exception CompletionStage doSmth() { step1() .thenCompose(res1 -> step2(res1)) .thenCompose(res2 -> step3(res2)) .exceptionally(e -> recoverFromException(e)); }
  26. 3 method types • thenCompose - uses current thread •

    thenComposeAsync - uses FJP.commonPool() • thenComposeAsync with executor - uses passed ExecutorService 26
  27. Be aware of StackOverflowError CF<Integer> current = CF.completedFuture<>(1); for (int

    i=0; i<10000; i++) { current = current.thenApply(val -> val + 1); } 27 CF<Integer> first = new CF<>(); CF<Integer> current = first; for (int i=0; i<10000; i++) { current = current.thenApply(val -> val + 1); } return first.complete(1); StackOverflow? 1) Neither 2) 1 sample 3) 2 sample 4) Both
  28. StackOverflowError 28

  29. And fix... 29

  30. 30

  31. 31

  32. For comprehensions Assume step3() requires input from both step1() and

    step2() 32 step1().thenCompose(res1 -> step2(res1).thenCompose(res2-> step3(res1, res2); ) ); Still looks like callback hell
  33. For comprehensions 33 step1().flatMap(res1 => step2(res1).flatMap(res2 => step3(res1, res2); )

    );
  34. For comprehensions for { res1 <- step1() res2 <- step2(res)

    res3 <- step3(res1, res2) } yield res3 34 step1().flatMap(res1 => step2(res1).flatMap(res2 => step3(res1, res2).map(res3=> res3 ) );
  35. For comprehensions • In Scala any class with methods below

    can be used in for comprehensions ◦ flatMap ◦ map ◦ withFilter 35
  36. For comprehensions is taken from... main :: IO() = do

    putStr “What’s your name?” name <- readLn putStr (“Hello, “ ++ name) 36 main :: IO() = (putStr “What’s your name?”) >> (readLn >>= (\name -> (putStr (“Hello, “ ++ name))))
  37. 1 vs 3 functions 37

  38. Scala. ExecutionContext • Mostly ExecutionContext.global is used • It’s using

    ForkJoinPool underneath • You can define your own ExecutionContext to run tasks on the same thread 38
  39. Same thread ExecutionContext object SameThreadExecutor extends ExecutionContextExecutor { def execute(runnable:

    Runnable): Unit = { runnable.run() } } 39 Any problems with that?
  40. StackOverflowError 40

  41. Trampolining 41

  42. Tail-recursion def factorial(n: Int) = { if (n==1) return 1;

    n*factorial(n-1); } 42 def factorial(curr: Int, n: Int) = { if (n==1) return curr; factorial(curr*n, n-1); }
  43. Mutual recursion problem 43

  44. Trampolining to the rescue 44

  45. Mutual recursion trampolining 45

  46. Trampolined ExecutionContext https://github.com/playframework/playframework/blob/master/framework/src/play-s treams/src/main/scala/play/api/libs/streams/Execution.scala#L31 1) Store next task in the

    thread local variable 2) Once current task is finished run through tasks stored in thread local 46
  47. Zipping futures 47

  48. Exception handling 1 48

  49. Exception handling 2 49

  50. List[Future[T]] -> Future[List[T]] 50

  51. Making Scala Futures more concise implicit class FutureOps[A](f: Future[A]) (implicit

    context: ExecutionContext) { def >>= [B](handler: A => Future[B]): Future[B] = f flatMap handler def >> [B](handler: => Future[B]): Future[B] = f flatMap {_ => handler} } 51
  52. Making Scala Futures more concise step1 .flatMap (_ => step2())

    .flatMap(res2 => step3(res2)) 52 step1 >> step2 >>= step3
  53. Future.unit & Future.never 53 Future is covariant and Nothing is

    a subtype of every type
  54. Future.never naive implementation 54 Memory leak!

  55. Cancel future 55 Nothing in Scala!

  56. Cancel future in Java • What mayInterruptIfRunning means? • Unlike

    plain old Java 5 future, mayInterruptIfRunning=true does not cause thread to be interrupted for CompletableFuture 56
  57. Cancel action when future is cancelled (async API) CompletableFuture<Response> sendRequest(Request

    req) { CF future = new CF(); server.send(req).addListener(resp -> { future.complete(resp); }); future.exceptionally(ex -> { if (ex instanceof CancellationException) { sever.cancel(req); } return null; }); return future; } 57
  58. Cancel action when future is cancelled (sync API) CompletableFuture<Response> sendRequest(Request

    req) { CF cf = new CF(); Future<Response> future = exec.submit(()->{ cf.complete(server.send(request)); //server.send handles interruptions }); cf.exceptionally(ex -> { if (ex instanceof CancellationException) { future.cancel(true); } throw ex; }); return cf; } 58
  59. Summary (Part 1) • Programming with Future’s is not much

    more difficult than writing blocking code • Future in Java 5 is very limited • Move to Java 8 (you still don’t use it?) • Use ListenableFuture if you can’t, but it’s not pleasure w/o lambdas • Learn Scala 59
  60. Logging 60

  61. Logging in single-threaded application • Just log what you need

    • You get easy to read logs, because only single thread is writing to logs 61
  62. Logging in multi-threaded application 1) Multiple threads are working concurrently

    => messages from different threads are interleaving 2) At least, you need to add ThreadId to the log to grep messages 3) Ideally, you need to add context (sessionId, requestId) to each log line 4) MDC (Mapped Diagnostic Context) 62
  63. Logging in async application 1) MDC rely on ThreadLocal =>

    could not be used with futures 2) You need to explicitly pass logging context from one stage to another 63
  64. Logging in async application (Java) class ContextResult<C, R> { final

    C context; final R result; <R2> ContextResult<C, R2> newResult(R2 newRes){ return new ContextResult(context, newRes); } <R2> ContextResult<C, R2> map(Function<R, R2> f) { return new ContextResult(context, f.apply(newRes)); } void throwException(Throwable t){ throw new ContextException(context, t); } } 64
  65. Logging in async application (Java) class ContextException<C> extends Exception {

    private C context; ContextException(C context, Throwable cause) { super(cause); this.context = context; } C getContext(){ return context; } } 65
  66. Logging in async application (Java) CF.supplyAsync(() -> new ContextResult(sessionId, 1))

    .thenApply(r -> r.newResult(r.result+1)) .thenApply(r -> r.map(x -> x+1)) .thenApply(r -> r.throwException(new Exception())) .exceptionally(t -> { ContextException<String> ex = (ContextException<String>) t; String sessionId = ex.getContext(); log.error(“Error in session {}”, sessionId); }); 66
  67. Logging in async application (Scala) implicit val context = “123”;

    Future {1} .map(x => x+1) .map(x => throw Exception()) .recover{case e => log.error(“Exception in session {}”, context)}; 67
  68. Exception handling 68

  69. Exception handling in sync application (in Java) 1) Unchecked exceptions

    int foo() 2) Checked exceptions int foo() throws Exception 69
  70. Exception handling in sync application (in Scala) Only unchecked exceptions

    def foo() : Int 70
  71. Exceptions pros/cons Pros: 1) Exceptions contain stack-trace info Cons: 1)

    Exceptions are expensive (stack unwinding) 2) No information about exception in method signature (only for unchecked exception) 71
  72. Functional style error handling. Try sealed trait Try[+T] case class

    Success[+T](value: T) extends Try[T] case class Failure[T <: Nothing](exception: Throwable) extends Try[T] object Try { def apply[T](f: => T): Try[T] = try { Success(f) } catch { case NonFatal(e) => Failure(e) } } 72
  73. Try monad sealed trait Try[+T] { def flatMap[U](f: (T) =>

    Try[U]): Try[U] def map[U](f: (T) => U): Try[U] def foreach[U](f: T => U): Unit def isSuccess: Boolean def isFailure: Boolean def getOrElse(f: => T): T = f } 73
  74. Try for-comprehension val result = for { v <- Try("5".toInt)

    k <- Try("six".toInt) z <- Try("9".toInt) } yield( v + k + z) result match { case Success(r) => r === 20 case Failure(e) => throw new IllegalStateException() } 74
  75. Try vs unchecked exceptions 1) Try is functional approach to

    unchecked exception 2) Try in method return type says “Function may throw, but particular error type is unknown” 75
  76. Either type sealed abstract class Either[+A, +B] final case class

    Right[+A, +B](value: B) extends Either[A, B] final case class Left[+A, +B](value: A) extends Either[A, B] Either is right-biased @since Scala 2.12 and could be used in for comprehensions Either in method return type says: “Function may throw and here is the type of possible error” 76
  77. Method signatures with Either def getUserByEmail(email: String) : Either[String, User]

    def getFacebookProfile(user: User) : Either[String, FacebookProfile] def getUserFacebookAge(profile: FacebookProfile): Either[String, Int] Now function might return Either[ErrorType, ValueType] and explicitly specify that it may return error of particular type 77
  78. Either for-comprehensions val ageOrError = for { user <- getUserByEmail(email)

    profile <- getFacebookProfile(user) age <- getUserAge(profile) } yield age 78 ageOrError.match { case Left(error) => log.error(“error occured {}”, error) case Right(age) => log.info(“user age is {}”, age) } Not that useful, to represent error type as string
  79. Errors as case classes sealed trait UserServiceError case class AuthError(why:

    String) extends UserServiceError case object ConnectionError extends UserServiceError 79 sealed trait SocialNetworksServiceError case object NoUserProfile extends SocialNetworksServiceError case object ConnectionError extends SocialNetworksServiceError sealed trait FacebookServiceError case object NoSuchUser extends FacebookServiceError case object FieldValueUnknown(fieldName: String) extends FacebookServiceError case object ConnectionError extends FacebookServiceError
  80. New method signatures def getUserByEmail(email: String) : Either[UserServiceError, User] def

    getFacebookProfile(user: User) : Either[SocialNetworksServiceError, FacebookProfile] def getUserFacebookAge(profile: FacebookProfile): Either[FacebookServiceErorr, Int] 80
  81. Example with HttpResponse Either[HttpResponse, HttpResponse] response = for { user

    <- getUserByEmail(email) .left.map { case AuthError(why: String) => Forbidden(why) case ConnectionError => InternalServerError() } profile <- getFacebookProfile(user) .left.map { case …. } age <- getUserFacebookAge(profile) .left.map { case …. } } yield Ok(age) 81
  82. Exception handling in async application 1) Future might complete either

    successfully or exceptionally 2) Exception handling in async programming is similar to unchecked exception in sync programming 3) You don’t know which exception might be thrown 82
  83. Future method signatures def getUserByEmail(email: String) : Future[User] //throws UserServiceException

    def getFacebookProfile(user: User) : Future[FacebookProfile] //throws SocialNetworksServiceException def getUserFacebookAge(profile: FacebookProfile): Future[Int] //throws FacebookServiceException 83
  84. Handling all exceptions in onComplete val ageFut = for {

    user <- getUserByEmail(email) profile <- getFacebookProfile(user) age <- getUserAge(profile) } yield age 84 ageFut onComplete { case Failure(t) => { if (t instanceof AuthException) return Forbidden(); if (t instance ConnectionException) return InternalServerError(); … return InternalServiceError(); } } Need to know all exceptions!
  85. Handling exceptions right after method call Future[HttpResponse] response = for

    { user <- getUserByEmail(email) .recover { //it’s important to recover with exception, otherwise other statements in for will run case AuthError(why: String) => throw HttpException(Forbidden(why)) case ConnectionError => throw HttpException(InternalServerError()) } …. } yield Ok(age) 85 Not bad, but compiler does not check exceptions that might be thrown response.recover{ case HttpException(response) => response }
  86. Future[Either] method signatures def getUserByEmail(email: String) : Future[Either[UserServiceError, User]] def

    getFacebookProfile(user: User) : Future[Either[SocialNetworksServiceErorr, FacebookProfile]] def getUserFacebookAge(profile: FacebookProfile): Future[Either[FacebookServiceError, Int]] 86
  87. You want to write like this response = for {

    user <- getUserByEmail(email) |> fromFutureEither { case AuthError(why: String) => Forbidden(why) case ConnectionError => InternalServerError() } profile <- getFacebookProfile(user) |> fromFutureEither { case …. } age <- getUserFacebookAge(profile) |> fromFutureEither { case …. } } yield Ok(age) 87
  88. Let’s define FutureEither... case class FutureEither[L, R](future: Future[Either[L, R]]) {

    def flatMap[R2](f: R => FutureEither[R2]): FutureEither[R2] = { val result = future flatMap { case Left(l) => Future.successful(Left(l)) case Right(r) => f(r).future } FutureEither(result) } } 88
  89. … and fromFutureEither def fromFutureEither[A, B] (err: A=>HttpResponse) (future: Future[Either[A,

    B]]) : FutureEither[HttpResponse, B] = FutureEither(future.map(_.left.map(err)) 89
  90. And now you can do it! responseFE : FutureEither[HttpResponse, HttpResponse]

    = for { user <- getUserByEmail(email) |> fromFutureEither { case AuthError(why: String) => Forbidden(why) case ConnectionError => InternalServerError() } …. } yield Ok(age) 90 responseFE.future //Future[Either[HttpResponse, HttpResponse]]
  91. EitherT[F[_], A, B] 1) Cats or scalaz library 2) Monad

    transformer 3) Wraps F[Either[A, B]] 4) Works for all monads F[_] type FutureEither[A, B] = EitherT[Future, A, B] 91
  92. Using EitherT responseFE : EitherT[Future, HttpResponse, HttpResponse] = for {

    user <- getUserByEmail(email) |> fromFutureEither { case AuthError(why: String) => Forbidden(why) case ConnectionError => InternalServerError() } …. } yield Ok(age) 92 responseFE.run //Future[Either[HttpResponse, HttpResponse]]
  93. Summary (Part 2) • MDC does not work for async

    applications • You need to explicitly pass logging context • … or use implicit in Scala • Checked exceptions is not bad • Use Either instead of checked exception in Scala • Make sure to use Either in monadic style • Future[Either] is similar to checked exceptions in async apps • Use EitherT monad-transformer to combine Future[Either] in for-comprehensions 93
  94. Reading list 1. http://www.nurkiewicz.com/2013/05/java-8-completablefuture-in-action.html 2. https://github.com/google/guava/wiki/ListenableFutureExplained 3. https://github.com/viktorklang/blog 4. http://yanns.github.io/blog/2016/02/10/trampoline-execution-context-with-scal

    a-futures/ 5. http://blog.higher-order.com/assets/trampolines.pdf 6. https://www.slideshare.net/GaryCoady/unsucking-error-handling-with-futures 7. https://alexn.org/blog/2017/01/30/asynchronous-programming-scala.html 8. https://monix.io/docs/2x/eval/task.html 9. https://github.com/Kotlin/kotlin-coroutines 10. https://ponyfoo.com/articles/understanding-javascript-async-await 11. http://docs.scala-lang.org/sips/pending/async.html 94
  95. Reading list 1. http://altair.cs.oswego.edu/mailman/listinfo/concurrency-interest 2. Brian Goetz, Java Concurrency in

    Practice 3. Felix Frank, Learning Concurrent Programming in Scala 4. java.util.concurrent.* 5. scala.concurrent.* 95
  96. Andrey Ershov andrershov@gmail.com @andrershov 96