§Handling asynchronous results
§Make controllers asynchronous
Internally, Play Framework is asynchronous from the bottom up. Play handles every request in an asynchronous, non-blocking way.
The default configuration is tuned for asynchronous controllers. In other words, the application code should avoid blocking in controllers, i.e., having the controller code wait for an operation. Common examples of such blocking operations are JDBC calls, streaming API, HTTP requests and long computations.
Although it’s possible to increase the number of threads in the default execution context to allow more concurrent requests to be processed by blocking controllers, following the recommended approach of keeping the controllers asynchronous makes it easier to scale and to keep the system responsive under load.
§Creating non-blocking actions
Because of the way Play works, action code must be as fast as possible, i.e., non-blocking. So what should we return from our action if we are not yet able to compute the result? We should return the promise of a result!
Java 8 provides a generic promise API called CompletionStage
. A CompletionStage<Result>
will eventually be redeemed with a value of type Result
. By using a CompletionStage<Result>
instead of a normal Result
, we are able to return from our action quickly without blocking anything. Play will then serve the result as soon as the promise is redeemed.
§How to create a CompletionStage<Result>
To create a CompletionStage<Result>
we need another promise first: the promise that will give us the actual value we need to compute the result:
CompletionStage<Double> promiseOfPIValue = computePIAsynchronously();
// Runs in same thread
CompletionStage<Result> promiseOfResult =
promiseOfPIValue.thenApply(pi -> ok("PI value computed: " + pi));
Play asynchronous API methods give you a CompletionStage
. This is the case when you are calling an external web service using the play.libs.WS
API, or if you are using Akka to schedule asynchronous tasks or to communicate with Actors using play.libs.Akka
.
In this case, using CompletionStage.thenApply
will execute the completion stage in the same calling thread as the previous task. This is fine when you have a small amount of CPU bound logic with no blocking.
A simple way to execute a block of code asynchronously and to get a CompletionStage
is to use the CompletableFuture.supplyAsync()
method:
// creates new task
CompletionStage<Integer> promiseOfInt =
CompletableFuture.supplyAsync(() -> intensiveComputation());
Using supplyAsync
creates a new task which will be placed on the fork join pool, and may be called from a different thread – although, here it’s using the default executor, and in practice you will specify an executor explicitly.
Only the “*Async” methods from
CompletionStage
provide asynchronous execution.
§Using HttpExecutionContext
You must supply the HTTP execution context explicitly as an executor when using a Java CompletionStage
inside an Action, to ensure that the classloader remains in scope.
You can supply the play.libs.concurrent.HttpExecutionContext
instance through dependency injection:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.HttpExecutionContext;
import play.mvc.*;
public class MyController extends Controller {
private HttpExecutionContext httpExecutionContext;
@Inject
public MyController(HttpExecutionContext ec) {
this.httpExecutionContext = ec;
}
public CompletionStage<Result> index() {
// Use a different task with explicit EC
return calculateResponse()
.thenApplyAsync(
answer -> {
return ok("answer was " + answer).flashing("info", "Response updated!");
},
httpExecutionContext.current());
}
private static CompletionStage<String> calculateResponse() {
return CompletableFuture.completedFuture("42");
}
}
Please see Class loaders for more information on using HttpExecutionContext
.
§Using CustomExecutionContext and HttpExecution
Using a CompletionStage
or an HttpExecutionContext
is only half of the picture though! At this point you are still on Play’s default ExecutionContext. If you are calling out to a blocking API such as JDBC, then you still will need to have your ExecutionStage run with a different executor, to move it off Play’s rendering thread pool. You can do this by creating a subclass of play.libs.concurrent.CustomExecutionContext
with a reference to the custom dispatcher.
Add the following imports:
import play.libs.concurrent.HttpExecution;
import javax.inject.Inject;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletionStage;
import static java.util.concurrent.CompletableFuture.supplyAsync;
Define a custom execution context:
public class MyExecutionContext extends CustomExecutionContext {
@Inject
public MyExecutionContext(ActorSystem actorSystem) {
// uses a custom thread pool defined in application.conf
super(actorSystem, "my.dispatcher");
}
}
You will need to define a custom dispatcher in application.conf
, which is done through Akka dispatcher configuration.
Once you have the custom dispatcher, add in the explicit executor and wrap it with HttpException.fromThread
:
public class Application extends Controller {
private MyExecutionContext myExecutionContext;
@Inject
public Application(MyExecutionContext myExecutionContext) {
this.myExecutionContext = myExecutionContext;
}
public CompletionStage<Result> index() {
// Wrap an existing thread pool, using the context from the current thread
Executor myEc = HttpExecution.fromThread(myExecutionContext);
return supplyAsync(() -> intensiveComputation(), myEc)
.thenApplyAsync(i -> ok("Got result: " + i), myEc);
}
public int intensiveComputation() {
return 2;
}
}
You can’t magically turn synchronous IO into asynchronous by wrapping it in a
CompletionStage
. If you can’t change the application’s architecture to avoid blocking operations, at some point that operation will have to be executed, and that thread is going to block. So in addition to enclosing the operation in aCompletionStage
, it’s necessary to configure it to run in a separate execution context that has been configured with enough threads to deal with the expected concurrency. See Understanding Play thread pools for more information, and download the play example templates that show database integration.
§Actions are asynchronous by default
Play actions are asynchronous by default. For instance, in the controller code below, the returned Result
is internally enclosed in a promise:
public Result index(Http.Request request) {
return ok("Got request " + request + "!");
}
Note: Whether the action code returns a
Result
or aCompletionStage<Result>
, both kinds of returned object are handled internally in the same way. There is a single kind ofAction
, which is asynchronous, and not two kinds (a synchronous one and an asynchronous one). Returning aCompletionStage
is a technique for writing non-blocking code.
§Handling time-outs
It is often useful to handle time-outs properly, to avoid having the web browser block and wait if something goes wrong. You can use the play.libs.concurrent.Futures.timeout
method to wrap a CompletionStage
in a non-blocking timeout.
class MyClass {
private final Futures futures;
private final Executor customExecutor = ForkJoinPool.commonPool();
@Inject
public MyClass(Futures futures) {
this.futures = futures;
}
CompletionStage<Double> callWithOneSecondTimeout() {
return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
}
public CompletionStage<String> delayedResult() {
long start = System.currentTimeMillis();
return futures.delayed(
() ->
CompletableFuture.supplyAsync(
() -> {
long end = System.currentTimeMillis();
long seconds = end - start;
return "rendered after " + seconds + " seconds";
},
customExecutor),
Duration.of(3, SECONDS));
}
}
Note: Timeout is not the same as cancellation – even in case of timeout, the given future will still complete, even though that completed value is not returned.
Next: Streaming HTTP responses
Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request. Have questions or advice to share? Go to our community forums to start a conversation with the community.