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
This commit is contained in:
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.GraphQLContext;
|
||||
import graphql.TrivialDataFetcher;
|
||||
import graphql.execution.AbortExecutionException;
|
||||
import graphql.execution.DataFetcherResult;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
@@ -107,6 +108,11 @@ final class ContextDataFetcherDecorator implements DataFetcher<Object> {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (ContextPropagationHelper.isCancelled(graphQlContext)) {
|
||||
return DataFetcherResult.newResult()
|
||||
.error(new AbortExecutionException("GraphQL request has been cancelled by the client."))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (this.subscription) {
|
||||
Flux<?> subscriptionResult = ReactiveAdapterRegistryHelper.toSubscriptionFlux(value)
|
||||
|
||||
@@ -131,6 +131,21 @@ public abstract class ContextPropagationHelper {
|
||||
return requestCancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the current request has been cancelled, {@code false} otherwise.
|
||||
* This checks whether a {@link #createCancelPublisher(GraphQLContext) cancellation publisher is present}
|
||||
* in the given context and the cancel signal has fired already.
|
||||
* @param context the current GraphQL context
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public static boolean isCancelled(GraphQLContext context) {
|
||||
Mono<Void> cancelSignal = context.get(CANCEL_PUBLISHER_KEY);
|
||||
if (cancelSignal != null) {
|
||||
return cancelSignal.toFuture().isDone();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the source {@link Mono} to the publisher from the given {@link GraphQLContext}.
|
||||
* The returned {@code Mono} will be cancelled when this publisher completes.
|
||||
|
||||
@@ -29,6 +29,7 @@ import graphql.GraphQL;
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.TrivialDataFetcher;
|
||||
import graphql.execution.AbortExecutionException;
|
||||
import graphql.execution.DataFetcherResult;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetcherFactories;
|
||||
@@ -55,7 +56,9 @@ import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Tests for {@link ContextDataFetcherDecorator}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
|
||||
public class ContextDataFetcherDecoratorTests {
|
||||
@@ -288,7 +291,7 @@ public class ContextDataFetcherDecoratorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelMonoDataFetcherWhenRequestCancelled() throws Exception {
|
||||
void cancelMonoDataFetcherWhenRequestCancelled() {
|
||||
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
|
||||
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
|
||||
.queryFetcher("greeting", (env) ->
|
||||
@@ -307,7 +310,7 @@ public class ContextDataFetcherDecoratorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelFluxDataFetcherWhenRequestCancelled() throws Exception {
|
||||
void cancelFluxDataFetcherWhenRequestCancelled() {
|
||||
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
|
||||
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
|
||||
.queryFetcher("greeting", (env) ->
|
||||
@@ -325,6 +328,22 @@ public class ContextDataFetcherDecoratorTests {
|
||||
await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnAbortExecutionForBlockingDataFetcherWhenRequestCancelled() throws Exception {
|
||||
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
|
||||
.queryFetcher("greeting", (env) -> "Hello")
|
||||
.toGraphQl();
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
Sinks.Empty<Void> requestCancelled = ContextPropagationHelper.createCancelPublisher(input.getGraphQLContext());
|
||||
requestCancelled.tryEmitEmpty();
|
||||
ExecutionResult result = graphQl.executeAsync(input).get();
|
||||
|
||||
assertThat(result.getErrors()).hasSize(1);
|
||||
assertThat(result.getErrors().get(0)).isInstanceOf(AbortExecutionException.class)
|
||||
.extracting("message").asString().isEqualTo("GraphQL request has been cancelled by the client.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelFluxDataFetcherSubscriptionWhenRequestCancelled() throws Exception {
|
||||
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
|
||||
|
||||
Reference in New Issue
Block a user