graphql.nadel.engine.execution.NadelExecutionStrategy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nadel-engine Show documentation
Show all versions of nadel-engine Show documentation
Nadel is a Java library that combines multiple GrahpQL services together into one API.
The newest version!
package graphql.nadel.engine.execution;
import graphql.Assert;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.execution.Async;
import graphql.execution.ExecutionContext;
import graphql.execution.ExecutionStepInfo;
import graphql.execution.ExecutionStepInfoFactory;
import graphql.execution.MergedField;
import graphql.execution.ResultPath;
import graphql.execution.nextgen.FieldSubSelection;
import graphql.introspection.Introspection;
import graphql.language.Field;
import graphql.language.InlineFragment;
import graphql.language.ObjectTypeDefinition;
import graphql.language.SelectionSet;
import graphql.language.TypeName;
import graphql.nadel.OperationKind;
import graphql.nadel.Service;
import graphql.nadel.dsl.NodeId;
import graphql.nadel.engine.BenchmarkContext;
import graphql.nadel.engine.FieldInfo;
import graphql.nadel.engine.FieldInfos;
import graphql.nadel.engine.NadelContext;
import graphql.nadel.engine.execution.transformation.FieldTransformation;
import graphql.nadel.engine.execution.transformation.TransformationMetadata.NormalizedFieldAndError;
import graphql.nadel.engine.hooks.EngineServiceExecutionHooks;
import graphql.nadel.engine.hooks.ResultRewriteParams;
import graphql.nadel.engine.result.LeafExecutionResultNode;
import graphql.nadel.engine.result.ResultComplexityAggregator;
import graphql.nadel.engine.result.RootExecutionResultNode;
import graphql.nadel.hooks.CreateServiceContextParams;
import graphql.nadel.hooks.ServiceExecutionHooks;
import graphql.nadel.hooks.ServiceOrError;
import graphql.nadel.instrumentation.NadelInstrumentation;
import graphql.nadel.normalized.NormalizedQueryField;
import graphql.nadel.normalized.NormalizedQueryFromAst;
import graphql.nadel.util.MergedFieldUtil;
import graphql.nadel.util.OperationNameUtil;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static graphql.Assert.assertNotEmpty;
import static graphql.Assert.assertNotNull;
import static graphql.language.InlineFragment.newInlineFragment;
import static graphql.language.SelectionSet.newSelectionSet;
import static graphql.language.TypeName.newTypeName;
import static graphql.nadel.schema.NadelDirectives.DYNAMIC_SERVICE_DIRECTIVE_DEFINITION;
import static graphql.nadel.schema.NadelDirectives.NAMESPACED_DIRECTIVE_DEFINITION;
import static graphql.nadel.util.NamespacedUtil.serviceOwnsNamespacedField;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
@Internal
public class NadelExecutionStrategy {
private final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory();
private final ServiceResultNodesToOverallResult serviceResultNodesToOverallResult = new ServiceResultNodesToOverallResult();
private final OverallQueryTransformer queryTransformer = new OverallQueryTransformer();
private final ServiceResultToResultNodes resultToResultNode = new ServiceResultToResultNodes();
private final FieldInfos fieldInfos;
private final GraphQLSchema overallSchema;
private final ServiceExecutor serviceExecutor;
private final HydrationInputResolver hydrationInputResolver;
private final ServiceExecutionHooks serviceExecutionHooks;
private final ExecutionPathSet hydrationInputPaths;
private final List services;
private static final Logger log = LoggerFactory.getLogger(NadelExecutionStrategy.class);
public NadelExecutionStrategy(List services,
FieldInfos fieldInfos,
GraphQLSchema overallSchema,
NadelInstrumentation instrumentation,
ServiceExecutionHooks serviceExecutionHooks) {
this.overallSchema = overallSchema;
assertNotEmpty(services);
this.fieldInfos = fieldInfos;
this.serviceExecutionHooks = serviceExecutionHooks;
this.serviceExecutor = new ServiceExecutor(instrumentation);
this.hydrationInputPaths = new ExecutionPathSet();
this.hydrationInputResolver = new HydrationInputResolver(services, overallSchema, serviceExecutor, serviceExecutionHooks, hydrationInputPaths);
this.services = services;
}
public CompletableFuture execute(ExecutionContext executionContext, FieldSubSelection fieldSubSelection, ResultComplexityAggregator resultComplexityAggregator) {
long startTime = System.currentTimeMillis();
ExecutionStepInfo rootExecutionStepInfo = fieldSubSelection.getExecutionStepInfo();
NadelContext nadelContext = getNadelContext(executionContext);
OperationKind operationKind = OperationKind.fromAst(executionContext.getOperationDefinition().getOperation());
CompletableFuture> oneServiceExecutionsCF = prepareServiceExecution(executionContext, fieldSubSelection, rootExecutionStepInfo);
return oneServiceExecutionsCF.thenCompose(oneServiceExecutions -> {
Map serviceContextsByService = serviceContextsByService(oneServiceExecutions);
List> resultNodes =
executeTopLevelFields(executionContext, nadelContext, operationKind, oneServiceExecutions, resultComplexityAggregator, hydrationInputPaths);
CompletableFuture rootResult = mergeTrees(resultNodes);
return rootResult
.thenCompose(
//
// all the nodes that are hydrated need to make new service calls to get their eventual value
//
rootExecutionResultNode -> hydrationInputResolver.resolveAllHydrationInputs(executionContext, rootExecutionResultNode, serviceContextsByService, resultComplexityAggregator)
.thenApply(resultNode -> (RootExecutionResultNode) resultNode))
.whenComplete((resultNode, throwable) -> {
possiblyLogException(resultNode, throwable);
long elapsedTime = System.currentTimeMillis() - startTime;
log.debug("NadelExecutionStrategy time: {} ms, executionId: {}", elapsedTime, executionContext.getExecutionId());
});
}).whenComplete(this::possiblyLogException);
}
private Map serviceContextsByService(List oneServiceExecutions) {
Map result = new LinkedHashMap<>();
for (OneServiceExecution oneServiceExecution : oneServiceExecutions) {
result.put(oneServiceExecution.service, oneServiceExecution.serviceContext);
}
return result;
}
private CompletableFuture> prepareServiceExecution(ExecutionContext executionCtx, FieldSubSelection fieldSubSelection, ExecutionStepInfo rootExecutionStepInfo) {
List> result = new ArrayList<>();
for (MergedField mergedField : fieldSubSelection.getMergedSelectionSet().getSubFieldsList()) {
ExecutionStepInfo fieldExecutionStepInfo = executionStepInfoFactory.newExecutionStepInfoForSubField(executionCtx, mergedField, rootExecutionStepInfo);
boolean isNamespaced = !fieldExecutionStepInfo.getFieldDefinition().getDirectives(NAMESPACED_DIRECTIVE_DEFINITION.getName()).isEmpty();
boolean usesDynamicService = fieldExecutionStepInfo.getFieldDefinition().getDirective(DYNAMIC_SERVICE_DIRECTIVE_DEFINITION.getName()) != null;
if (isNamespaced) {
result.addAll(getServiceExecutionsForNamespacedField(executionCtx, rootExecutionStepInfo, mergedField, fieldExecutionStepInfo));
} else if (usesDynamicService) {
result.add(getServiceExecutionForDynamicServiceField(executionCtx, rootExecutionStepInfo, fieldExecutionStepInfo));
} else {
Service service = getServiceForFieldDefinition(fieldExecutionStepInfo.getFieldDefinition());
result.add(getOneServiceExecution(executionCtx, fieldExecutionStepInfo, service));
}
}
return Async.each(result);
}
private CompletableFuture getServiceExecutionForDynamicServiceField(ExecutionContext executionCtx, ExecutionStepInfo rootExecutionStepInfo, ExecutionStepInfo fieldExecutionStepInfo) {
ServiceOrError serviceOrError = assertNotNull(
serviceExecutionHooks.resolveServiceForField(services, fieldExecutionStepInfo),
() -> "Service resolution hook must never return null."
);
if (serviceOrError.getService() == null) {
GraphQLError graphQLError = assertNotNull(serviceOrError.getError(), () -> "Hook must return an error object when Service is null");
return CompletableFuture.completedFuture(new OneServiceExecution(null, null, null, true, graphQLError, fieldExecutionStepInfo));
}
Assert.assertTrue(
fieldExecutionStepInfo.getUnwrappedNonNullType() instanceof GraphQLInterfaceType,
() -> format("field annotated with %s directive is expected to be of GraphQLInterfaceType", DYNAMIC_SERVICE_DIRECTIVE_DEFINITION.getName())
);
Service service = serviceOrError.getService();
List serviceObjectTypes = service.getDefinitionRegistry().getDefinitions(ObjectTypeDefinition.class)
.stream()
.map(ObjectTypeDefinition::getName)
.collect(toList());
NormalizedQueryFromAst normalizedQuery = getNadelContext(executionCtx).getNormalizedOverallQuery();
List inlineFragments = wrapFieldsInInlineFragments(fieldExecutionStepInfo, serviceObjectTypes, normalizedQuery);
SelectionSet selectionSet = SelectionSet.newSelectionSet(inlineFragments).build();
Field transform = fieldExecutionStepInfo
.getField()
.getSingleField()
.transform(builder -> builder.selectionSet(selectionSet));
MergedField newMergedField = MergedField.newMergedField().addField(transform).build();
ExecutionStepInfo executionStepInfo = executionStepInfoFactory.newExecutionStepInfoForSubField(executionCtx, newMergedField, rootExecutionStepInfo);
return getOneServiceExecution(executionCtx, executionStepInfo, service);
}
private List wrapFieldsInInlineFragments(ExecutionStepInfo fieldExecutionStepInfo, List serviceObjectTypes, NormalizedQueryFromAst normalizedQuery) {
return normalizedQuery.getTopLevelFields()
.stream()
.filter(field -> field.getFieldDefinition().getName().equals(fieldExecutionStepInfo.getFieldDefinition().getName()))
.flatMap(field -> field.getChildren()
.stream()
.filter(childField -> serviceObjectTypes.contains(childField.getObjectType().getName()))
.map(normalizedQueryField -> {
TypeName typeName = newTypeName(normalizedQueryField.getObjectType().getName()).build();
return newInlineFragment()
.typeCondition(typeName)
.selectionSet(newSelectionSet().selection(normalizedQuery.getMergedFieldByNormalizedFields().get(normalizedQueryField).getSingleField()).build())
.build();
}))
.collect(toList());
}
private List> getServiceExecutionsForNamespacedField(
ExecutionContext executionCtx, ExecutionStepInfo rootExecutionStepInfo, MergedField mergedField, ExecutionStepInfo stepInfoForNamespacedField
) {
ArrayList> serviceExecutions = new ArrayList<>();
Assert.assertTrue(
stepInfoForNamespacedField.getUnwrappedNonNullType() instanceof GraphQLObjectType,
() -> "field annotated with @namespaced directive is expected to be of GraphQLObjectType");
GraphQLObjectType namespacedObjectType = (GraphQLObjectType) stepInfoForNamespacedField.getUnwrappedNonNullType();
Map> serviceSetHashMap = fieldInfos.splitObjectFieldsByServices(namespacedObjectType);
for (Map.Entry> entry : serviceSetHashMap.entrySet()) {
Service service = entry.getKey();
Set secondLevelFieldDefinitionsForService = entry.getValue();
Optional maybeNewMergedField = MergedFieldUtil.includeSubSelection(
mergedField,
namespacedObjectType,
executionCtx,
field -> secondLevelFieldDefinitionsForService
.stream()
.anyMatch(graphQLFieldDefinition ->
fieldMatchesDefinition(graphQLFieldDefinition, field) ||
(isTypename(field) && serviceOwnsNamespacedField(namespacedObjectType, service))
)
);
maybeNewMergedField.ifPresent(newMergedField -> {
ExecutionStepInfo newFieldExecutionStepInfo = executionStepInfoFactory.newExecutionStepInfoForSubField(executionCtx, newMergedField, rootExecutionStepInfo);
serviceExecutions.add(getOneServiceExecution(executionCtx, newFieldExecutionStepInfo, service));
});
}
return serviceExecutions;
}
private static boolean isTypename(MergedField field) {
return field.getName().equals(Introspection.TypeNameMetaFieldDef.getName());
}
private static boolean fieldMatchesDefinition(GraphQLFieldDefinition graphQLFieldDefinition, MergedField field) {
return graphQLFieldDefinition.getName().equals(field.getName());
}
private CompletableFuture getOneServiceExecution(ExecutionContext executionCtx, ExecutionStepInfo fieldExecutionStepInfo, Service service) {
CreateServiceContextParams parameters = CreateServiceContextParams.newParameters()
.from(executionCtx)
.service(service)
.executionStepInfo(fieldExecutionStepInfo)
.build();
CompletableFuture