Play Framework 2.5 JavaAsync throwing CompletionException
Asked Answered
B

2

8

I'm using Play 2.5 to build a simple app. For better performance I'm using Akka chunked response with Java 8 CompletionStage strategy. Below is the code by which chunked response is getting generated(it's working fine when not using ComperableFuture):

@Singleton
public class AbstractSource {

    public Source<ByteString, ?> getChunked(String html) {

        return Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
                .mapMaterializedValue(sourceActor -> {
                    sourceActor.tell(ByteString.fromString(html), null);
                    sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
                    return null;
                });

    }

}

And here is my controller:

@Singleton
@AddCSRFToken
public class Application extends Controller {

    @Inject
    private AbstractSource abstractSource;

    public CompletionStage<Result> index() {


        CompletionStage<Source<ByteString, ?>> source = CompletableFuture.supplyAsync(() -> 
                                                  abstractSource.getChunked(index.render(CSRF.getToken(request()).map(t -> 
                                                    t.value()).orElse("no token")).body()
                                                   )
                                                );

        return source.thenApply( chunks -> ok().chunked(chunks));

    }

}

Now when I'm running the app it's throwing following exception:

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[CompletionException: java.lang.RuntimeException: There is no HTTP Context available from here.]]
    at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:269)
    at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:195)
    at play.api.GlobalSettings$class.onError(GlobalSettings.scala:160)
    at play.api.DefaultGlobal$.onError(GlobalSettings.scala:188)
    at play.api.http.GlobalSettingsHttpErrorHandler.onServerError(HttpErrorHandler.scala:98)
    at play.core.server.netty.PlayRequestHandler$$anonfun$2$$anonfun$apply$1.applyOrElse(PlayRequestHandler.scala:99)
    at play.core.server.netty.PlayRequestHandler$$anonfun$2$$anonfun$apply$1.applyOrElse(PlayRequestHandler.scala:98)
    at scala.concurrent.Future$$anonfun$recoverWith$1.apply(Future.scala:344)
    at scala.concurrent.Future$$anonfun$recoverWith$1.apply(Future.scala:343)
    at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32)
Caused by: java.util.concurrent.CompletionException: java.lang.RuntimeException: There is no HTTP Context available from here.
    at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
    at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1592)
    at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException: There is no HTTP Context available from here.
    at play.mvc.Http$Context.current(Http.java:57)
    at play.mvc.Controller.request(Controller.java:36)
    at com.mabsisa.ui.web.controllers.Application.lambda$index$1(Application.java:31)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
    ... 5 common frames omitted

I'm not using HTTP context anywhere, so why this is not working I'm not getting. Same code is working when returning normal Result with chunked response. Please help with this

Bicentenary answered 26/3, 2016 at 14:7 Comment(0)
A
14

You have to supply the HTTP execution context when dealing with CompletableFuture / CompletionStage. In Scala the context information is passed via implicits, these are not available in Java - this is why Play uses ThreadLocal.

However you can lose this information when switching threads and that is why you have the problem. You may think that you don't access the HTTP context but actually you do - you are using request().

So you have to change your code to use supplyAsync with an Executor:

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html#supplyAsync-java.util.function.Supplier-java.util.concurrent.Executor-

From this:

CompletableFuture.supplyAsync(() -> abstractSource.getChunked(index.render(CSRF.getToken(request()).map(t -> 
                                                    t.value()).orElse("no token")).body()
                                                   )
                                                );

to this:

CompletableFuture.supplyAsync(() -> abstractSource.getChunked(index.render(CSRF.getToken(request()).map(t -> 
                                                    t.value()).orElse("no token")).body()
                                                   )
                                                , ec.current());

where ec is your context: @Inject HttpExecutionContext ec;

Abominate answered 1/4, 2016 at 9:44 Comment(6)
awsome answer with good explanation......one more thing want to ask if this is a good design approach in play or not? cn you please share your opinion?Bicentenary
Returning a CompletionStage<Result> is the de facto standard to define async actions in Play 2.5, so you are fine here. Regarding chunked responses in Play: take a look at playframework.com/documentation/2.5.x/… - by using Akka streams you are already on a good track so don't worry here either - your design approach is good!Abominate
Glad that I could help!Abominate
This is filed as github.com/playframework/playframework/pull/6452 and there is documentation here playframework.com/documentation/2.5.x/…Soinski
Good answer, but if i correctly understand you pass Default Play Executor to CompletableFuture this cause block request executor if you do heavy task in your asyncRetread
@MojtabaAsg this is absolutely correct. Look at the answer as a guidance. If you intend to do heavy computation it might be good idea to provide another execution context which you tune according to your needs.Abominate
L
1

I addition to Anton's answer.

If you are building a non-blocking app using Play Java API, it might become quite cumbersome to inject HttpExecutionContext and pass ec.current()) every time you need to call methods on CompletionStage.

To make life easier you can use a decorator, which will preserve the context between calls.

public class ContextPreservingCompletionStage<T> implements CompletionStage<T> {

    private HttpExecutionContext context;
    private CompletionStage<T> delegate;

    public ContextPreservingCompletionStage(CompletionStage<T> delegate,
                                            HttpExecutionContext context) {
        this.delegate = delegate;
        this.context = context;
    }
    ...
}

So you will need to pass context only once:

return new ContextPreservingCompletionStage<>(someCompletableFuture, context)
                            .thenCompose(something -> {...});
                            .thenApply(something -> {...});

Instead of

return someCompletableFuture.thenComposeAsync(something -> {...}, context.current())
                            .thenApplyAsync(something -> {...}, context.current());

That is particularly useful if you are building a multi-tier app, and passing CompletionStages between different classes.

Full decorator implementation example is here.

Ledaledah answered 14/1, 2018 at 17:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.