Skip to content

Commit acb1828

Browse files
committed
Avoid further processing when GraphQL request is cancelled
As of gh-1149, CANCEL signals are propagated from the transport request up to reactive `DataFetcher`s. This efficiently cancels processing and avoids spending resources when execution results won't be sent to the client. Prior to this commit, this would have no effect on non-reactive `DataFetcher`s because they would still be executed. While we cannot consistently cancel ongoing blocking operations, we can avoid further processing and other `DataFetcher`s from being called by returning an error result instead of the original result. This commit updates the `ContextDataFetcherDecorator` to detect if the request has been cancelled and return early a data fetcher result with a `AbortExecutionException` error instead of the original result. Closes gh-1153
1 parent 5398021 commit acb1828

File tree

3 files changed

+42
-2
lines changed

3 files changed

+42
-2
lines changed

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java

+6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import graphql.ExecutionInput;
2222
import graphql.GraphQLContext;
2323
import graphql.TrivialDataFetcher;
24+
import graphql.execution.AbortExecutionException;
2425
import graphql.execution.DataFetcherResult;
2526
import graphql.schema.DataFetcher;
2627
import graphql.schema.DataFetchingEnvironment;
@@ -107,6 +108,11 @@ public Object get(DataFetchingEnvironment env) throws Exception {
107108
if (value == null) {
108109
return null;
109110
}
111+
if (ContextPropagationHelper.isCancelled(graphQlContext)) {
112+
return DataFetcherResult.newResult()
113+
.error(new AbortExecutionException("GraphQL request has been cancelled by the client."))
114+
.build();
115+
}
110116

111117
if (this.subscription) {
112118
Flux<?> subscriptionResult = ReactiveAdapterRegistryHelper.toSubscriptionFlux(value)

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java

+15
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ public static Sinks.Empty<Void> createCancelPublisher(GraphQLContext context) {
131131
return requestCancelled;
132132
}
133133

134+
/**
135+
* Return {@code true} if the current request has been cancelled, {@code false} otherwise.
136+
* This checks whether a {@link #createCancelPublisher(GraphQLContext) cancellation publisher is present}
137+
* in the given context and the cancel signal has fired already.
138+
* @param context the current GraphQL context
139+
* @since 1.4.0
140+
*/
141+
public static boolean isCancelled(GraphQLContext context) {
142+
Mono<Void> cancelSignal = context.get(CANCEL_PUBLISHER_KEY);
143+
if (cancelSignal != null) {
144+
return cancelSignal.toFuture().isDone();
145+
}
146+
return false;
147+
}
148+
134149
/**
135150
* Bind the source {@link Mono} to the publisher from the given {@link GraphQLContext}.
136151
* The returned {@code Mono} will be cancelled when this publisher completes.

spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import graphql.GraphQLError;
3030
import graphql.GraphqlErrorBuilder;
3131
import graphql.TrivialDataFetcher;
32+
import graphql.execution.AbortExecutionException;
3233
import graphql.execution.DataFetcherResult;
3334
import graphql.schema.DataFetcher;
3435
import graphql.schema.DataFetcherFactories;
@@ -55,7 +56,9 @@
5556

5657
/**
5758
* Tests for {@link ContextDataFetcherDecorator}.
59+
*
5860
* @author Rossen Stoyanchev
61+
* @author Brian Clozel
5962
*/
6063
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
6164
public class ContextDataFetcherDecoratorTests {
@@ -288,7 +291,7 @@ void trivialDataFetcherIsNotDecorated() {
288291
}
289292

290293
@Test
291-
void cancelMonoDataFetcherWhenRequestCancelled() throws Exception {
294+
void cancelMonoDataFetcherWhenRequestCancelled() {
292295
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
293296
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
294297
.queryFetcher("greeting", (env) ->
@@ -307,7 +310,7 @@ void cancelMonoDataFetcherWhenRequestCancelled() throws Exception {
307310
}
308311

309312
@Test
310-
void cancelFluxDataFetcherWhenRequestCancelled() throws Exception {
313+
void cancelFluxDataFetcherWhenRequestCancelled() {
311314
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
312315
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
313316
.queryFetcher("greeting", (env) ->
@@ -325,6 +328,22 @@ void cancelFluxDataFetcherWhenRequestCancelled() throws Exception {
325328
await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get);
326329
}
327330

331+
@Test
332+
void returnAbortExecutionForBlockingDataFetcherWhenRequestCancelled() throws Exception {
333+
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
334+
.queryFetcher("greeting", (env) -> "Hello")
335+
.toGraphQl();
336+
337+
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
338+
Sinks.Empty<Void> requestCancelled = ContextPropagationHelper.createCancelPublisher(input.getGraphQLContext());
339+
requestCancelled.tryEmitEmpty();
340+
ExecutionResult result = graphQl.executeAsync(input).get();
341+
342+
assertThat(result.getErrors()).hasSize(1);
343+
assertThat(result.getErrors().get(0)).isInstanceOf(AbortExecutionException.class)
344+
.extracting("message").asString().isEqualTo("GraphQL request has been cancelled by the client.");
345+
}
346+
328347
@Test
329348
void cancelFluxDataFetcherSubscriptionWhenRequestCancelled() throws Exception {
330349
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();

0 commit comments

Comments
 (0)