Successfully reported this slideshow.

Play Framework: async I/O with Java and Scala

164

Share

Loading in …3
×
1 of 184
1 of 184

More Related Content

Related Books

Free with a 14 day trial from Scribd

See all

Play Framework: async I/O with Java and Scala

  1. 1. Asynchronous I/O with Java and Scala
  2. 2. LinkedIn uses a service oriented architecture (SOA)
  3. 3. Hundreds of different types of services, thousands of instances in multiple data centers. Internet Load Balancer Profile frontend Company frontend Recruiter frontend Profile backend Search backend Company backend Recruiter backend Ads backend Data Store Data Store Data Store Data Store
  4. 4. Services communicate with each other via remote calls
  5. 5. Profile frontend Profile backend /profile/123 HTTP request
  6. 6. Profile frontend Profile backend JSON response { "id": 123, "first": "Yevgeniy", "last": "Brikman" }
  7. 7. Most people are used to synchronous I/O when making requests between servers
  8. 8. The most popular frameworks typically use one-thread-per-request and blocking I/O
  9. 9. executeMethod blocks the thread until the response comes back void doGet(HttpServletRequest req, HttpServletResponse res) { // Apache HttpClient HttpClient client = new HttpClient(); GetMethod method = new GetMethod("www.example.com"); // executeMethod is a blocking, synchronous call int statusCode = client.executeMethod(method); System.out.println("Response " + statusCode); } MyServlet.java
  10. 10. Evented servers have one thread/process per CPU core and use non-blocking I/O
  11. 11. http.request is a non-blocking call: the next line executes before the response comes back MyNodeApp.js var callback = function(data) { console.log("Response: " + data); }; var options = { hostname: 'www.google.com', path: '/upload' }; // Non-blocking HTTP call http.request(options, callback); console.log('This line may execute before the callback!');
  12. 12. Why threaded vs. evented matters for LinkedIn
  13. 13. void doGet(HttpServletRequest req, HttpServletResponse res) { // Call a number of backend services to get data Profile profile = profileSvc.getProfile(); Company company = companySvc.getCompany(); Skills skills = skillsSvc.getSkills(); } MyServlet.java Our services spend most of their time waiting for data from other services and data stores
  14. 14. I/O is very expensive http://www.eecs.berkeley.edu/~rcs/research/interactive_latency.html
  15. 15. In a threaded server, threads spend most of the time idle, waiting on I/O
  16. 16. Threading dilemma 1. Creating new threads on the fly is expensive: a. Use a thread pool 2. Too many threads in the thread pool: a. Memory overhead b. Context switching overhead 3. Too few threads in the thread pool: a. Run out of threads, latency goes up b. Sensitive to downstream latency!
  17. 17. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store Let's say latency goes up a little here
  18. 18. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store Causes threads to get backed up here
  19. 19. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store Latency goes up
  20. 20. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store Now threads get backed up here
  21. 21. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store And here
  22. 22. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store Here too
  23. 23. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store And there
  24. 24. Internet Load Balancer Frontend Server Frontend Server Frontend Server Backend Server Backend Server Backend Server Backend Server Backend Server Data Store Data Store Data Store Data Store And... the site is down.
  25. 25. This is thread pool hell
  26. 26. Play is built on top of Netty, so it supports non- blocking I/O
  27. 27. NIO benefits 1. No sensitivity to downstream slowness 2. Easy to parallelize I/O 3. Supports many concurrent and long-running connections, enabling: a. WebSockets b. Comet c. Server-Sent Events
  28. 28. This talk is a brief introduction to writing asynchronous code with the Play Framework.
  29. 29. For each section, I will include simplified examples: first in Java and then Scala.
  30. 30. The world’s largest professional network
  31. 31. at We've been using Play in production for more than 6 months
  32. 32. A few apps built on Play
  33. 33. Channels (frontend)
  34. 34. Premium Subscriptions (frontend)
  35. 35. Polls (frontend + backend)
  36. 36. REST search (internal tool)
  37. 37. About me Leading the Play project as part of LinkedIn's Service Infrastructure Team. Also: hackdays, engineering blog, incubator, open source.
  38. 38. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  39. 39. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  40. 40. Download and install Play from http://www.playframework.com
  41. 41. > play new my-app
  42. 42. > play idea > play eclipse
  43. 43. > play run
  44. 44. http://localhost:9000
  45. 45. Application layout app → Application sources └ assets → Compiled asset sources └ controllers → Application controllers └ models → Application business layer └ views → Templates conf → Configurations files └ application.conf → Main configuration file └ routes → Routes definition public → Public assets └ stylesheets → CSS files └ javascripts → Javascript files └ images → Image files project → sbt configuration files └ Build.scala → Application build script └ plugins.sbt → sbt plugins lib → Unmanaged libraries dependencies logs → Standard logs folder target → Generated stuff test → Unit or functional tests
  46. 46. Let's get a feel for Play by creating a Java Controller
  47. 47. public class HelloWorld extends Controller { public static Result index() { return ok("Hello World"); } } Controllers are Java classes with methods that return a Result, such as a 200 OK app/controllers/HelloWorld.java
  48. 48. Don't worry about the use of static. Yes, Play supports IOC. Using static (and other shortcuts) lets me keep the examples simple.
  49. 49. GET /hello controllers.HelloWorld.index() Expose the controller/action at a URL conf/routes
  50. 50. http://localhost:9000/hello
  51. 51. Woohoo, hot reload!
  52. 52. public class HelloWorld extends Controller { public static Result index(String name) { return ok("Hello " + name); } } Add a parameter app/controllers/HelloWorld.java
  53. 53. GET /hello controllers.HelloWorld.index( name) Read the parameter from the query string conf/routes
  54. 54. http://localhost:9000/hello?name=Jim
  55. 55. GET /hello/:name controllers.HelloWorld.index(name) Read the parameter from the URL instead conf/routes
  56. 56. http://localhost:9000/hello/Jim
  57. 57. public class HelloWorld extends Controller { public static Result index(String name, int age) { return ok("Hello " + name + " you are " + age + " years old"); } } Add another parameter, this time an int app/controllers/HelloWorld.java
  58. 58. GET /hello/:name/ :age controllers.HelloWorld.index(name: String, age: Int) Add the parameter. Note the type checking! conf/routes
  59. 59. http://localhost:9000/hello/Jim/28
  60. 60. @(name: String, age: Int) <html> <head></head> <body> <img src="/assets/images/play-logo.png"/> <p> Hello <b>@name</b>, you are <b>@age</b> years old </p> </body> </html> Add a view app/views/hello.scala.html
  61. 61. public class HelloWorld extends Controller { public static Result index(String name, int age) { return ok(views.html.hello.render(name, age)); } } Render the view from the controller app/controllers/HelloWorld.java
  62. 62. http://localhost:9000/hello/Jim/28
  63. 63. Play also natively supports Scala
  64. 64. app/controllers/HelloWorldScala.scala Just add a .scala file under /app and Play will compile it object HelloWorldScala extends Controller { def index = Action { Ok("Hello World Scala") } }
  65. 65. GET /scala controllers.HelloWorldScala.index() Add it to the routes file as usual conf/routes
  66. 66. http://localhost:9000/scala
  67. 67. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  68. 68. Let's use Play's Web Services library (WS) to make some non-blocking HTTP calls
  69. 69. public class Proxy extends Controller { public static Result index(String url) { // Non blocking HTTP call Promise<Response> responsePromise = WS.url(url).get(); // How do we turn a Promise into a Play Result? } } app/controllers/Proxy.java Create a new controller and use WS to make an HTTP GET
  70. 70. A Promise<T> will eventually contain the value T (or an error)
  71. 71. (Play Framework source code) Play has a built-in subclass of Result called AsyncResult that takes a Promise<Result> public static class AsyncResult implements Result { private final Promise<Result> promise; public AsyncResult(Promise<Result> promise) { this.promise = promise; } }
  72. 72. public class Proxy extends Controller { public static Result index(String url) { Promise<Response> response = WS.url(url).get(); // Transform asynchronously into a Play Result Promise<Result> result = response.map(toResult); return async(result); } // A function that can transform a Response into a Result private static Function<Response, Result> toResult = new Function<Response, Result>() { public Result apply(Response response) { return ok(response.getBody()).as(("text/html"); } }; } app/controllers/Proxy.java We can use the map method to turn a Promise<Response> into a Promise<Result>
  73. 73. GET /proxy controllers.Proxy.index(url) Add this endpoint to the routes file conf/routes
  74. 74. http://localhost:9000/proxy?url=http://example.com
  75. 75. We just built a completely non-blocking proxy!
  76. 76. public class Proxy extends Controller { public static Result index(String url) { Logger.info("Before the HTTP call"); Promise<Response> response = WS.url(url).get(); Promise<Result> result = response.map(toResult); Logger.info("After the HTTP call"); return async(result); } private static Function<Response, Result> toResult = new Function<Response, Result>() { public Result apply(Response response) { Logger.info("Inside the toResult function"); return ok(response.getBody()).as("text/html"); } }; } app/controllers/Proxy.java To see that it's non-blocking, add logging
  77. 77. Refresh the page and the logs show the HTTP call really is non-blocking
  78. 78. Let's create the same Proxy in Scala
  79. 79. object ProxyScala extends Controller { def index(url: String) = Action { val future: Future[Response] = WS.url(url).get() // How do we turn a Future into a Play Result? } } app/controllers/ProxyScala.scala Create a new controller and use WS to make an HTTP GET
  80. 80. A Future[T] will eventually contain the value T (or an error)
  81. 81. (Play Framework source code) Play has a built-in subclass of Result called AsyncResult that takes a Future<Result> case class AsyncResult(result: Future[Result]) extends Result // Convenience function to create an AsyncResult def Async(promise: Promise[Result]) = AsyncResult(promise)
  82. 82. object ProxyScala extends Controller { def index(url: String) = Action { val future: Future[Response] = WS.url(url).get() Async { future.map { response => Ok(response.body).as("text/html") } } } } app/controllers/ProxyScala.scala We can use the map method to turn a Future [Response] into a Future[Result]
  83. 83. GET /scala/proxy controllers.ProxyScala.index (url) Add this endpoint to the routes file conf/routes
  84. 84. http://localhost:9000/scala/proxy?url=http://example.com
  85. 85. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  86. 86. What is this "map" thing all about?
  87. 87. It's easiest to think about map with Lists.
  88. 88. val names = List("Jim", "Dean", "Kunal") Let's start with a List of Strings
  89. 89. val names = List("Jim", "Dean", "Kunal") def lower(str: String): String = str.toLowerCase And a simple function that converts a String to lowercase
  90. 90. List.map(f) will return a new List where each element in the new List is the result of calling f on each element of the original List
  91. 91. val names = List("Jim", "Dean", "Kunal") def lower(str: String): String = str.toLowerCase names.map(lower) // Output: List("jim", "dean", "kunal") Mapping the lower function over names gives us a new List where each name is lowercase
  92. 92. We saw map transform a List[String] into a new List[String]. Can we transform a List[X] into some other type List[Y]?
  93. 93. val names = List("Jim", "Dean", "Kunal") def strlen(str: String): Int = str.length Start with the same List, but now a new function strlen that returns the length of a String
  94. 94. val names = List("Jim", "Dean", "Kunal") def strlen(str: String): Int = str.length names.map(strlen) // Output: List(3, 4, 5) Mapping strlen over names returns a new List with the length of each String in names
  95. 95. Now we see that map can transform a List [String] into a new List[Int].
  96. 96. class List[A] { def map[B](f: A => B): List[B] } More generally, this is map's signature
  97. 97. Let's look at one more example
  98. 98. val names = List("Jim", "Dean", "Kunal") def explode(str: String): List[Char] = str.toCharArray. toList Same List, but now a new function explode that returns a List of Characters in a String
  99. 99. val names = List("Jim", "Dean", "Kunal") def explode(str: String): List[Char] = str.toCharArray. toList names.map(explode) // Output: // List(List(J, i, m), List(D, e, a, n), List(K, u, n, a, l)) If we map explode over names, we get nested Lists. But what if we want just one, flat List?
  100. 100. class List[A] { def flatMap[B](f: A => List[B]): List[B] } We can use flatMap , which will combine (flatten) any nested Lists
  101. 101. val names = List("Jim", "Dean", "Kunal") def explode(str: String): List[Char] = str.toCharArray. toList names.flatMap(explode) // Output: List(J, i, m, D, e, a, n, K, u, n, a, l) Using flatMap gives us a single List with each individual character
  102. 102. map and flatMap are defined on any "collection" or "container": List, Set, Map, etc
  103. 103. val namesSet = Set("Jim", "Dean", "Kunal") def explode(str: String): List[Char] = str.toCharArray. toList namesSet.flatMap(explode) // Output: Set(e, n, J, u, a, m, i, l, K, D) Using flatMap on a Set
  104. 104. Futures and Promises are also "containers": they just happen to contain 1 item.
  105. 105. val future: Future[Response] = WS.url(url).get() val future: Future[Result] = future.map { response => Ok(response.body).as("text/html") } This is why it makes sense to use map to turn a Future[Response] into a Future[Result]
  106. 106. The "container" class controls when the function passed to map or flatMap actually gets applied!
  107. 107. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  108. 108. Making I/O requests in parallel is essential for performance in a Service Oriented Architecture
  109. 109. With non-blocking I/O, parallel is the default // These 3 HTTP requests will execute in parallel Promise<Response> yahoo = WS.url("http://yahoo.com").get(); Promise<Response> google = WS.url("http://google.com").get(); Promise<Response> bing = WS.url("http://bing.com").get();
  110. 110. Let's fetch 3 websites in parallel and time them
  111. 111. First, define a function that makes makes an HTTP GET and returns a Promise with timing info public Promise<Timing> timed(final String url) { final long start = System.currentTimeMillis(); return WS.url(url).get().map(new Function<Response, Timing>() { public Timing apply(Response response) throws Throwable { return new Timing(url, System.currentTimeMillis() - start); } }); } public class Timing { public String url; public long latency; }
  112. 112. Next, make a controller that fires 3 requests in parallel using the timed function we just created public class Parallel extends Controller { public static Result index() { final Promise<Timing> yahoo = timed("http://www.yahoo.com"); final Promise<Timing> google = timed("http://www.google.com"); final Promise<Timing> bing = timed("http://www.bing.com"); } }
  113. 113. Compose the 3 Promises into a single Promise that will redeem when all 3 are done public class Parallel extends Controller { public static Result index() { final Promise<Long> yahoo = timed("http://www.yahoo.com"); final Promise<Long> google = timed("http://www.google.com"); final Promise<Long> bing = timed("http://www.bing.com"); Promise<List<Timing>> all = Promise.waitAll(yahoo, google, bing); } }
  114. 114. Render the results as JSON public class Parallel extends Controller { public static Result index() { final Promise<Timing> yahoo = timed("http://www.yahoo.com"); final Promise<Timing> google = timed("http://www.google.com"); final Promise<Timing> bing = timed("http://www.bing.com"); Promise<List<Timing>> all = Promise.waitAll(yahoo, google, bing); return async(all.map(new Function<List<Timing>, Result>() { public Result apply(List<Timing> timings) throws Throwable { return ok(Json.toJson(timings)); } })); } }
  115. 115. GET /parallel controllers.Parallel.index() Add it to the routes file conf/routes
  116. 116. http://localhost:9000/parallel
  117. 117. How about parallel requests in Scala?
  118. 118. Once again, define a function that makes an HTTP GET and returns a Future with timing info def timed(url: String): Future[Timing] = { val start = System.currentTimeMillis() WS.url(url).get().map(_ => Timing(url, System.currentTimeMillis() - start) ) } case class Timing(url: String, latency: Long)
  119. 119. Next, make a controller that fires 3 requests in parallel using the timed function we just created object ParallelScala extends Controller { def index = Action { val yahoo = timed("http://www.yahoo.com") val google = timed("http://www.google.com") val bing = timed("http://www.bing.com") } }
  120. 120. Compose the 3 Futures into a single Future that will redeem when all 3 are done object ParallelScala extends Controller { def index = Action { val yahoo = timed("http://www.yahoo.com") val google = timed("http://www.google.com") val bing = timed("http://www.bing.com") val all = Future.sequence(Seq(yahoo, google, bing)) } }
  121. 121. Render the results as JSON object ParallelScala extends Controller { def index = Action { val yahoo = timed("http://www.yahoo.com") val google = timed("http://www.google.com") val bing = timed("http://www.bing.com") val all = Future.sequence(Seq(yahoo, google, bing)) Async { all.map(timings => Ok(Json.toJson(timings))) } } }
  122. 122. GET /scala/parallel controllers.ParallelScala.index() Add it to the routes file conf/routes
  123. 123. http://localhost:9000/scala/parallel
  124. 124. If parallel is the default, how do you do sequential steps that depend on each other?
  125. 125. Example: make a request to duckduckgo's instant answer API (step 1) and proxy an image from the response (step 2)
  126. 126. First, call make a non-blocking call to duckduckgo public class LuckyImage extends Controller { public static Result index(String query) { Promise<Response> duck = WS.url("http://www.duckduckgo.com") .setQueryParameter("q", query) .setQueryParameter("format", "json") .get(); } }
  127. 127. As a first step, we'll just proxy the response public class LuckyImage extends Controller { public static Result index(String query) { Promise<Response> duck = WS.url("http://www.duckduckgo.com") .setQueryParameter("q", query) .setQueryParameter("format", "json") .get(); return async(duck.map(new Function<Response, Result>() { public Result apply(Response response) throws Throwable { return ok(response.getBodyAsStream()) .as(response.getHeader("Content-Type")); } })); } }
  128. 128. GET /lucky controllers.LuckyImage.index (url) Add it to the routes file conf/routes
  129. 129. http://localhost:9000/lucky?query=linkedin
  130. 130. As the second step, get the image URL from the response and proxy just that. Promise<Response> duck = // ... (same request as before) return async(duck.flatMap(new Function<Response, Promise<Result>>(){ public Promise<Result> apply(Response response) { String url = getImageUrlFromResponse(response); return WS.url(url).get().map(new Function<Response, Result>() { public Result apply(Response response) { return ok(response.getBodyAsStream()) .as(response.getHeader("Content-Type")); } }); } }));
  131. 131. http://localhost:9000/lucky?query=linkedin
  132. 132. Ok, let's try the same example in Scala
  133. 133. First, make the request object LuckyImageScala extends Controller { def index(query: String) = Action { val duck = WS.url("http://www.duckduckgo.com") .withQueryString("q" -> query, "format" -> "json").get() } }
  134. 134. Then extract the image URL and proxy it object LuckyImageScala extends Controller { def index(query: String) = Action { val duck = WS.url("http://www.duckduckgo.com") .withQueryString("q" -> query, "format" -> "json").get() Async { duck.flatMap { response => val url = getImageUrlFromResponse(response) WS.url(url).get().map { r => Ok(r.getAHCResponse.getResponseBodyAsBytes) .as(r.getAHCResponse.getHeader("Content-Type")) } } } } }
  135. 135. GET /scala/lucky controllers.LuckyImageScala.index (url) Add it to the routes file conf/routes
  136. 136. http://localhost:9000/lucky?query=play+framework
  137. 137. In both Java and Scala, you order async actions sequentially by nesting map and flatMap calls.
  138. 138. Many sequential steps will lead to lots of nesting. step1.flatMap(new Function<Response, Promise<Result>>(){ public Promise<Result> apply(Response response1) { step2.flatMap(new Function<Response, Promise<Result>>(){ public Promise<Result> apply(Response response2) { step3.flatMap(new Function<Response, Promise<Result>>(){ public Promise<Result> apply(Response response3) { // etc } } } }); } });
  139. 139. This is "callback hell"
  140. 140. ParSeq https://github.com/linkedin/parseq
  141. 141. ParSeq is a framework that makes it easier to write asynchronous code in Java
  142. 142. Wrap asynchronous work in ParSeq Task objects, which are similar to Promises and Futures Task<Response> yahoo = Tasks.wrap(WS.url("http://www.yahoo.com")) Task<Response> google = Tasks.wrap(WS.url("http://www.google.com")) Task<Response> bing = Tasks.wrap(WS.url("http://www.bing.com"))
  143. 143. Use Tasks.par to compose tasks in parallel Task<Response> yahoo = Tasks.wrap(WS.url("http://www.yahoo.com")) Task<Response> google = Tasks.wrap(WS.url("http://www.google.com")) Task<Response> bing = Tasks.wrap(WS.url("http://www.bing.com")) // Create a new Task that will run all 3 tasks above at the same time // and redeem when they are all done Task<?> parallel = Tasks.par(yahoo, google, bing)
  144. 144. Use Tasks.seq to compose tasks sequentially Task<Response> step1 = new Task() { ... } Task<Response> step2 = new Task() { ... } Task<Response> step3 = new Task() { ... } // Create a new Task that will run the tasks above one at a time, // in the order specified, and complete with the return value of the // last one Task<Response> sequential = Tasks.seq(task1, task2, task3)
  145. 145. ParSeq makes async code declarative and easier to reason about: you can read it top to bottom! Task<Result> complex = Tasks.seq( Tasks.par(profileTask, companyTask, skillsTask), Tasks.par(recommendedJobsTask, wvmxTask), Tasks.par(hydrateImagesTask, hydrateJobsTask, hydrateSkillsTask) Tasks.seq(assemblePageTask, fireTrackingTask) )
  146. 146. We'll soon be open sourcing a plugin soon to make it easy to use ParSeq in Play.
  147. 147. Sequence Comprehensions
  148. 148. Syntactic sugar built into Scala. Translates into map and flatMap calls without the nesting.
  149. 149. Instead of this... aFuture.flatMap { a => bFuture.flatMap { b => cFuture.flatMap { c => dFuture.map { d => // Build a result using a, b, c, d } } } }
  150. 150. Use this. Note that this syntax works for any object with map and flatMap methods. for { a <- aFuture b <- bFuture c <- cFuture d <- dFuture } yield { // Build a result using a, b, c, d }
  151. 151. Sequential async I/O example. Note that each step can refer to previous ones. for { a <- WS.url(...).get() b <- WS.url(a).get() c <- WS.url(a + b).get() d <- WS.url(a + b + c).get() } yield { // Build a result using a, b, c, d }
  152. 152. Parallel async I/O example. Only difference is that the async calls are outside the for statement. val futureA = WS.url(...) val futureB = WS.url(...) val futureC = WS.url(...) val futureD = WS.url(...) for { a <- futureA b <- futureB c <- futureC d <- futureD } yield { // Build a result using a, b, c, d }
  153. 153. Sequence comprehensions provide a clean and consistent API for async code: you can read it top to bottom!
  154. 154. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  155. 155. With a single server, the odds of hitting an error are relatively low
  156. 156. In a distributed system with thousands of servers, the odds that you hit an error are very high Internet Load Balancer Profile frontend Company frontend Recruiter frontend Profile backend Search backend Company backend Recruiter backend Ads backend Data Store Data Store Data Store Data Store
  157. 157. Even if a single server is up 99.999% of the time, with 1000 servers, the odds that one is down are 1 - 0.999991000 = ~1%
  158. 158. Here is how to make your async code more resilient to errors and slow performance
  159. 159. We can use the recover method on a Promise to specify how to handle errors public class Errors extends Controller { public static Result index(String url) { F.Promise<WS.Response> promise = WS.url(url).get(); return async(promise.map(new F.Function<WS.Response, Result>() { public Result apply(WS.Response response) throws Throwable { return ok("Got a response!"); } }).recover(new F.Function<Throwable, Result>() { public Result apply(Throwable throwable) throws Throwable { return internalServerError("Got an exception: " + throwable); } })); } }
  160. 160. There is an analogous recover method on Scala Futures as well object ErrorsScala extends Controller { def index(url: String) = Action { Async { WS.url(url).get().map { response => Ok("Got a response: " + response.status) }.recover { case t: Throwable => InternalServerError("Got an exception: " + t) } } } }
  161. 161. GET /errors controllers.Errors.index(url) GET /scala/errors controllers.ErrorsScala.index(url) Add to the routes file conf/routes
  162. 162. http://localhost:9000/errors?url=http://www.example.com
  163. 163. http://localhost:9000/errors?url=http://www.not-a-real-url.com
  164. 164. If some of the data you fetch isn't required to render the page, you can use an Option pattern
  165. 165. Create a helper method: on success, return Some<T>. On failure, log the error, return None. public static <T> Promise<Option<T>> optional(Promise<T> promise){ return promise.map(new Function<T, Option<T>>() { public Option<T> apply(T value) throws Throwable { if (value == null) { return Option.None(); } return Option.Some(value); } }).recover(new Function<Throwable, Option<T>>() { public Option<T> apply(Throwable t) throws Throwable { Logger.error("Hit an error", t); return Option.None(); } }); }
  166. 166. Wrap Promises with optional and inside of map, use isDefined to see if you have a value public static Result index(String url) { Promise<Option<Response>> promise = optional(WS.url(url).get()); return async(promise.map(new Function<Option<Response>, Result>() { public Result apply(Option<Response> responseOpt){ if (responseOpt.isDefined()) { Response response = responseOpt.get(); // Use the response to build the page } else { // Build the page without the response } } })); }
  167. 167. The same pattern is even prettier in Scala, as Option is a first-class citizen of the language
  168. 168. Reusable helper method to convert Future[T] to Future[Option[T]] def optional[T](future: Future[T]): Future[Option[T]] = { future.map(Option.apply).recover { case t: Throwable => Logger.error("Hit an error", t) None } }
  169. 169. Wrap Futures with optional and use pattern matching, comprehensions, etc within map object OptionExampleScala extends Controller { def index(url: String) = Action { Async { optional(WS.url(url).get()).map { case Some(response) => // Use the response to build the page case _ => // Build the page without the response } } } }
  170. 170. Sometimes, waiting too long for data is worse than not showing that data at all
  171. 171. You can create a Promise that will be redeemed with someValue after the specified timeout Promise.timeout(someValue, 500, TimeUnit.MILLISECONDS)
  172. 172. Compose two Promises using or: the result takes on the value of the first one to complete Promise<Response> response = WS.url(url).get(); Promise<Either<Object, Response>> withTimeout = Promise.timeout(null, timeout, TimeUnit.MILLISECONDS).or(response);
  173. 173. If right is defined, you got a value in time; otherwise, it must have timed out first. Promise<Response> response = WS.url(url).get(); Promise<Either<Object, Response>> withTimeout = Promise.timeout(null, timeout, TimeUnit.MILLISECONDS).or(response); withTimeout.map(new Function<Either<Object, Response>, Result>(){ public Result apply(Either<Object, Response> either) { if (either.right.isDefined()) { Response response = either.right.get(); // Use the response to build the page } else { // Hit a timeout, build the page without the response } } });
  174. 174. The Scala version import play.api.libs.concurrent._ object TimeoutExampleScala extends Controller { def index(url: String, timeout: Long) = Action { val timeout = Promise.timeout(null, timeout, MILLISECONDS) val future = timeout.either(WS.url(url).get()) future.map { case Right(response) => // Use the response to build the page case _ => // Hit a timeout, build the page without the response } } }
  175. 175. Outline 1. A quick intro to Play 2. Basic async code 3. map and flatMap 4. Parallel and sequential 5. Errors and timeouts 6. Coming soon
  176. 176. play-async-plugin We'll be open sourcing this plugin soon
  177. 177. It is a collection of async utilities, including...
  178. 178. ParSeq integration for Play to make asynchronous Java code easier Task<Response> yahoo = Tasks.wrap(WS.url("http://www.yahoo.com")) Task<Response> google = Tasks.wrap(WS.url("http://www.google.com")) Task<Response> bing = Tasks.wrap(WS.url("http://www.bing.com")) // Create a new Task that will run the tasks above one at a time, // in the order specified, and complete with the return value of the // last one Task<Response> sequential = Tasks.seq(yahoo, google, bing)
  179. 179. Config-driven SLAs (timeouts) for any async I/O sla.plugin.slas = [ { resources: ["/profile", "/companies"] timeout: "2s" }, { resources: ["/pymk", "/skills/*"], timeout: "500ms" } ]
  180. 180. In-browser visualization of all async requests for a page, including timing, responses, and errors
  181. 181. We're just getting started with Play! We'll be sharing more as we go.
  182. 182. LinkedIn Engineering Blog http://engineering.linkedin.com
  183. 183. @LinkedInEng on Twitter https://twitter.com/LinkedInEng
  184. 184. Thank you!

×