Skip to content

Commit 6dad9c6

Browse files
authored
Merge pull request #268 from olim7t/async
Make HttpRequestHandlerImpl.handle() async when possible (fixes #259)
2 parents befcdd1 + fb087e1 commit 6dad9c6

File tree

8 files changed

+114
-70
lines changed

8 files changed

+114
-70
lines changed

.github/workflows/pull-request.yml

+7
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,34 @@ jobs:
5353
name: Sonar analysis
5454
needs: validation
5555
runs-on: ubuntu-latest
56+
env:
57+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
5658
steps:
5759
- uses: actions/checkout@v2
60+
if: env.SONAR_TOKEN != null
5861
with:
5962
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
6063
- name: Set up JDK 11
64+
if: env.SONAR_TOKEN != null
6165
uses: actions/setup-java@v1
6266
with:
6367
java-version: 11
6468
- name: Cache SonarCloud packages
69+
if: env.SONAR_TOKEN != null
6570
uses: actions/cache@v1
6671
with:
6772
path: ~/.sonar/cache
6873
key: ${{ runner.os }}-sonar
6974
restore-keys: ${{ runner.os }}-sonar
7075
- name: Cache Gradle packages
76+
if: env.SONAR_TOKEN != null
7177
uses: actions/cache@v1
7278
with:
7379
path: ~/.gradle/caches
7480
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
7581
restore-keys: ${{ runner.os }}-gradle
7682
- name: Build and analyze
83+
if: env.SONAR_TOKEN != null
7784
env:
7885
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
7986
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLInvoker.java

+24-13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput;
99
import graphql.kickstart.execution.input.GraphQLInvocationInput;
1010
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput;
11+
import java.util.ArrayList;
1112
import java.util.List;
1213
import java.util.concurrent.CompletableFuture;
1314
import lombok.AllArgsConstructor;
@@ -28,26 +29,36 @@ public CompletableFuture<ExecutionResult> executeAsync(
2829
}
2930

3031
public GraphQLQueryResult query(GraphQLInvocationInput invocationInput) {
32+
return queryAsync(invocationInput).join();
33+
}
34+
35+
public CompletableFuture<GraphQLQueryResult> queryAsync(GraphQLInvocationInput invocationInput) {
3136
if (invocationInput instanceof GraphQLSingleInvocationInput) {
32-
return GraphQLQueryResult.create(query((GraphQLSingleInvocationInput) invocationInput));
37+
return executeAsync((GraphQLSingleInvocationInput)invocationInput).thenApply(GraphQLQueryResult::create);
3338
}
3439
GraphQLBatchedInvocationInput batchedInvocationInput = (GraphQLBatchedInvocationInput) invocationInput;
35-
return GraphQLQueryResult.create(query(batchedInvocationInput));
40+
return executeAsync(batchedInvocationInput).thenApply(GraphQLQueryResult::create);
3641
}
3742

38-
private ExecutionResult query(GraphQLSingleInvocationInput singleInvocationInput) {
39-
return executeAsync(singleInvocationInput).join();
43+
private CompletableFuture<List<ExecutionResult>> executeAsync(GraphQLBatchedInvocationInput batchedInvocationInput) {
44+
GraphQL graphQL = batchedDataLoaderGraphQLBuilder.newGraphQL(batchedInvocationInput, graphQLBuilder);
45+
return sequence(
46+
batchedInvocationInput.getExecutionInputs().stream()
47+
.map(executionInput -> proxy.executeAsync(graphQL, executionInput))
48+
.collect(toList()));
4049
}
4150

42-
private List<ExecutionResult> query(GraphQLBatchedInvocationInput batchedInvocationInput) {
43-
GraphQL graphQL = batchedDataLoaderGraphQLBuilder
44-
.newGraphQL(batchedInvocationInput, graphQLBuilder);
45-
return batchedInvocationInput.getExecutionInputs().stream()
46-
.map(executionInput -> proxy.executeAsync(graphQL, executionInput))
47-
.collect(toList())
48-
.stream()
49-
.map(CompletableFuture::join)
50-
.collect(toList());
51+
@SuppressWarnings({"unchecked", "rawtypes"})
52+
private <T> CompletableFuture<List<T>> sequence(List<CompletableFuture<T>> futures) {
53+
CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[0]);
54+
return CompletableFuture.allOf(futuresArray).thenApply(aVoid -> {
55+
List<T> result = new ArrayList<>(futures.size());
56+
for (CompletableFuture future : futuresArray) {
57+
assert future.isDone(); // per the API contract of allOf()
58+
result.add((T) future.join());
59+
}
60+
return result;
61+
});
5162
}
5263
}
5364

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/AbstractGraphQLHttpServlet.java

+13-35
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.function.Consumer;
1919
import java.util.function.Function;
2020
import java.util.stream.Collectors;
21-
import javax.servlet.AsyncContext;
2221
import javax.servlet.Servlet;
2322
import javax.servlet.http.HttpServlet;
2423
import javax.servlet.http.HttpServletRequest;
@@ -135,59 +134,38 @@ public String executeQuery(String query) {
135134
}
136135
}
137136

