All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.execution.batched.BatchedExecutionStrategy Maven / Gradle / Ivy

package graphql.execution.batched;

import graphql.Assert;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.PublicApi;
import graphql.TrivialDataFetcher;
import graphql.execution.Async;
import graphql.execution.DataFetcherExceptionHandler;
import graphql.execution.ExecutionContext;
import graphql.execution.ExecutionStepInfo;
import graphql.execution.ExecutionStrategy;
import graphql.execution.ExecutionStrategyParameters;
import graphql.execution.FieldCollectorParameters;
import graphql.execution.MergedField;
import graphql.execution.MergedSelectionSet;
import graphql.execution.NonNullableFieldValidator;
import graphql.execution.ResolveType;
import graphql.execution.ResultPath;
import graphql.execution.SimpleDataFetcherExceptionHandler;
import graphql.execution.directives.QueryDirectivesImpl;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.normalized.NormalizedField;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingFieldSelectionSet;
import graphql.schema.DataFetchingFieldSelectionSetImpl;
import graphql.schema.GraphQLCodeRegistry;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLUnionType;

import java.lang.reflect.Array;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.IntStream;

import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo;
import static graphql.execution.FieldCollectorParameters.newParameters;
import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

/**
 * 
* BatchedExecutionStrategy has been deprecated in favour of {@link graphql.execution.AsyncExecutionStrategy} * and {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation}. *

* BatchedExecutionStrategy does not properly implement the graphql runtime specification. Specifically it * does not correctly handle non null fields and how they are to cascade up their parent fields. It has proven * an intractable problem to make this code handle these cases. *

* See http://facebook.github.io/graphql/October2016/#sec-Errors-and-Non-Nullability *

* We will remove it once we are sure the alternative is as least good as the BatchedExecutionStrategy. * *

*

* Execution Strategy that minimizes calls to the data fetcher when used in conjunction with {@link DataFetcher}s that have * {@link DataFetcher#get(DataFetchingEnvironment)} methods annotated with {@link Batched}. See the javadoc comment on * {@link Batched} for a more detailed description of batched data fetchers. *

* The strategy runs a BFS over terms of the query and passes a list of all the relevant sources to the batched data fetcher. *

* Normal DataFetchers can be used, however they will not see benefits of batching as they expect a single source object * at a time. * * @deprecated This has been deprecated in favour of using {@link graphql.execution.AsyncExecutionStrategy} and {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation} */ @PublicApi @Deprecated public class BatchedExecutionStrategy extends ExecutionStrategy { private final BatchedDataFetcherFactory batchingFactory = new BatchedDataFetcherFactory(); private final ResolveType resolveType = new ResolveType(); public BatchedExecutionStrategy() { this(new SimpleDataFetcherExceptionHandler()); } public BatchedExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHandler) { super(dataFetcherExceptionHandler); } @Override @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { InstrumentationContext executionStrategyCtx = executionContext.getInstrumentation() .beginExecutionStrategy(new InstrumentationExecutionStrategyParameters(executionContext, parameters)); GraphQLObjectType type = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); ExecutionNode root = new ExecutionNode(type, parameters.getExecutionStepInfo(), parameters.getFields().getSubFields(), singletonList(MapOrList.createMap(new LinkedHashMap<>())), Collections.singletonList(parameters.getSource()) ); Queue nodes = new ArrayDeque<>(); CompletableFuture result = new CompletableFuture<>(); executeImpl(executionContext, parameters, root, root, nodes, root.getFields().keySet().iterator(), result); executionStrategyCtx.onDispatched(result); result.whenComplete(executionStrategyCtx::onCompleted); return result; } @SuppressWarnings("FutureReturnValueIgnored") private void executeImpl(ExecutionContext executionContext, ExecutionStrategyParameters parameters, ExecutionNode root, ExecutionNode curNode, Queue queueOfNodes, Iterator curFieldNames, CompletableFuture overallResult) { if (!curFieldNames.hasNext() && queueOfNodes.isEmpty()) { overallResult.complete(new ExecutionResultImpl(root.getParentResults().get(0).toObject(), executionContext.getErrors())); return; } if (!curFieldNames.hasNext()) { curNode = queueOfNodes.poll(); curFieldNames = curNode.getFields().keySet().iterator(); } String fieldName = curFieldNames.next(); MergedField currentField = curNode.getFields().get(fieldName); // // once an object is resolved from a interface / union to a node with an object type, the // parent type info has effectively changed (it has got more specific), even though the path etc... // has not changed ExecutionStepInfo currentParentExecutionStepInfo = parameters.getExecutionStepInfo(); ExecutionStepInfo newParentExecutionStepInfo = newExecutionStepInfo() .type(curNode.getType()) .fieldDefinition(currentParentExecutionStepInfo.getFieldDefinition()) .field(currentParentExecutionStepInfo.getField()) .path(currentParentExecutionStepInfo.getPath()) .parentInfo(currentParentExecutionStepInfo.getParent()) .build(); ResultPath fieldPath = curNode.getExecutionStepInfo().getPath().segment(mkNameForPath(currentField)); GraphQLFieldDefinition fieldDefinition = getFieldDef(executionContext.getGraphQLSchema(), curNode.getType(), currentField.getSingleField()); ExecutionStepInfo executionStepInfo = newExecutionStepInfo() .type(fieldDefinition.getType()) .fieldDefinition(fieldDefinition) .field(currentField) .path(fieldPath) .parentInfo(newParentExecutionStepInfo) .build(); ExecutionStrategyParameters newParameters = parameters .transform(builder -> builder .path(fieldPath) .field(currentField) .executionStepInfo(executionStepInfo) ); ExecutionNode finalCurNode = curNode; Iterator finalCurFieldNames = curFieldNames; resolveField(executionContext, newParameters, fieldName, curNode) .whenComplete((childNodes, exception) -> { if (exception != null) { handleNonNullException(executionContext, overallResult, exception); return; } queueOfNodes.addAll(childNodes); executeImpl(executionContext, newParameters, root, finalCurNode, queueOfNodes, finalCurFieldNames, overallResult); }); } private CompletableFuture> resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, String fieldName, ExecutionNode node) { GraphQLObjectType parentType = node.getType(); MergedField fields = node.getFields().get(fieldName); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fields.getSingleField()); Instrumentation instrumentation = executionContext.getInstrumentation(); ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); InstrumentationContext fieldCtx = instrumentation.beginField( new InstrumentationFieldParameters(executionContext, () -> executionStepInfo) ); CompletableFuture fetchedData = fetchData(executionContext, parameters, fieldName, node, fieldDef); CompletableFuture> result = fetchedData.thenApply((fetchedValues) -> { GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); Map argumentValues = valuesResolver.getArgumentValues( codeRegistry, fieldDef.getArguments(), fields.getSingleField().getArguments(), executionContext.getVariables()); return completeValues(executionContext, fetchedValues, executionStepInfo, fieldName, fields, argumentValues); }); fieldCtx.onDispatched(null); result = result.whenComplete((nodes, throwable) -> fieldCtx.onCompleted(null, throwable)); return result; } private CompletableFuture fetchData(ExecutionContext executionContext, ExecutionStrategyParameters parameters, String fieldName, ExecutionNode node, GraphQLFieldDefinition fieldDef) { GraphQLObjectType parentType = node.getType(); MergedField fields = node.getFields().get(fieldName); List parentResults = node.getParentResults(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); Map argumentValues = valuesResolver.getArgumentValues( codeRegistry, fieldDef.getArguments(), fields.getSingleField().getArguments(), executionContext.getVariables()); QueryDirectivesImpl queryDirectives = new QueryDirectivesImpl(fields, executionContext.getGraphQLSchema(), executionContext.getVariables()); Supplier normalizedFieldSupplier = getNormalizedField(executionContext, parameters, parameters::getExecutionStepInfo); GraphQLOutputType fieldType = fieldDef.getType(); DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldType, normalizedFieldSupplier); DataFetchingEnvironment environment = newDataFetchingEnvironment(executionContext) .source(node.getSources()) .arguments(argumentValues) .fieldDefinition(fieldDef) .mergedField(fields) .fieldType(fieldDef.getType()) .executionStepInfo(parameters.getExecutionStepInfo()) .parentType(parentType) .selectionSet(fieldCollector) .queryDirectives(queryDirectives) .build(); DataFetcher supplied = codeRegistry.getDataFetcher(parentType, fieldDef); boolean trivialDataFetcher = supplied instanceof TrivialDataFetcher; BatchedDataFetcher batchedDataFetcher = batchingFactory.create(supplied); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldFetchParameters instrumentationFieldFetchParameters = new InstrumentationFieldFetchParameters(executionContext, fieldDef, environment, parameters, trivialDataFetcher); InstrumentationContext fetchCtx = instrumentation.beginFieldFetch(instrumentationFieldFetchParameters); CompletableFuture fetchedValue; try { DataFetcher dataFetcher = instrumentation.instrumentDataFetcher( batchedDataFetcher, instrumentationFieldFetchParameters); Object fetchedValueRaw = dataFetcher.get(environment); fetchedValue = Async.toCompletableFuture(fetchedValueRaw); } catch (Exception e) { fetchedValue = new CompletableFuture<>(); fetchedValue.completeExceptionally(e); } return fetchedValue .thenApply((result) -> assertResult(parentResults, result)) .whenComplete(fetchCtx::onCompleted) .handle(handleResult(executionContext, parameters, parentResults, environment)); } private BiFunction, Throwable, FetchedValues> handleResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List parentResults, DataFetchingEnvironment environment) { return (result, exception) -> { if (exception != null) { if (exception instanceof CompletionException) { exception = exception.getCause(); } handleFetchingException(executionContext, environment, exception); result = Collections.nCopies(parentResults.size(), null); } List values = result; List retVal = new ArrayList<>(); for (int i = 0; i < parentResults.size(); i++) { Object value = executionContext.getValueUnboxer().unbox(values.get(i)); retVal.add(new FetchedValue(parentResults.get(i), value)); } return new FetchedValues(retVal, parameters.getExecutionStepInfo(), parameters.getPath()); }; } private List assertResult(List parentResults, Object result) { result = convertPossibleArray(result); if (result != null && !(result instanceof Iterable)) { throw new BatchAssertionFailed(String.format("BatchedDataFetcher provided an invalid result: Iterable expected but got '%s'. Affected fields are set to null.", result.getClass().getName())); } @SuppressWarnings("unchecked") Iterable iterableResult = (Iterable) result; if (iterableResult == null) { throw new BatchAssertionFailed("BatchedDataFetcher provided a null Iterable of result values. Affected fields are set to null."); } List resultList = new ArrayList<>(); iterableResult.forEach(resultList::add); long size = resultList.size(); if (size != parentResults.size()) { throw new BatchAssertionFailed(String.format("BatchedDataFetcher provided invalid number of result values, expected %d but got %d. Affected fields are set to null.", parentResults.size(), size)); } return resultList; } private List completeValues(ExecutionContext executionContext, FetchedValues fetchedValues, ExecutionStepInfo executionStepInfo, String fieldName, MergedField fields, Map argumentValues) { handleNonNullType(executionContext, fetchedValues); GraphQLType unwrappedFieldType = executionStepInfo.getUnwrappedNonNullType(); if (isPrimitive(unwrappedFieldType)) { handlePrimitives(fetchedValues, fieldName, unwrappedFieldType); return Collections.emptyList(); } else if (isObject(unwrappedFieldType)) { return handleObject(executionContext, argumentValues, fetchedValues, fieldName, fields, executionStepInfo); } else if (isList(unwrappedFieldType)) { return handleList(executionContext, argumentValues, fetchedValues, fieldName, fields, executionStepInfo); } else { return Assert.assertShouldNeverHappen("can't handle type: %s", unwrappedFieldType); } } private List handleList(ExecutionContext executionContext, Map argumentValues, FetchedValues fetchedValues, String fieldName, MergedField fields, ExecutionStepInfo executionStepInfo) { GraphQLList listType = (GraphQLList) executionStepInfo.getUnwrappedNonNullType(); List flattenedValues = new ArrayList<>(); for (FetchedValue value : fetchedValues.getValues()) { MapOrList mapOrList = value.getParentResult(); if (value.getValue() == null) { mapOrList.putOrAdd(fieldName, null); continue; } MapOrList listResult = mapOrList.createAndPutList(fieldName); for (Object rawValue : toIterable(value.getValue())) { rawValue = executionContext.getValueUnboxer().unbox(rawValue); flattenedValues.add(new FetchedValue(listResult, rawValue)); } } GraphQLOutputType innerSubType = (GraphQLOutputType) listType.getWrappedType(); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull((GraphQLOutputType) GraphQLTypeUtil.unwrapNonNull(innerSubType)); FetchedValues flattenedFetchedValues = new FetchedValues(flattenedValues, newExecutionStepInfo, fetchedValues.getPath()); return completeValues(executionContext, flattenedFetchedValues, newExecutionStepInfo, fieldName, fields, argumentValues); } private List handleObject(ExecutionContext executionContext, Map argumentValues, FetchedValues fetchedValues, String fieldName, MergedField fields, ExecutionStepInfo executionStepInfo) { // collect list of values by actual type (needed because of interfaces and unions) Map> resultsByType = new LinkedHashMap<>(); Map> sourceByType = new LinkedHashMap<>(); for (FetchedValue value : fetchedValues.getValues()) { MapOrList mapOrList = value.getParentResult(); if (value.getValue() == null) { mapOrList.putOrAdd(fieldName, null); continue; } MapOrList childResult = mapOrList.createAndPutMap(fieldName); GraphQLObjectType resolvedType = getGraphQLObjectType(executionContext, fields, executionStepInfo.getUnwrappedNonNullType(), value.getValue(), argumentValues); resultsByType.putIfAbsent(resolvedType, new ArrayList<>()); resultsByType.get(resolvedType).add(childResult); sourceByType.putIfAbsent(resolvedType, new ArrayList<>()); sourceByType.get(resolvedType).add(value.getValue()); } List childNodes = new ArrayList<>(); for (GraphQLObjectType resolvedType : resultsByType.keySet()) { List results = resultsByType.get(resolvedType); List sources = sourceByType.get(resolvedType); MergedSelectionSet childFields = getChildFields(executionContext, resolvedType, fields); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedType); childNodes.add(new ExecutionNode(resolvedType, newExecutionStepInfo, childFields.getSubFields(), results, sources)); } return childNodes; } private void handleNonNullType(ExecutionContext executionContext, FetchedValues fetchedValues) { ExecutionStepInfo executionStepInfo = fetchedValues.getExecutionStepInfo(); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ResultPath path = fetchedValues.getPath(); for (FetchedValue value : fetchedValues.getValues()) { nonNullableFieldValidator.validate(path, value.getValue()); } } private MergedSelectionSet getChildFields(ExecutionContext executionContext, GraphQLObjectType resolvedType, MergedField fields) { FieldCollectorParameters collectorParameters = newParameters() .schema(executionContext.getGraphQLSchema()) .objectType(resolvedType) .fragments(executionContext.getFragmentsByName()) .variables(executionContext.getVariables()) .build(); return fieldCollector.collectFields(collectorParameters, fields); } private GraphQLObjectType getGraphQLObjectType(ExecutionContext executionContext, MergedField field, GraphQLType fieldType, Object value, Map argumentValues) { return resolveType.resolveType(executionContext, field, value, argumentValues, fieldType); } private void handlePrimitives(FetchedValues fetchedValues, String fieldName, GraphQLType fieldType) { for (FetchedValue value : fetchedValues.getValues()) { Object coercedValue = coerce(fieldType, value.getValue()); //6.6.1 http://facebook.github.io/graphql/#sec-Field-entries if (coercedValue instanceof Double && ((Double) coercedValue).isNaN()) { coercedValue = null; } value.getParentResult().putOrAdd(fieldName, coercedValue); } } private Object coerce(GraphQLType type, Object value) { if (value == null) { return null; } if (type instanceof GraphQLEnumType) { return ((GraphQLEnumType) type).serialize(value); } else { return ((GraphQLScalarType) type).getCoercing().serialize(value); } } private boolean isList(GraphQLType type) { return type instanceof GraphQLList; } private boolean isPrimitive(GraphQLType type) { return type instanceof GraphQLScalarType || type instanceof GraphQLEnumType; } private boolean isObject(GraphQLType type) { return type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType; } private Object convertPossibleArray(Object result) { if (result != null && result.getClass().isArray()) { return IntStream.range(0, Array.getLength(result)) .mapToObj(i -> Array.get(result, i)) .collect(toList()); } return result; } }