Skip to content

Configurable per query or per request context/dataloader batching #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,10 @@ Here's an example of a GraphQL provider that implements three interfaces at the

* [ExampleGraphQLProvider](examples/osgi/providers/src/main/java/graphql/servlet/examples/osgi/ExampleGraphQLProvider.java)

## Request-scoped DataLoaders
## Context and DataLoader settings

It is possible to use dataloaders in a request scope by customizing [GraphQLContextBuilder](https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/GraphQLContextBuilder.java).
And instantiating a new [DataLoaderRegistry](https://github.com/graphql-java/java-dataloader/blob/master/src/main/java/org/dataloader/DataLoaderRegistry.java) for each GraphQLContext.
It is possible to create context, and consequently dataloaders, in both a request scope and a per query scope by customizing [GraphQLContextBuilder](https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/context/GraphQLContextBuilder.java) and selecting the appropriate [ContextSetting](https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/context/ContextSetting.java) with the provided [GraphQLConfiguration](https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/config/GraphQLConfiguration.java).
A new [DataLoaderRegistry](https://github.com/graphql-java/java-dataloader/blob/master/src/main/java/org/dataloader/DataLoaderRegistry.java) should be created in each call to the GraphQLContextBuilder, and the servlet will call the builder at the appropriate times.
For eg:
```java
public class CustomGraphQLContextBuilder implements GraphQLContextBuilder {
Expand All @@ -249,26 +249,17 @@ public class CustomGraphQLContextBuilder implements GraphQLContextBuilder {

@Override
public GraphQLContext build(HttpServletRequest req) {
GraphQLContext context = new GraphQLContext(req);
context.setDataLoaderRegistry(buildDataLoaderRegistry());

return context;
return new GraphQLContext(buildDataLoaderRegistry());
}

@Override
public GraphQLContext build() {
GraphQLContext context = new GraphQLContext();
context.setDataLoaderRegistry(buildDataLoaderRegistry());

return context;
return new GraphQLContext(buildDataLoaderRegistry());
}

@Override
public GraphQLContext build(HandshakeRequest request) {
GraphQLContext context = new GraphQLContext(request);
context.setDataLoaderRegistry(buildDataLoaderRegistry());

return context;
return new GraphQLContext(buildDataLoaderRegistry());
}

private DataLoaderRegistry buildDataLoaderRegistry() {
Expand All @@ -290,3 +281,6 @@ public class CustomGraphQLContextBuilder implements GraphQLContextBuilder {
}

```
If per request is selected this will cause all queries within the http request, if using a batch, to share dataloader caches and batch together load calls as efficently as possible. The dataloaders are dispatched using instrumentation and the correct instrumentation will be selected according to the ContextSetting. The default context setting in GraphQLConfiguration is per query.

Two additional context settings are provided, one for each of the previous settings but without the addition of the Dataloader dispatching instrumentation. This is useful for those not using Dataloaders or wanting to supply their own dispatching instrumentation though the instrumentation supplier within the GraphQLQueryInvoker.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version = 7.5.1-SNAPSHOT
group = com.graphql-java-kickstart

LIB_GRAPHQL_JAVA_VER = 12.0
LIB_GRAPHQL_JAVA_VER = 13.0
LIB_JACKSON_VER = 2.9.8
94 changes: 70 additions & 24 deletions src/main/java/graphql/servlet/AbstractGraphQLHttpServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@
import graphql.ExecutionResult;
import graphql.introspection.IntrospectionQuery;
import graphql.schema.GraphQLFieldDefinition;
import graphql.servlet.internal.GraphQLRequest;
import graphql.servlet.internal.VariableMapper;
import graphql.servlet.core.GraphQLMBean;
import graphql.servlet.core.GraphQLObjectMapper;
import graphql.servlet.core.GraphQLQueryInvoker;
import graphql.servlet.core.GraphQLServletListener;
import graphql.servlet.config.GraphQLConfiguration;
import graphql.servlet.context.ContextSetting;
import graphql.servlet.input.BatchInputPreProcessResult;
import graphql.servlet.input.BatchInputPreProcessor;
import graphql.servlet.input.GraphQLBatchedInvocationInput;
import graphql.servlet.input.GraphQLSingleInvocationInput;
import graphql.servlet.input.GraphQLInvocationInputFactory;
import graphql.servlet.core.internal.GraphQLRequest;
import graphql.servlet.core.internal.VariableMapper;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
Expand All @@ -30,6 +41,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand All @@ -46,13 +58,13 @@
*/
public abstract class AbstractGraphQLHttpServlet extends HttpServlet implements Servlet, GraphQLMBean {

public static final Logger log = LoggerFactory.getLogger(AbstractGraphQLHttpServlet.class);
private static final Logger log = LoggerFactory.getLogger(AbstractGraphQLHttpServlet.class);

public static final String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8";
public static final String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8";
public static final String APPLICATION_GRAPHQL = "application/graphql";
public static final int STATUS_OK = 200;
public static final int STATUS_BAD_REQUEST = 400;
private static final String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8";
private static final String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8";
private static final String APPLICATION_GRAPHQL = "application/graphql";
private static final int STATUS_OK = 200;
private static final int STATUS_BAD_REQUEST = 400;

private static final GraphQLRequest INTROSPECTION_REQUEST = new GraphQLRequest(IntrospectionQuery.INTROSPECTION_QUERY, new HashMap<>(), null);
private static final String[] MULTIPART_KEYS = new String[]{"operations", "graphql", "query"};
Expand Down Expand Up @@ -123,13 +135,17 @@ public void init() {
path = request.getServletPath();
}
if (path.contentEquals("/schema.json")) {
query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(INTROSPECTION_REQUEST, request, response), response);
query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(INTROSPECTION_REQUEST, request, response),
request, response);
} else {
String query = request.getParameter("query");
if (query != null) {

if (isBatchedQuery(query)) {
queryBatched(queryInvoker, graphQLObjectMapper, invocationInputFactory.createReadOnly(graphQLObjectMapper.readBatchedGraphQLRequest(query), request, response), response);
List<GraphQLRequest> requests = graphQLObjectMapper.readBatchedGraphQLRequest(query);
GraphQLBatchedInvocationInput batchedInvocationInput =
invocationInputFactory.createReadOnly(configuration.getContextSetting(), requests, request, response);
queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration);
} else {
final Map<String, Object> variables = new HashMap<>();
if (request.getParameter("variables") != null) {
Expand All @@ -138,7 +154,9 @@ public void init() {

String operationName = request.getParameter("operationName");

query(queryInvoker, graphQLObjectMapper, invocationInputFactory.createReadOnly(new GraphQLRequest(query, variables, operationName), request, response), response);
query(queryInvoker, graphQLObjectMapper,
invocationInputFactory.createReadOnly(new GraphQLRequest(query, variables, operationName), request, response),
request, response);
}
} else {
response.setStatus(STATUS_BAD_REQUEST);
Expand All @@ -155,7 +173,9 @@ public void init() {
try {
if (APPLICATION_GRAPHQL.equals(request.getContentType())) {
String query = CharStreams.toString(request.getReader());
query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(new GraphQLRequest(query, null, null), request, response), response);
query(queryInvoker, graphQLObjectMapper,
invocationInputFactory.create(new GraphQLRequest(query, null, null), request, response),
request, response);
} else if (request.getContentType() != null && request.getContentType().startsWith("multipart/form-data") && !request.getParts().isEmpty()) {
final Map<String, List<Part>> fileItems = request.getParts()
.stream()
Expand All @@ -182,10 +202,9 @@ public void init() {
List<GraphQLRequest> graphQLRequests =
graphQLObjectMapper.readBatchedGraphQLRequest(inputStream);
variablesMap.ifPresent(map -> graphQLRequests.forEach(r -> mapMultipartVariables(r, map, fileItems)));
GraphQLBatchedInvocationInput invocationInput =
invocationInputFactory.create(graphQLRequests, request, response);
invocationInput.getContext().setParts(fileItems);
queryBatched(queryInvoker, graphQLObjectMapper, invocationInput, response);
GraphQLBatchedInvocationInput batchedInvocationInput = invocationInputFactory.create(configuration.getContextSetting(),
graphQLRequests, request, response);
queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration);
return;
} else {
GraphQLRequest graphQLRequest;
Expand All @@ -198,8 +217,7 @@ public void init() {
variablesMap.ifPresent(m -> mapMultipartVariables(graphQLRequest, m, fileItems));
GraphQLSingleInvocationInput invocationInput =
invocationInputFactory.create(graphQLRequest, request, response);
invocationInput.getContext().setParts(fileItems);
query(queryInvoker, graphQLObjectMapper, invocationInput, response);
query(queryInvoker, graphQLObjectMapper, invocationInput, request, response);
return;
}
}
Expand All @@ -211,9 +229,12 @@ public void init() {
InputStream inputStream = asMarkableInputStream(request.getInputStream());

if (isBatchedQuery(inputStream)) {
queryBatched(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(graphQLObjectMapper.readBatchedGraphQLRequest(inputStream), request, response), response);
List<GraphQLRequest> requests = graphQLObjectMapper.readBatchedGraphQLRequest(inputStream);
GraphQLBatchedInvocationInput batchedInvocationInput =
invocationInputFactory.create(configuration.getContextSetting(), requests, request, response);
queryBatched(queryInvoker, batchedInvocationInput, request, response, configuration);
} else {
query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(graphQLObjectMapper.readGraphQLRequest(inputStream), request, response), response);
query(queryInvoker, graphQLObjectMapper, invocationInputFactory.create(graphQLObjectMapper.readGraphQLRequest(inputStream), request, response), request, response);
}
}
} catch (Exception e) {
Expand Down Expand Up @@ -348,18 +369,21 @@ private Optional<Part> getFileItem(Map<String, List<Part>> fileItems, String nam
return Optional.ofNullable(fileItems.get(name)).filter(list -> !list.isEmpty()).map(list -> list.get(0));
}

private void query(GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, GraphQLSingleInvocationInput invocationInput, HttpServletResponse resp) throws IOException {
private void query(GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, GraphQLSingleInvocationInput invocationInput,
HttpServletRequest req, HttpServletResponse resp) throws IOException {
ExecutionResult result = queryInvoker.query(invocationInput);

if (!(result.getData() instanceof Publisher)) {
resp.setContentType(APPLICATION_JSON_UTF8);
resp.setStatus(STATUS_OK);
resp.getWriter().write(graphQLObjectMapper.serializeResultAsJson(result));
} else {
if (req == null) {
throw new IllegalStateException("Http servlet request can not be null");
}
resp.setContentType(APPLICATION_EVENT_STREAM_UTF8);
resp.setStatus(STATUS_OK);

HttpServletRequest req = invocationInput.getContext().getHttpServletRequest().orElseThrow(IllegalStateException::new);
boolean isInAsyncThread = req.isAsyncStarted();
AsyncContext asyncContext = isInAsyncThread ? req.getAsyncContext() : req.startAsync(req, resp);
asyncContext.setTimeout(configuration.getSubscriptionTimeout());
Expand All @@ -378,8 +402,30 @@ private void query(GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQL
}
}

private void queryBatched(GraphQLQueryInvoker queryInvoker, GraphQLObjectMapper graphQLObjectMapper, GraphQLBatchedInvocationInput invocationInput, HttpServletResponse resp) throws Exception {
queryInvoker.query(invocationInput, resp, graphQLObjectMapper);
private void queryBatched(GraphQLQueryInvoker queryInvoker, GraphQLBatchedInvocationInput batchedInvocationInput, HttpServletRequest request,
HttpServletResponse response, GraphQLConfiguration configuration) throws IOException {
BatchInputPreProcessor batchInputPreProcessor = configuration.getBatchInputPreProcessor();
ContextSetting contextSetting = configuration.getContextSetting();
BatchInputPreProcessResult batchInputPreProcessResult = batchInputPreProcessor.preProcessBatch(batchedInvocationInput, request, response);
if (batchInputPreProcessResult.isExecutable()) {
List<ExecutionResult> results = queryInvoker.query(batchInputPreProcessResult.getBatchedInvocationInput().getExecutionInputs(),
contextSetting);
response.setContentType(AbstractGraphQLHttpServlet.APPLICATION_JSON_UTF8);
response.setStatus(AbstractGraphQLHttpServlet.STATUS_OK);
Writer writer = response.getWriter();
Iterator<ExecutionResult> executionInputIterator = results.iterator();
writer.write("[");
GraphQLObjectMapper graphQLObjectMapper = configuration.getObjectMapper();
while (executionInputIterator.hasNext()) {
writer.write(graphQLObjectMapper.serializeResultAsJson(executionInputIterator.next()));
if (executionInputIterator.hasNext()) {
writer.write(",");
}
}
writer.write("]");
} else {
response.sendError(batchInputPreProcessResult.getStatusCode(), batchInputPreProcessResult.getStatusMessage());
}
}

private <R> List<R> runListeners(Function<? super GraphQLServletListener, R> action) {
Expand Down
25 changes: 0 additions & 25 deletions src/main/java/graphql/servlet/BatchExecutionHandler.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package graphql.servlet;

import graphql.servlet.config.GraphQLConfiguration;

import java.util.Objects;

class ConfiguredGraphQLHttpServlet extends GraphQLHttpServlet {
Expand Down
37 changes: 0 additions & 37 deletions src/main/java/graphql/servlet/DefaultBatchExecutionHandler.java

This file was deleted.

4 changes: 4 additions & 0 deletions src/main/java/graphql/servlet/DefaultGraphQLServlet.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package graphql.servlet;

import graphql.servlet.core.GraphQLObjectMapper;
import graphql.servlet.core.GraphQLQueryInvoker;
import graphql.servlet.input.GraphQLInvocationInputFactory;

public class DefaultGraphQLServlet extends AbstractGraphQLHttpServlet {

@Override
Expand Down
30 changes: 0 additions & 30 deletions src/main/java/graphql/servlet/GraphQLBatchedInvocationInput.java

This file was deleted.

Loading