138-
private void doRequestAsync(HttpServletRequest request, HttpServletResponse response,
139-
HttpRequestHandler handler) {
140-
if (configuration.isAsyncServletModeEnabled()) {
141-
AsyncContext asyncContext = request.startAsync(request, response);
142-
asyncContext.setTimeout(configuration.getAsyncTimeout());
143-
HttpServletRequest asyncRequest = (HttpServletRequest) asyncContext.getRequest();
144-
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
145-
configuration.getAsyncExecutor()
146-
.execute(() -> doRequest(asyncRequest, asyncResponse, handler, asyncContext));
147-
} else {
148-
doRequest(request, response, handler, null);
149-
}
137+
@Override
138+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
139+
doRequest(req, resp);
150140
}
151141

152-
private void doRequest(HttpServletRequest request, HttpServletResponse response,
153-
HttpRequestHandler handler,
154-
AsyncContext asyncContext) {
142+
@Override
143+
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
144+
doRequest(req, resp);
145+
}
155146

147+
private void doRequest(HttpServletRequest request, HttpServletResponse response) {
148+
init();
156149
List<GraphQLServletListener.RequestCallback> requestCallbacks = runListeners(
157150
l -> l.onRequest(request, response));
158151

159152
try {
160-
handler.handle(request, response);
153+
requestHandler.handle(request, response);
161154
runCallbacks(requestCallbacks, c -> c.onSuccess(request, response));
162-
} catch (Throwable t) {
155+
} catch (Exception t) {
163156
log.error("Error executing GraphQL request!", t);
164157
runCallbacks(requestCallbacks, c -> c.onError(request, response, t));
165158
} finally {
166159
runCallbacks(requestCallbacks, c -> c.onFinally(request, response));
167-
if (asyncContext != null) {
168-
asyncContext.complete();
169-
}
170160
}
171161
}
172162

