hackmabrain
hackmabrain

Reputation: 477

Play 2.5 preserve context in async calls

In our controller class we reach out to another service to get some data :

Future<JsonNode> futureSite = someClient.getSite(siteId, queryParams);

return FutureConverters.toJava(futureSite).thenApplyAsync((siteJson) -> {
    Site site = Json.fromJson(siteJson, Site.class);
    try {
        return function.apply(site);
    } catch (RequestException e) {
        return e.result;
    }
}).exceptionally(throwable -> {
    if(throwable instanceof OurClientException) {
        if(((OurClientException) throwable).httpStatusCode == 404) {
           return entityNotFound("Site", siteId);
        }
    }
    return null;
});

What we notice is that context which is set in unit tests (we use scalatest-play) is lost and becomes null after we make the Async call (FutureConverters.toJava(futureSite).thenApplyAsync((siteJson), as t is on a separate thread.

Which causes problem down in the controller code, where we use the above function ... request() would now throw a runtime exception saying there is no context available.

How can we preserve the context ?

Upvotes: 1

Views: 503

Answers (2)

Yury Kisliak
Yury Kisliak

Reputation: 439

I addition to Nick's V 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.

Upvotes: 0

Nick V
Nick V

Reputation: 1671

You should inject play.libs.concurrent.HttpExecutionContext to your controller and then specify current context as second argument for CompletionStage#thenApplyAsync(..,..).

public class Application extends Controller {
@Inject HttpExecutionContext ec;

public CompletionStage<Result> index() {
    someCompletableFuture.supplyAsync(() -> { 
      // do something with request()
    }, ec.current());
}}

P.S. https://www.playframework.com/documentation/2.5.x/JavaAsync#Using-CompletionStage-inside-an-Action

Upvotes: 3

Related Questions