Skip to main content

GraphQL Java and Threads

· 4 min read
Andreas Marek

We follow a fundamental rule in GraphQL Java regarding Threads: GraphQL Java never creates Threads or interacts with Thread pools. We do this because we want to give the user the full control and whatever GraphQL Java would do, it would not be correct for every use case.

Additionally to being strictly unopinionated regarding Threads, GraphQL Java is also fully reactive, implemented via CompletableFuture (CF). These two constrain together mean we rely on the CF returned by the user. Specifically we piggyback on the CF returned by the DataFetcher (or other async methods which can be implemented by the user, but we focus here on DataFetcher as it is by far the most important).

// Pseudo code in GraphQL Java

CompletableFuture<Object> dataFetcherResult = invokeDataFetcher();
dataFetcherResult.thenApply(result -> {
// in which Thread where this code happens is controlled by the CF returned
continueExecutingQuery(result);
});

Blocking DataFetcher

Lets assume you are accessing a DB in a blocking way in your DataFetcher:

String get(DataFetchingEnvironment env) {
return getValueFromDb(env); // blocking the Thread until the value is read from DB
};

This is not completely wrong, but not recommend in general as the consequence of this kind of DataFecher is that GraphQL can't execute the query in the most efficient way.

For example for the following query:

{
dbData1
dbData2
dbData3
}

If the DataFetcher for these dbData fields don't return a CF, but block the Thread until the data is read, GraphQL Java will not work with maximum efficiency.

GraphQL Java can invoke the DataFetcher for all three fields in parallel. But if your DataFetcher for dbData1 is blocking, GraphQL Java will also be blocked and only invoke the next DataFetcher once dbData<n> is finished. The recommend solution to this problem is offloading your blocking code onto a separate Thread pool as shown here:

CompletableFuture<String> get(DataFetchingEnvironment env) {
return CompletableFuture.supplyAsync( getValueFromDb(env), dbThreadPool );
};

This code will maximize the performance and will cause all three fields to be fetched in parallel.

Different pools for different work

The subsequent work done by GraphQL Java will be executed in the same dbThreadPool until it encounters a new DataFetcher returned by the user code and this new CF dedicates the Thread for the subsequent work.

If you want to have separate pools for different kind of work, one for the actual DataFetcher which normally involve IO and one of the actual GraphQL Java work (which is pure CPU), you need to switch back from your offloaded pool to a dedicated GraphQL Java pool before returning the CF. You can achieve this with code like this:

CompletableFuture<String> get(DataFetchingEnvironment env) {
return CompletableFuture.supplyAsync( getValueFromDb(env), dbThreadPool )
.handleAsync((result,exception) -> {
if(exception !=null) throw exception;
return result;
}, graphqlJavaPool);
};

Notice the .handleAsync which doesn't do anything except forwarding the result, but on a different pool (graphqlJavaPool).

This way you have different pools for different kind of work (one for CPU bound GraphQL Java work and one for multiple ones for IO bound work), which can be configured and monitored independently.

In a fully reactive system

If your system is fully reactive your DataFetcher will more look like this

CompletableFuture<String> get(DataFetchingEnvironment env) {
return callAnotherServiceNonBlocking(env); // returns CompletableFuture
};

The code above could be implemented via Async Http Client or WebFlux WebClient. Both provide fully reactive HTTP clients.

Because the code is non blocking there is no need to offload anything on a dedicated Thread pool to avoid blocking GraphQL Java.

You still might want to consider using a dedicated GraphQL Java pool as you otherwise would use Threads which are dedicated to IO. How much this is really relevant depends highly on your use case.

For example Async Http Client (AHC) uses by default 2 * #cores (this value comes actually from Netty) Threads. If you don't use a dedicated Thread Pool for GraphQL Java you might encounter situations under load where all AHC Threads are either busy or blocked by GraphQL Java code and as a result your system is not as performant as it could be. Normally only load tests in production like environments can show the relevance of different Thread pools.

Feedback or questions

We use GitHub Discussions for general feedback and questions.