173-
@Override
174-
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
175-
init();
176-
doRequestAsync(req, resp, requestHandler);
177-
}
178-
179-
@Override
180-
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
181-
init();
182-
doRequestAsync(req, resp, requestHandler);
183-
}
184-
185163
private <R> List<R> runListeners(Function<? super GraphQLServletListener, R> action) {
186164
return configuration.getListeners().stream()
187165
.map(listener -> {
188166
try {
189167
return action.apply(listener);
190-
} catch (Throwable t) {
168+
} catch (Exception t) {
191169
log.error("Error running listener: {}", listener, t);
192170
return null;
193171
}
@@ -200,7 +178,7 @@ private <T> void runCallbacks(List<T> callbacks, Consumer<T> action) {
200178
callbacks.forEach(callback -> {
201179
try {
202180
action.accept(callback);
203-
} catch (Throwable t) {
181+
} catch (Exception t) {
204182
log.error("Error running callback: {}", callback, t);
205183
}
206184
});

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/GraphQLConfiguration.java

+11
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,19 @@ public class GraphQLConfiguration {
3030
private final GraphQLInvoker graphQLInvoker;
3131
private final GraphQLObjectMapper objectMapper;
3232
private final List<GraphQLServletListener> listeners;
33+
/**
34+
* For removal
35+
* @since 10.1.0
36+
*/
37+
@Deprecated
3338
private final boolean asyncServletModeEnabled;
39+
/**
40+
* For removal
41+
* @since 10.1.0
42+
*/
43+
@Deprecated
3444
private final Executor asyncExecutor;
45+
3546
private final long subscriptionTimeout;
3647
@Getter
3748
private final long asyncTimeout;

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandlerImpl.java

+43-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.kickstart.servlet;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
34
import graphql.GraphQLException;
45
import graphql.kickstart.execution.GraphQLInvoker;
56
import graphql.kickstart.execution.GraphQLQueryResult;
@@ -9,6 +10,9 @@
910
import graphql.kickstart.servlet.input.BatchInputPreProcessResult;
1011
import graphql.kickstart.servlet.input.BatchInputPreProcessor;
1112
import java.io.IOException;
13+
import java.io.UncheckedIOException;
14+
import java.util.concurrent.CompletableFuture;
15+
import javax.servlet.AsyncContext;
1216
import javax.servlet.http.HttpServletRequest;
1317
import javax.servlet.http.HttpServletResponse;
1418
import lombok.extern.slf4j.Slf4j;
@@ -36,11 +40,11 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr
3640
GraphQLInvocationInput invocationInput = invocationInputParser
3741
.getGraphQLInvocationInput(request, response);
3842
execute(invocationInput, request, response);
39-
} catch (GraphQLException e) {
43+
} catch (GraphQLException| JsonProcessingException e) {
4044
response.setStatus(STATUS_BAD_REQUEST);
4145
log.info("Bad request: cannot handle http request", e);
4246
throw e;
43-
} catch (Throwable t) {
47+
} catch (Exception t) {
4448
response.setStatus(500);
4549
log.error("Cannot handle http request", t);
4650
throw t;
@@ -49,38 +53,65 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr
4953

5054
protected void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
5155
HttpServletResponse response) throws IOException {
52-
GraphQLQueryResult queryResult = invoke(invocationInput, request, response);
56+
if (request.isAsyncSupported()) {
57+
AsyncContext asyncContext = request.isAsyncStarted()
58+
? request.getAsyncContext()
59+
: request.startAsync(request, response);
60+
asyncContext.setTimeout(configuration.getAsyncTimeout());
61+
invoke(invocationInput, request, response)
62+
.thenAccept(result -> writeResultResponse(invocationInput, result, request, response))
63+
.exceptionally(t -> writeErrorResponse(t, response))
64+
.thenAccept(aVoid -> asyncContext.complete());
65+
} else {
66+
try {
67+
GraphQLQueryResult result = invoke(invocationInput, request, response).join();
68+
writeResultResponse(invocationInput, result, request, response);
69+
} catch (Exception t) {
70+
writeErrorResponse(t, response);
71+
}
72+
}
73+
}
5374

75+
private void writeResultResponse(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult, HttpServletRequest request,
76+
HttpServletResponse response) {
5477
QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult);
55-
queryResponseWriter.write(request, response);
78+
try {
79+
queryResponseWriter.write(request, response);
80+
} catch (IOException e) {
81+
throw new UncheckedIOException(e);
82+
}
5683
}
5784

58-
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput,
59-
GraphQLQueryResult queryResult) {
85+
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult) {
6086
return QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
6187
configuration.getSubscriptionTimeout());
6288
}
6389

64-
private GraphQLQueryResult invoke(GraphQLInvocationInput invocationInput,
65-
HttpServletRequest request,
90+
private Void writeErrorResponse(Throwable t, HttpServletResponse response) {
91+
response.setStatus(STATUS_BAD_REQUEST);
92+
log.info("Bad GET request: path was not \"/schema.json\" or no query variable named \"query\" given", t);
93+
return null;
94+
}
95+
96+
private CompletableFuture<GraphQLQueryResult> invoke(GraphQLInvocationInput invocationInput, HttpServletRequest request,
6697
HttpServletResponse response) {
6798
if (invocationInput instanceof GraphQLSingleInvocationInput) {
68-
return graphQLInvoker.query(invocationInput);
99+
return graphQLInvoker.queryAsync(invocationInput);
69100
}
70101
return invokeBatched((GraphQLBatchedInvocationInput) invocationInput, request, response);
71102
}
72103

73-
private GraphQLQueryResult invokeBatched(GraphQLBatchedInvocationInput batchedInvocationInput,
104+
private CompletableFuture<GraphQLQueryResult> invokeBatched(GraphQLBatchedInvocationInput batchedInvocationInput,
74105
HttpServletRequest request,
75106
HttpServletResponse response) {
76107
BatchInputPreProcessor preprocessor = configuration.getBatchInputPreProcessor();
77108
BatchInputPreProcessResult result = preprocessor
78109
.preProcessBatch(batchedInvocationInput, request, response);
79110
if (result.isExecutable()) {
80-
return graphQLInvoker.query(result.getBatchedInvocationInput());
111+
return graphQLInvoker.queryAsync(result.getBatchedInvocationInput());
81112
}
82113

83-
return GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage());
114+
return CompletableFuture.completedFuture(GraphQLQueryResult.createError(result.getStatusCode(), result.getStatusMessage()));
84115
}
85116

86117
}

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/cache/CachingHttpRequestHandlerImpl.java

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ protected void execute(GraphQLInvocationInput invocationInput, HttpServletReques
4444
}
4545
}
4646

