From 3af03bb791244e881ced1e9bf2da4a2712050750 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Mon, 14 Nov 2016 21:54:26 -0500 Subject: [PATCH 01/14] Async execution strategy --- build.gradle | 11 +- src/main/java/graphql/GraphQL.java | 10 +- src/main/java/graphql/GraphQLAsync.java | 86 ++++++++ .../graphql/execution/AsyncExecution.java | 44 ++++ .../java/graphql/execution/Execution.java | 11 +- .../async/AsyncExecutionStrategy.java | 207 ++++++++++++++++++ .../async/AsyncExecutionStrategyTest.groovy | 101 +++++++++ 7 files changed, 456 insertions(+), 14 deletions(-) create mode 100644 src/main/java/graphql/GraphQLAsync.java create mode 100644 src/main/java/graphql/execution/AsyncExecution.java create mode 100644 src/main/java/graphql/execution/async/AsyncExecutionStrategy.java create mode 100644 src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy diff --git a/build.gradle b/build.gradle index 373fbee8db..7239cbe48e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import java.text.SimpleDateFormat - plugins { id "com.jfrog.bintray" version "1.2" } @@ -10,10 +8,11 @@ apply plugin: 'maven-publish' apply plugin: 'antlr' apply plugin: 'jacoco' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 def releaseVersion = System.properties.RELEASE_VERSION -version = releaseVersion ? releaseVersion : new SimpleDateFormat('yyyy-MM-dd\'T\'HH-mm-ss').format(new Date()) +version = "3.0.0-SNAPSHOT" +//version = releaseVersion ? releaseVersion : new SimpleDateFormat('yyyy-MM-dd\'T\'HH-mm-ss').format(new Date()) group = 'com.graphql-java' @@ -30,12 +29,14 @@ jar { dependencies { compile 'org.antlr:antlr4-runtime:4.5.1' compile 'org.slf4j:slf4j-api:1.7.12' + compile 'com.spotify:completable-futures:0.3.0' antlr "org.antlr:antlr4:4.5.1" testCompile group: 'junit', name: 'junit', version: '4.11' testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' testCompile 'org.codehaus.groovy:groovy-all:2.4.4' testCompile 'cglib:cglib-nodep:3.1' testCompile 'org.objenesis:objenesis:2.1' + testCompile 'org.slf4j:slf4j-log4j12:1.7.21' } compileJava.source file("build/generated-src"), sourceSets.main.java diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index aca02821ef..405a91a2ed 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionStage; import static graphql.Assert.assertNotNull; @@ -34,11 +35,10 @@ public ExecutionId provide(String query, String operationName, Object context) { } }; - private final GraphQLSchema graphQLSchema; - private final ExecutionStrategy queryStrategy; - private final ExecutionStrategy mutationStrategy; - private final ExecutionIdProvider idProvider; - + protected final GraphQLSchema graphQLSchema; + protected final ExecutionStrategy queryStrategy; + protected final ExecutionStrategy mutationStrategy; + protected final ExecutionIdProvider idProvider; /** * A GraphQL object ready to execute queries diff --git a/src/main/java/graphql/GraphQLAsync.java b/src/main/java/graphql/GraphQLAsync.java new file mode 100644 index 0000000000..1fe6ffb31b --- /dev/null +++ b/src/main/java/graphql/GraphQLAsync.java @@ -0,0 +1,86 @@ +package graphql; + +import graphql.execution.AsyncExecution; +import graphql.execution.Execution; +import graphql.execution.ExecutionStrategy; +import graphql.language.Document; +import graphql.language.SourceLocation; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.validation.ValidationError; +import graphql.validation.Validator; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import static graphql.Assert.assertNotNull; +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class GraphQLAsync extends GraphQL { + + private static Logger log = LoggerFactory.getLogger(GraphQLAsync.class); + + public GraphQLAsync(GraphQLSchema graphQLSchema) { + super(graphQLSchema); + } + + public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { + super(graphQLSchema, queryStrategy); + } + + public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { + super(graphQLSchema, queryStrategy, mutationStrategy); + } + + public CompletionStage executeAsync(String requestString) { + return executeAsync(requestString, null); + + } + + public CompletionStage executeAsync(String requestString, Object context) { + return executeAsync(requestString, context, Collections.emptyMap()); + + } + + public CompletionStage executeAsync(String requestString, String operationName, Object context) { + return executeAsync(requestString, operationName, context, Collections.emptyMap()); + + } + + public CompletionStage executeAsync(String requestString, Object context, Map arguments) { + return executeAsync(requestString, null, context, arguments); + + } + + @SuppressWarnings("unchecked") + public CompletionStage executeAsync(String requestString, String operationName, Object context, Map arguments) { + + assertNotNull(arguments, "arguments can't be null"); + log.debug("Executing request. operation name: {}. Request: {} ", operationName, requestString); + Parser parser = new Parser(); + Document document; + try { + document = parser.parseDocument(requestString); + } catch (ParseCancellationException e) { + RecognitionException recognitionException = (RecognitionException) e.getCause(); + SourceLocation sourceLocation = new SourceLocation(recognitionException.getOffendingToken().getLine(), recognitionException.getOffendingToken().getCharPositionInLine()); + InvalidSyntaxError invalidSyntaxError = new InvalidSyntaxError(sourceLocation); + return completedFuture(new ExecutionResultImpl(Arrays.asList(invalidSyntaxError))); + } + + Validator validator = new Validator(); + List validationErrors = validator.validateDocument(graphQLSchema, document); + if (validationErrors.size() > 0) { + return completedFuture(new ExecutionResultImpl(validationErrors)); + } + AsyncExecution execution = new AsyncExecution(queryStrategy, mutationStrategy); + return execution.executeAsync(graphQLSchema, context, document, operationName, arguments); + } +} diff --git a/src/main/java/graphql/execution/AsyncExecution.java b/src/main/java/graphql/execution/AsyncExecution.java new file mode 100644 index 0000000000..750894875c --- /dev/null +++ b/src/main/java/graphql/execution/AsyncExecution.java @@ -0,0 +1,44 @@ +package graphql.execution; + +import graphql.ExecutionResult; +import graphql.execution.async.AsyncExecutionStrategy; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +public class AsyncExecution extends Execution { + + public AsyncExecution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { + super(queryStrategy, mutationStrategy); + } + + public CompletionStage executeAsync(GraphQLSchema graphQLSchema, Object root, Document document, String operationName, Map args) { + ExecutionContextBuilder executionContextBuilder = new ExecutionContextBuilder(new ValuesResolver()); + ExecutionContext executionContext = executionContextBuilder.build(graphQLSchema, queryStrategy, mutationStrategy, root, document, operationName, args); + return executeOperationAsync(executionContext, root, executionContext.getOperationDefinition()); + } + + private CompletionStage executeOperationAsync( + ExecutionContext executionContext, + Object root, + OperationDefinition operationDefinition) { + GraphQLObjectType operationRootType = getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); + + Map> fields = new LinkedHashMap>(); + fieldCollector.collectFields(executionContext, operationRootType, operationDefinition.getSelectionSet(), new ArrayList(), fields); + + if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { + return ((AsyncExecutionStrategy) mutationStrategy).executeAsync(executionContext, operationRootType, root, fields); + } else { + return ((AsyncExecutionStrategy) queryStrategy).executeAsync(executionContext, operationRootType, root, fields); + } + } +} diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 17abdfc830..f80b123992 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -3,6 +3,7 @@ import graphql.ExecutionResult; import graphql.GraphQLException; +import graphql.execution.async.AsyncExecutionStrategy; import graphql.language.Document; import graphql.language.Field; import graphql.language.OperationDefinition; @@ -13,12 +14,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; public class Execution { - private FieldCollector fieldCollector = new FieldCollector(); - private ExecutionStrategy queryStrategy; - private ExecutionStrategy mutationStrategy; + protected FieldCollector fieldCollector = new FieldCollector(); + protected ExecutionStrategy queryStrategy; + protected ExecutionStrategy mutationStrategy; public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { this.queryStrategy = queryStrategy != null ? queryStrategy : new SimpleExecutionStrategy(); @@ -33,7 +36,7 @@ public ExecutionResult execute(ExecutionId executionId, GraphQLSchema graphQLSch return executeOperation(executionContext, root, executionContext.getOperationDefinition()); } - private GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { + protected GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { return graphQLSchema.getMutationType(); diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java new file mode 100644 index 0000000000..9130072893 --- /dev/null +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -0,0 +1,207 @@ +package graphql.execution.async; + +import com.spotify.futures.CompletableFutures; +import graphql.ExceptionWhileDataFetching; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLException; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategy; +import graphql.language.Field; +import graphql.schema.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; + + +public final class AsyncExecutionStrategy extends ExecutionStrategy { + + public static AsyncExecutionStrategy serial() { + return new AsyncExecutionStrategy(true); + } + + public static AsyncExecutionStrategy parallel() { + return new AsyncExecutionStrategy(false); + } + + private static final Logger log = LoggerFactory.getLogger(AsyncExecutionStrategy.class); + + private final boolean serial; + + private AsyncExecutionStrategy(boolean serial) { + this.serial = serial; + } + + @Override + public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + try { + return executeAsync(executionContext, parentType, source, fields).toCompletableFuture().get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public CompletionStage executeAsync(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + + Map>> fieldsToExecute = fields.keySet() + .stream() + .collect(Collectors.toMap( + Function.identity(), + field -> () -> resolveFieldAsync(executionContext, parentType, source, fields.get(field)), + (a, b) -> a, + LinkedHashMap::new + )); + + return executeFields(fieldsToExecute).thenApply(resultMap -> { + Map dataMap = new HashMap<>(); + resultMap.forEach((key, result) -> { + dataMap.put(key, result.getData()); + }); + return new ExecutionResultImpl(dataMap, executionContext.getErrors()); + }); + } + + protected CompletionStage resolveFieldAsync(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, List fields) { + GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fields.get(0)); + + Map argumentValues = valuesResolver.getArgumentValues(fieldDef.getArguments(), fields.get(0).getArguments(), executionContext.getVariables()); + DataFetchingEnvironment environment = new DataFetchingEnvironment( + source, + argumentValues, + executionContext.getRoot(), + fields, + fieldDef.getType(), + parentType, + executionContext.getGraphQLSchema() + ); + + CompletionStage stage; + try { + Object resolvedValue = fieldDef.getDataFetcher().get(environment); + stage = resolvedValue instanceof CompletionStage ? (CompletionStage) resolvedValue : completedFuture(resolvedValue); + } catch (Exception e) { + log.warn("Exception while fetching data", e); + executionContext.addError(new ExceptionWhileDataFetching(e)); + stage = completedFuture(null); + } + + return stage.exceptionally(e -> { + log.warn("Exception while fetching data", e); + executionContext.addError(new ExceptionWhileDataFetching(e)); + return null; + }).thenCompose(o -> completeValueAsync(executionContext, fieldDef.getType(), fields, o)); + } + + protected CompletionStage completeValueAsync(ExecutionContext executionContext, GraphQLType fieldType, List fields, Object result) { + if (fieldType instanceof GraphQLNonNull) { + return completeValueAsync(executionContext, ((GraphQLNonNull) fieldType).getWrappedType(), fields, result).thenApply(result1 -> { + if (result1.getData() == null) { + throw new GraphQLException("Cannot return null for non-nullable type: " + fields); + } + return result1; + }); + } else if (result == null) { + return completedFuture(new ExecutionResultImpl(null, null)); + } else if (fieldType instanceof GraphQLList) { + if (result.getClass().isArray()) { + result = Arrays.asList((Object[]) result); + } + return completeValueForListAsync(executionContext, (GraphQLList) fieldType, fields, (Iterable) result); + } else if (fieldType instanceof GraphQLScalarType) { + return completedFuture(completeValueForScalar((GraphQLScalarType) fieldType, result)); + } else if (fieldType instanceof GraphQLEnumType) { + return completedFuture(completeValueForEnum((GraphQLEnumType) fieldType, result)); + } + + GraphQLObjectType resolvedType; + if (fieldType instanceof GraphQLInterfaceType) { + resolvedType = resolveType((GraphQLInterfaceType) fieldType, result); + } else if (fieldType instanceof GraphQLUnionType) { + resolvedType = resolveType((GraphQLUnionType) fieldType, result); + } else { + resolvedType = (GraphQLObjectType) fieldType; + } + + Map> subFields = new LinkedHashMap<>(); + List visitedFragments = new ArrayList<>(); + for (Field field : fields) { + if (field.getSelectionSet() == null) continue; + fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields); + } + + // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. + AsyncExecutionStrategy queryStrategy = (AsyncExecutionStrategy) executionContext.getQueryStrategy(); + return queryStrategy.executeAsync(executionContext, resolvedType, result, subFields); + } + + protected CompletionStage completeValueForListAsync(ExecutionContext executionContext, GraphQLList fieldType, List fields, Iterable result) { + List> completedResults = new ArrayList<>(); + for (Object item : result) { + CompletionStage completedValue = completeValueAsync(executionContext, fieldType.getWrappedType(), fields, item); + completedResults.add(completedValue); + } + return CompletableFutures.allAsList(completedResults).thenApply(results -> { + List items = results.stream().map(ExecutionResult::getData).collect(toList()); + return new ExecutionResultImpl(items, null); + }); + } + + private CompletionStage> executeFields(Map>> map) { + if (serial) { + List>>> resolvers = new ArrayList<>(map.entrySet()); + LinkedHashMap results = new LinkedHashMap<>(); + return executeInSerial(resolvers, results, 0); + } else { + return executeInParallel(map); + } + } + + private CompletionStage> executeInSerial(List>>> resolvers, Map results, int i) { + return resolvers.get(i).getValue().get().thenCompose(result -> { + results.put(resolvers.get(i).getKey(), result); + if (i == resolvers.size() - 1) { + return completedFuture(results); + } else { + return executeInSerial(resolvers, results, i + 1); + } + }); + } + + /** + * `ConcurrentHashMap`, used by `executeInParallel()`, does not allow null keys or values + */ + private static final Object NULL = new Object(); + + private CompletionStage> executeInParallel(Map>> resolvers) { + CompletableFuture> future = new CompletableFuture<>(); + Set awaiting = new ConcurrentHashMap<>(new HashMap<>(resolvers)).keySet(); // `keySet()` is a view and will be modified, so copy first + Map results = new ConcurrentHashMap<>(); + resolvers.entrySet().forEach(entry -> { + entry.getValue().get().thenAccept(result -> { + K key = entry.getKey(); + results.put(key, result); + awaiting.remove(key); + if (awaiting.isEmpty()) { + Map map = new LinkedHashMap<>(); + resolvers.keySet().forEach(key1 -> { + V value = results.get(key1); + map.put(key1, value == NULL ? null : value); + }); + future.complete(map); + } + }); + }); + return future; + } +} diff --git a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy new file mode 100644 index 0000000000..659123d1b2 --- /dev/null +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -0,0 +1,101 @@ +package graphql.execution.async + +import graphql.execution.ExecutionContext +import graphql.execution.ExecutionStrategy +import graphql.language.Field +import graphql.language.SelectionSet +import graphql.schema.* +import spock.lang.Ignore +import spock.lang.Specification +import spock.lang.Unroll + +import static com.spotify.futures.CompletableFutures.exceptionallyCompletedFuture +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema +import static java.util.concurrent.CompletableFuture.completedFuture + +class AsyncExecutionStrategyTest extends Specification { + + def strategy = AsyncExecutionStrategy.serial() + def fields = [field: [new Field('field')]] + + def parentType, executionContext, result, actual + + @Unroll + def "async field"() { + given: + parentType = buildParentType(type, fetcher) + executionContext = buildExecutionContext(strategy, parentType) + actual = strategy.execute(executionContext, parentType, null, fields); + + expect: + actual.data.field == expected + + where: + fetcher | type || expected + { it -> null } | GraphQLString || null + { it -> 'a' } | GraphQLString || 'a' + { it -> 'a' } | new GraphQLNonNull(GraphQLString) || 'a' + { it -> ['a'] } | new GraphQLList(GraphQLString) || ['a'] + { it -> ['a'] } | new GraphQLList(new GraphQLNonNull(GraphQLString)) || ['a'] + { it -> completedFuture(null) } | GraphQLString || null + { it -> completedFuture('value') } | GraphQLString || 'value' + { it -> completedFuture('value') } | new GraphQLNonNull(GraphQLString) || 'value' + { it -> completedFuture(['value']) } | new GraphQLList(new GraphQLNonNull(GraphQLString)) || ['value'] + { it -> throw new RuntimeException() } | GraphQLString || null + { it -> exceptionallyCompletedFuture(new RuntimeException()) } | GraphQLString || null + } + + def "async obj"() { + given: + def type = new GraphQLList(newObject() + .name('composite') + .field(field('field', GraphQLString, { 'value' })) + .build()) + strategy = AsyncExecutionStrategy.serial() + parentType = buildParentType(type, { completedFuture([[field: 'value']]) }) + executionContext = buildExecutionContext(strategy, parentType) + fields = [field: [new Field('field', new SelectionSet([new Field('field')]))]] + + when: + actual = strategy.execute(executionContext, parentType, null, fields); + + then: + actual.data == [field: [[field: 'value']]] + + } + + @Ignore + def "fields execute in the correct order"() { + + } + + GraphQLFieldDefinition field(String name, GraphQLOutputType type, DataFetcher fetcher) { + newFieldDefinition() + .name(name) + .type(type) + .dataFetcher(fetcher) + .build() + } + + GraphQLObjectType buildParentType(GraphQLOutputType type, DataFetcher fetcher) { + newObject() + .name('object') + .field(field('field', type, fetcher)) + .build() + } + + ExecutionContext buildExecutionContext(ExecutionStrategy strategy, GraphQLObjectType parentType) { + def executionContext = new ExecutionContext( + newSchema().query(parentType).build(), + strategy, + null, + null, + null, + null, + null + ) + } +} From dd574c3a175f229b83a50519e838c45e6b399495 Mon Sep 17 00:00:00 2001 From: Bruno Santos Date: Tue, 3 Jan 2017 11:29:40 -0500 Subject: [PATCH 02/14] Should allow for specific CompletableFuture implementations graphql-java async implmentation should allow for non-default CompletableFeatures as some users/frameworks/toolkits want to use their own thread pools. --- .../async/AsyncExecutionStrategy.java | 23 +++++++++++++++---- .../async/CompletableFutureFactory.java | 7 ++++++ .../DefaultCompletableFutureFactory.java | 15 ++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/main/java/graphql/execution/async/CompletableFutureFactory.java create mode 100644 src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 9130072893..7a22383b8f 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -20,6 +20,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static java.util.Objects.isNull; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.stream.Collectors.toList; @@ -27,19 +28,33 @@ public final class AsyncExecutionStrategy extends ExecutionStrategy { public static AsyncExecutionStrategy serial() { - return new AsyncExecutionStrategy(true); + return new AsyncExecutionStrategy(true, null); + } + + public static AsyncExecutionStrategy serial(final CompletableFutureFactory factory) { + return new AsyncExecutionStrategy(true, factory); } public static AsyncExecutionStrategy parallel() { - return new AsyncExecutionStrategy(false); + return new AsyncExecutionStrategy(false, null); + } + + public static AsyncExecutionStrategy parallel(final CompletableFutureFactory factory) { + return new AsyncExecutionStrategy(false, factory); } private static final Logger log = LoggerFactory.getLogger(AsyncExecutionStrategy.class); private final boolean serial; + private final CompletableFutureFactory completableFutureFactory; - private AsyncExecutionStrategy(boolean serial) { + private AsyncExecutionStrategy(boolean serial, final CompletableFutureFactory completableFutureFactory) { this.serial = serial; + if (isNull(completableFutureFactory)) { + this.completableFutureFactory = DefaultCompletableFutureFactory.defaultFactory(); + } else { + this.completableFutureFactory = completableFutureFactory; + } } @Override @@ -184,7 +199,7 @@ private CompletionStage> executeInSerial(List CompletionStage> executeInParallel(Map>> resolvers) { - CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture> future = completableFutureFactory.future(); Set awaiting = new ConcurrentHashMap<>(new HashMap<>(resolvers)).keySet(); // `keySet()` is a view and will be modified, so copy first Map results = new ConcurrentHashMap<>(); resolvers.entrySet().forEach(entry -> { diff --git a/src/main/java/graphql/execution/async/CompletableFutureFactory.java b/src/main/java/graphql/execution/async/CompletableFutureFactory.java new file mode 100644 index 0000000000..30d96d5a8d --- /dev/null +++ b/src/main/java/graphql/execution/async/CompletableFutureFactory.java @@ -0,0 +1,7 @@ +package graphql.execution.async; + +import java.util.concurrent.CompletableFuture; + +public interface CompletableFutureFactory { + CompletableFuture future(); +} diff --git a/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java b/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java new file mode 100644 index 0000000000..44b7f135c5 --- /dev/null +++ b/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java @@ -0,0 +1,15 @@ +package graphql.execution.async; + +import java.util.concurrent.CompletableFuture; + +public class DefaultCompletableFutureFactory implements CompletableFutureFactory { + + public static CompletableFutureFactory defaultFactory() { + return new DefaultCompletableFutureFactory(); + } + + @Override + public CompletableFuture future() { + return new CompletableFuture<>(); + } +} From cfc56c74a999c33d78543ac183bc3769076485fd Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 13:42:12 -0500 Subject: [PATCH 03/14] Test for null with isNull --- .../graphql/execution/async/AsyncExecutionStrategy.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 7a22383b8f..689790394a 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -121,12 +121,12 @@ protected CompletionStage resolveFieldAsync(ExecutionContext ex protected CompletionStage completeValueAsync(ExecutionContext executionContext, GraphQLType fieldType, List fields, Object result) { if (fieldType instanceof GraphQLNonNull) { return completeValueAsync(executionContext, ((GraphQLNonNull) fieldType).getWrappedType(), fields, result).thenApply(result1 -> { - if (result1.getData() == null) { + if (isNull(result1.getData())) { throw new GraphQLException("Cannot return null for non-nullable type: " + fields); } return result1; }); - } else if (result == null) { + } else if (isNull(result)) { return completedFuture(new ExecutionResultImpl(null, null)); } else if (fieldType instanceof GraphQLList) { if (result.getClass().isArray()) { @@ -151,7 +151,7 @@ protected CompletionStage completeValueAsync(ExecutionContext e Map> subFields = new LinkedHashMap<>(); List visitedFragments = new ArrayList<>(); for (Field field : fields) { - if (field.getSelectionSet() == null) continue; + if (isNull(field.getSelectionSet())) continue; fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields); } From 101a9d63b0e492bd15c55e87a5fd4a9cc1985518 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 13:42:33 -0500 Subject: [PATCH 04/14] Static import Arrays.asList --- .../java/graphql/execution/async/AsyncExecutionStrategy.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 689790394a..f8c166404b 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -20,6 +20,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static java.util.Arrays.asList; import static java.util.Objects.isNull; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.stream.Collectors.toList; @@ -130,7 +131,7 @@ protected CompletionStage completeValueAsync(ExecutionContext e return completedFuture(new ExecutionResultImpl(null, null)); } else if (fieldType instanceof GraphQLList) { if (result.getClass().isArray()) { - result = Arrays.asList((Object[]) result); + result = asList((Object[]) result); } return completeValueForListAsync(executionContext, (GraphQLList) fieldType, fields, (Iterable) result); } else if (fieldType instanceof GraphQLScalarType) { From 3c4f59fb408ded35d382fef67a612e02f6a702d8 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 15:36:41 -0500 Subject: [PATCH 05/14] Removed unnecessary `@SuppressWarnings` --- .../java/graphql/execution/async/AsyncExecutionStrategy.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index f8c166404b..845c21ffde 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -67,7 +67,6 @@ public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectT } } - @SuppressWarnings("unchecked") public CompletionStage executeAsync(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { Map>> fieldsToExecute = fields.keySet() From 182845dd88fb8da7e30d54ed8211994c604e0393 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 17:00:34 -0500 Subject: [PATCH 06/14] `executeInParallel` correctly handles null results --- .../execution/async/AsyncExecutionStrategy.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 845c21ffde..d7e0411ebd 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -193,11 +193,6 @@ private CompletionStage> executeInSerial(List CompletionStage> executeInParallel(Map>> resolvers) { CompletableFuture> future = completableFutureFactory.future(); Set awaiting = new ConcurrentHashMap<>(new HashMap<>(resolvers)).keySet(); // `keySet()` is a view and will be modified, so copy first @@ -205,14 +200,13 @@ private CompletionStage> executeInParallel(Map { entry.getValue().get().thenAccept(result -> { K key = entry.getKey(); - results.put(key, result); + if (!isNull(result)) { + results.put(key, result); + } awaiting.remove(key); if (awaiting.isEmpty()) { Map map = new LinkedHashMap<>(); - resolvers.keySet().forEach(key1 -> { - V value = results.get(key1); - map.put(key1, value == NULL ? null : value); - }); + resolvers.keySet().forEach(key1 -> map.put(key1, results.get(key1))); future.complete(map); } }); From f3c536609e24444b30e9ed090b909500cdf1d729 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 17:01:38 -0500 Subject: [PATCH 07/14] `executeInParallel` handles exceptions --- .../graphql/execution/async/AsyncExecutionStrategy.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index d7e0411ebd..7c37bfd9f4 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -199,6 +199,9 @@ private CompletionStage> executeInParallel(Map results = new ConcurrentHashMap<>(); resolvers.entrySet().forEach(entry -> { entry.getValue().get().thenAccept(result -> { + if (future.isCompletedExceptionally()) { + return; + } K key = entry.getKey(); if (!isNull(result)) { results.put(key, result); @@ -209,6 +212,9 @@ private CompletionStage> executeInParallel(Map map.put(key1, results.get(key1))); future.complete(map); } + }).exceptionally(e -> { + future.completeExceptionally(e); + return null; }); }); return future; From 54d8c028f74db9a7a1bf18de0306338b655a39a6 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 17:13:09 -0500 Subject: [PATCH 08/14] Removed unnecessary map copy --- .../java/graphql/execution/async/AsyncExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 7c37bfd9f4..9b13fd7548 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -195,7 +195,7 @@ private CompletionStage> executeInSerial(List CompletionStage> executeInParallel(Map>> resolvers) { CompletableFuture> future = completableFutureFactory.future(); - Set awaiting = new ConcurrentHashMap<>(new HashMap<>(resolvers)).keySet(); // `keySet()` is a view and will be modified, so copy first + Set awaiting = new ConcurrentHashMap<>(resolvers).keySet(); Map results = new ConcurrentHashMap<>(); resolvers.entrySet().forEach(entry -> { entry.getValue().get().thenAccept(result -> { From a585ef7176f54d6699caeb7f0db3873fc37db3a5 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Wed, 4 Jan 2017 18:09:30 -0500 Subject: [PATCH 09/14] Capture propagating `NonNullException` --- src/main/java/graphql/NonNullException.java | 8 +++ .../async/AsyncExecutionStrategy.java | 14 ++--- .../async/AsyncExecutionStrategyTest.groovy | 54 +++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/main/java/graphql/NonNullException.java diff --git a/src/main/java/graphql/NonNullException.java b/src/main/java/graphql/NonNullException.java new file mode 100644 index 0000000000..33ecf61cc3 --- /dev/null +++ b/src/main/java/graphql/NonNullException.java @@ -0,0 +1,8 @@ +package graphql; + +public class NonNullException extends GraphQLException { + + public NonNullException(String s) { + super(s); + } +} diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 9b13fd7548..94520ebfb5 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -1,10 +1,7 @@ package graphql.execution.async; import com.spotify.futures.CompletableFutures; -import graphql.ExceptionWhileDataFetching; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLException; +import graphql.*; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategy; import graphql.language.Field; @@ -78,12 +75,17 @@ public CompletionStage executeAsync(ExecutionContext executionC LinkedHashMap::new )); - return executeFields(fieldsToExecute).thenApply(resultMap -> { + return executeFields(fieldsToExecute).thenApply(resultMap -> { Map dataMap = new HashMap<>(); resultMap.forEach((key, result) -> { dataMap.put(key, result.getData()); }); return new ExecutionResultImpl(dataMap, executionContext.getErrors()); + }).exceptionally(e -> { + if (e.getCause() instanceof NonNullException) { + return new ExecutionResultImpl(null, null); + } + throw new RuntimeException(e); }); } @@ -122,7 +124,7 @@ protected CompletionStage completeValueAsync(ExecutionContext e if (fieldType instanceof GraphQLNonNull) { return completeValueAsync(executionContext, ((GraphQLNonNull) fieldType).getWrappedType(), fields, result).thenApply(result1 -> { if (isNull(result1.getData())) { - throw new GraphQLException("Cannot return null for non-nullable type: " + fields); + throw new NonNullException("Cannot return null for non-nullable type: " + fields); } return result1; }); diff --git a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy index 659123d1b2..7e0d782405 100644 --- a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -64,7 +64,61 @@ class AsyncExecutionStrategyTest extends Specification { then: actual.data == [field: [[field: 'value']]] + } + + // http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability + // - "Since Non-Null type fields cannot be null, field errors are propagated + // to be handled by the parent field. If the parent field may be null then + // it resolves to null, otherwise if it is a Non-Null type, the field error + // is further propagated to it’s parent field." + // - "...only one error should be added to the errors list per field." + + def "null non-null field results in null nullable parent"() { + given: + def type = newObject() + .name('ParentType') + .field(newFieldDefinition() + .name('field') + .type(new GraphQLNonNull(GraphQLString)) + .dataFetcher({ null })) + .build() + def strategy = AsyncExecutionStrategy.serial() + def executionContext = buildExecutionContext(strategy, type) + def fields = [field: [new Field('field')]] + + when: + actual = strategy.execute(executionContext, type, [:], fields) + then: + actual.data == null + } + + def "null non-null fields propagate to nearest nullable parent"() { + given: + def type = newObject() + .name('ParentType') + .field(newFieldDefinition() + .name('nullableField') + .type(newObject() + .name('ChildType') + .field(newFieldDefinition() + .name('nonNullField') + .type(new GraphQLNonNull(newObject() + .name('GrandChildType') + .field(newFieldDefinition() + .name('nonNullField') + .type(new GraphQLNonNull(GraphQLString))).build())))) + .dataFetcher({[nonNullField: [:]]})) + .build() + def strategy = AsyncExecutionStrategy.serial() + def executionContext = buildExecutionContext(strategy, type) + def fields = [nullableField: [new Field('nullableField', new SelectionSet([new Field('nonNullField', new SelectionSet([new Field('nonNullField')]))]))]] + + when: + actual = strategy.execute(executionContext, type, [:], fields) + + then: + actual.data == [nullableField: null] } @Ignore From d551919c91c49707a7cd3ba4f96c81caf7b86743 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Thu, 5 Jan 2017 10:06:26 -0500 Subject: [PATCH 10/14] Cosmetic changes to async strategy spec --- .../async/AsyncExecutionStrategyTest.groovy | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy index 7e0d782405..16047c5ef1 100644 --- a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -18,19 +18,18 @@ import static java.util.concurrent.CompletableFuture.completedFuture class AsyncExecutionStrategyTest extends Specification { - def strategy = AsyncExecutionStrategy.serial() - def fields = [field: [new Field('field')]] - - def parentType, executionContext, result, actual - @Unroll def "async field"() { given: - parentType = buildParentType(type, fetcher) - executionContext = buildExecutionContext(strategy, parentType) - actual = strategy.execute(executionContext, parentType, null, fields); + def strategy = AsyncExecutionStrategy.serial() + def parentType = buildParentType(type, fetcher) + def executionContext = buildExecutionContext(strategy, parentType) + def fields = [field: [new Field('field')]] - expect: + when: + def actual = strategy.execute(executionContext, parentType, null, fields); + + then: actual.data.field == expected where: @@ -51,16 +50,16 @@ class AsyncExecutionStrategyTest extends Specification { def "async obj"() { given: def type = new GraphQLList(newObject() - .name('composite') + .name('Composite') .field(field('field', GraphQLString, { 'value' })) .build()) - strategy = AsyncExecutionStrategy.serial() - parentType = buildParentType(type, { completedFuture([[field: 'value']]) }) - executionContext = buildExecutionContext(strategy, parentType) - fields = [field: [new Field('field', new SelectionSet([new Field('field')]))]] + def strategy = AsyncExecutionStrategy.serial() + def parentType = buildParentType(type, { completedFuture([[field: 'value']]) }) + def executionContext = buildExecutionContext(strategy, parentType) + def fields = [field: [new Field('field', new SelectionSet([new Field('field')]))]] when: - actual = strategy.execute(executionContext, parentType, null, fields); + def actual = strategy.execute(executionContext, parentType, null, fields); then: actual.data == [field: [[field: 'value']]] @@ -87,7 +86,7 @@ class AsyncExecutionStrategyTest extends Specification { def fields = [field: [new Field('field')]] when: - actual = strategy.execute(executionContext, type, [:], fields) + def actual = strategy.execute(executionContext, type, [:], fields) then: actual.data == null @@ -115,7 +114,7 @@ class AsyncExecutionStrategyTest extends Specification { def fields = [nullableField: [new Field('nullableField', new SelectionSet([new Field('nonNullField', new SelectionSet([new Field('nonNullField')]))]))]] when: - actual = strategy.execute(executionContext, type, [:], fields) + def actual = strategy.execute(executionContext, type, [:], fields) then: actual.data == [nullableField: null] From 5d88bc58d41b328396e882e39a7f881233c65b7f Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Mon, 30 Jan 2017 14:58:12 -0500 Subject: [PATCH 11/14] Fixup after rebase: DataFetchingEnvironment now an interface --- .../java/graphql/execution/async/AsyncExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java index 94520ebfb5..675ae22954 100644 --- a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -93,7 +93,7 @@ protected CompletionStage resolveFieldAsync(ExecutionContext ex GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fields.get(0)); Map argumentValues = valuesResolver.getArgumentValues(fieldDef.getArguments(), fields.get(0).getArguments(), executionContext.getVariables()); - DataFetchingEnvironment environment = new DataFetchingEnvironment( + DataFetchingEnvironment environment = new DataFetchingEnvironmentImpl( source, argumentValues, executionContext.getRoot(), From 4b063a132b4f2d192060721ab587301762859fd7 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Mon, 30 Jan 2017 15:28:54 -0500 Subject: [PATCH 12/14] Fixup after rebase: ExecutionContext now has an ExecutionId --- .../graphql/execution/async/AsyncExecutionStrategyTest.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy index 16047c5ef1..af240716bc 100644 --- a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -142,6 +142,7 @@ class AsyncExecutionStrategyTest extends Specification { ExecutionContext buildExecutionContext(ExecutionStrategy strategy, GraphQLObjectType parentType) { def executionContext = new ExecutionContext( + null, newSchema().query(parentType).build(), strategy, null, From 3e4eee50a2685f05a218cce6f57994c9aaf3fe19 Mon Sep 17 00:00:00 2001 From: Bruno Santos Date: Sat, 11 Feb 2017 19:02:15 -0500 Subject: [PATCH 13/14] Should set execution ID --- src/main/java/graphql/execution/AsyncExecution.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/AsyncExecution.java b/src/main/java/graphql/execution/AsyncExecution.java index 750894875c..cba15da0c7 100644 --- a/src/main/java/graphql/execution/AsyncExecution.java +++ b/src/main/java/graphql/execution/AsyncExecution.java @@ -22,7 +22,9 @@ public AsyncExecution(ExecutionStrategy queryStrategy, ExecutionStrategy mutatio public CompletionStage executeAsync(GraphQLSchema graphQLSchema, Object root, Document document, String operationName, Map args) { ExecutionContextBuilder executionContextBuilder = new ExecutionContextBuilder(new ValuesResolver()); - ExecutionContext executionContext = executionContextBuilder.build(graphQLSchema, queryStrategy, mutationStrategy, root, document, operationName, args); + ExecutionContext executionContext = executionContextBuilder + .executionId(ExecutionId.generate()) + .build(graphQLSchema, queryStrategy, mutationStrategy, root, document, operationName, args); return executeOperationAsync(executionContext, root, executionContext.getOperationDefinition()); } From 0c6f5127e45ced904d770952d42da41549055f26 Mon Sep 17 00:00:00 2001 From: Dmitry Minkovsky Date: Tue, 8 Aug 2017 13:12:31 -0400 Subject: [PATCH 14/14] changed defaults --- src/main/java/graphql/GraphQLAsync.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/GraphQLAsync.java b/src/main/java/graphql/GraphQLAsync.java index 1fe6ffb31b..e0e7e2c90a 100644 --- a/src/main/java/graphql/GraphQLAsync.java +++ b/src/main/java/graphql/GraphQLAsync.java @@ -3,6 +3,7 @@ import graphql.execution.AsyncExecution; import graphql.execution.Execution; import graphql.execution.ExecutionStrategy; +import graphql.execution.async.AsyncExecutionStrategy; import graphql.language.Document; import graphql.language.SourceLocation; import graphql.parser.Parser; @@ -28,11 +29,11 @@ public class GraphQLAsync extends GraphQL { private static Logger log = LoggerFactory.getLogger(GraphQLAsync.class); public GraphQLAsync(GraphQLSchema graphQLSchema) { - super(graphQLSchema); + this(graphQLSchema, AsyncExecutionStrategy.parallel()); } public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { - super(graphQLSchema, queryStrategy); + this(graphQLSchema, queryStrategy, AsyncExecutionStrategy.serial()); } public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) {