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

Андрей Ершов — 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.

Moscow JUG

August 31, 2017
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

  1. 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
  2. Agenda • Future, ListenableFuture, CompletableFuture in Java • Future в

    Scala • Comparison • Logging in async applications • Exception handling in async applications 3
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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(); }
  9. ListenableFuture creation - the right way pool = Executors.newFixedThreadPool(5); executor

    = MoreExecutors.listenableDecorator(pool); ListenableFuture future = executor.submit(task); 16
  10. 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
  11. 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
  12. 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; }
  13. 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
  14. 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)); }
  15. 3 method types • thenCompose - uses current thread •

    thenComposeAsync - uses FJP.commonPool() • thenComposeAsync with executor - uses passed ExecutorService 26
  16. 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
  17. 30

  18. 31

  19. 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
  20. 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 ) );
  21. For comprehensions • In Scala any class with methods below

    can be used in for comprehensions ◦ flatMap ◦ map ◦ withFilter 35
  22. 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))))
  23. 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
  24. 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); }
  25. 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
  26. Making Scala Futures more concise step1 .flatMap (_ => step2())

    .flatMap(res2 => step3(res2)) 52 step1 >> step2 >>= step3
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Exception handling in sync application (in Java) 1) Unchecked exceptions

    int foo() 2) Checked exceptions int foo() throws Exception 69
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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!
  53. 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 }
  54. 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
  55. 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
  56. 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
  57. … and fromFutureEither def fromFutureEither[A, B] (err: A=>HttpResponse) (future: Future[Either[A,

    B]]) : FutureEither[HttpResponse, B] = FutureEither(future.map(_.left.map(err)) 89
  58. 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]]
  59. 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
  60. 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]]
  61. 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
  62. 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
  63. 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