47+
@Override
4748
protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput,
4849
GraphQLQueryResult queryResult) {
4950
return CachingQueryResponseWriter.createCacheWriter(

graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/AbstractGraphQLHttpServletSpec.groovy

+6-4
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class AbstractGraphQLHttpServletSpec extends Specification {
127127
getResponseContent().data.echo == "special char á"
128128
}
129129

130-
def "async query over HTTP GET starts async request"() {
130+
def "disabling async support on request over HTTP GET does not start async request"() {
131131
setup:
132132
servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env ->
133133
AtomicReference<SingleSubscriberPublisher<String>> publisherRef = new AtomicReference<>();
@@ -138,12 +138,13 @@ class AbstractGraphQLHttpServletSpec extends Specification {
138138
return publisherRef.get()
139139
}, true)
140140
request.addParameter('query', 'query { echo(arg:"test") }')
141+
request.setAsyncSupported(false)
141142

142143
when:
143144
servlet.doGet(request, response)
144145

145146
then:
146-
request.asyncStarted == true
147+
request.asyncContext == null
147148
}
148149

149150
def "query over HTTP GET with variables returns data"() {
@@ -442,7 +443,7 @@ class AbstractGraphQLHttpServletSpec extends Specification {
442443
getResponseContent().data.echo == "test"
443444
}
444445

445-
def "async query over HTTP POST starts async request"() {
446+
def "disabling async support on request over HTTP POST does not start async request"() {
446447
setup:
447448
servlet = TestUtils.createDefaultServlet({ env -> env.arguments.arg }, { env -> env.arguments.arg }, { env ->
448449
AtomicReference<SingleSubscriberPublisher<String>> publisherRef = new AtomicReference<>();
@@ -455,12 +456,13 @@ class AbstractGraphQLHttpServletSpec extends Specification {
455456
request.setContent(mapper.writeValueAsBytes([
456457
query: 'query { echo(arg:"test") }'
457458
]))
459+
request.setAsyncSupported(false)
458460

459461
when:
460462
servlet.doPost(request, response)
461463

462464
then:
463-
request.asyncStarted == true
465+
request.asyncContext == null
464466
}
465467

466468
def "query over HTTP POST body with graphql contentType returns data"() {

0 commit comments

Comments
 (0)