Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LightOperationExecutor support for parameterless operations #502

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions src/main/java/io/leangen/graphql/execution/LightOperationExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package io.leangen.graphql.execution;

import graphql.GraphQLException;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.LightDataFetcher;
import io.leangen.graphql.generator.mapping.ArgumentInjector;
import io.leangen.graphql.generator.mapping.ConverterRegistry;
import io.leangen.graphql.generator.mapping.DelegatingOutputConverter;
import io.leangen.graphql.metadata.Operation;
import io.leangen.graphql.metadata.Resolver;
import io.leangen.graphql.metadata.strategy.value.ValueMapper;
import io.leangen.graphql.util.Utils;
import org.dataloader.BatchLoaderEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
* Created by soumik.dutta on 9/21/24.
* <p>
* LightOperationExecutor is a performance improvement over OperationExecutor for simple queries as it implements LightDataFetcher interface from GraphQL-Java <a href="https://github.com/graphql-java/graphql-java/pull/2953">#2953</a>
* <br>
* LightDataFetcher implementations do not require DataFetchingEnvironment for getting values and are considered cheaper to execute as DFE is not constructed for the query
*/
public class LightOperationExecutor extends OperationExecutor implements LightDataFetcher<Object> {

private static final Logger log = LoggerFactory.getLogger(LightOperationExecutor.class);
private final Operation operation;
private final ValueMapper valueMapper;
private final GlobalEnvironment globalEnvironment;
private final ConverterRegistry converterRegistry;
private final DerivedTypeRegistry derivedTypes;
private final Map<Resolver, List<ResolverInterceptor>> innerInterceptors;
private final Map<Resolver, List<ResolverInterceptor>> outerInterceptors;
private final Map<String, Object> NO_ARGUMENTS = new HashMap<>();


public LightOperationExecutor(Operation operation, ValueMapper valueMapper, GlobalEnvironment globalEnvironment, ResolverInterceptorFactory interceptorFactory) {
super(operation, valueMapper, globalEnvironment, interceptorFactory);
this.operation = operation;
this.valueMapper = valueMapper;
this.globalEnvironment = globalEnvironment;
this.converterRegistry = optimizeConverters(operation.getResolvers(), globalEnvironment.converters);
this.derivedTypes = deriveTypes(operation.getResolvers(), converterRegistry);
this.innerInterceptors = operation.getResolvers()
.stream()
.collect(Collectors.toMap(Function.identity(),
res -> interceptorFactory.getInterceptors(new ResolverInterceptorFactoryParams(res))));
this.outerInterceptors = operation.getResolvers()
.stream()
.collect(Collectors.toMap(Function.identity(),
res -> interceptorFactory.getOuterInterceptors(new ResolverInterceptorFactoryParams(res))));
}

@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
throw (T) t;
}

@Override
public Object get(GraphQLFieldDefinition fieldDefinition, Object sourceObject, Supplier<DataFetchingEnvironment> env) throws Exception {
// TODO: fix setting clientMutationId as this requires materialized DataFetchingEnvironment, something we are avoiding in LightOperationExecutor
// ContextUtils.setClientMutationId(env.getContext(), env.getArgument(CLIENT_MUTATION_ID));

Resolver resolver = this.operation.getApplicableResolver(NO_ARGUMENTS.keySet());
if (resolver == null) {
throw new GraphQLException(
"Resolver for operation " + operation.getName() + " accepting arguments: " + NO_ARGUMENTS.keySet() + " not implemented");
}
ResolutionEnvironment resolutionEnvironment = new ResolutionEnvironment(resolver, env, this.valueMapper, this.globalEnvironment,
this.converterRegistry, this.derivedTypes);
return execute(resolver, resolutionEnvironment, sourceObject);
}

@Override
public Object get(DataFetchingEnvironment env) throws Exception {
log.warn(
"LightDataFetcher should not be called with materialised dfe. \nThis should be marked as not implemented. However using DataLoaderDispatcherInstrumentation causes problem with the `instance of LightDataFetcher` check in ExecutionStrategy in GraphQL-Java");
return super.get(env);
}

public Object execute(List<Object> keys, Map<String, Object> arguments, BatchLoaderEnvironment env) throws Exception {
Resolver resolver = this.operation.getApplicableResolver(arguments.keySet());
if (resolver == null) {
throw new GraphQLException("Batch loader for operation " + operation.getName() + " not implemented");
}
ResolutionEnvironment resolutionEnvironment = new ResolutionEnvironment(resolver, keys, env, this.valueMapper, this.globalEnvironment,
this.converterRegistry, this.derivedTypes);
return execute(resolver, resolutionEnvironment, arguments);
}

/**
* Prepares input arguments by calling respective {@link ArgumentInjector}s
* and invokes the underlying resolver method/field
*
* @param resolver The resolver to be invoked once the arguments are prepared
* @param resolutionEnvironment An object containing all contextual information needed during operation resolution
* @param sourceObject Object of which the method is being called
* @return The result returned by the underlying method/field, potentially proxied and wrapped
* @throws Exception If the invocation of the underlying method/field or any of the interceptors throws
*/
private Object execute(Resolver resolver, ResolutionEnvironment resolutionEnvironment, Object sourceObject) throws Exception {
final Object[] NO_ARGUMENTS = new Object[]{};
InvocationContext invocationContext = new InvocationContext(operation, resolver, resolutionEnvironment, NO_ARGUMENTS);
Queue<ResolverInterceptor> interceptors = new ArrayDeque<>(this.outerInterceptors.get(resolver));
interceptors.add(
(ctx, cont) -> resolutionEnvironment.convertOutput(cont.proceed(ctx), resolver.getTypedElement(), resolver.getReturnType()));
interceptors.addAll(this.innerInterceptors.get(resolver));
interceptors.add((ctx, cont) -> {
try {
return resolver.resolve(sourceObject, NO_ARGUMENTS);
} catch (ReflectiveOperationException e) {
sneakyThrow(unwrap(e));
}
return null; //never happens, needed because of sneakyThrow
});
return execute(invocationContext, interceptors);
}

private Object execute(InvocationContext context, Queue<ResolverInterceptor> interceptors) throws Exception {
return interceptors.remove().aroundInvoke(context, (ctx) -> execute(ctx, interceptors));
}

private ConverterRegistry optimizeConverters(Collection<Resolver> resolvers, ConverterRegistry converters) {
return converters.optimize(resolvers.stream().map(Resolver::getTypedElement).collect(Collectors.toList()));
}

private DerivedTypeRegistry deriveTypes(Collection<Resolver> resolvers, ConverterRegistry converterRegistry) {
return new DerivedTypeRegistry(resolvers.stream().map(Resolver::getTypedElement).collect(Collectors.toList()),
Utils.extractInstances(converterRegistry.getOutputConverters(), DelegatingOutputConverter.class).collect(Collectors.toList()));
}

private Throwable unwrap(ReflectiveOperationException e) {
Throwable cause = e.getCause();
if (cause != null && cause != e) {
return cause;
}
return e;
}

public Operation getOperation() {
return operation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import graphql.execution.DataFetcherResult;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLFieldDefinition;
import io.leangen.graphql.generator.mapping.ArgumentInjector;
import io.leangen.graphql.generator.mapping.ConverterRegistry;
import io.leangen.graphql.generator.mapping.DelegatingOutputConverter;
Expand All @@ -13,10 +14,12 @@
import io.leangen.graphql.metadata.strategy.value.ValueMapper;
import io.leangen.graphql.util.ContextUtils;
import io.leangen.graphql.util.Utils;
import jdk.jshell.spi.ExecutionControl;
import org.dataloader.BatchLoaderEnvironment;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static io.leangen.graphql.util.GraphQLUtils.CLIENT_MUTATION_ID;
Expand Down Expand Up @@ -46,6 +49,11 @@ public OperationExecutor(Operation operation, ValueMapper valueMapper, GlobalEnv
res -> interceptorFactory.getOuterInterceptors(new ResolverInterceptorFactoryParams(res))));
}

public Object get(GraphQLFieldDefinition fieldDefinition, Object sourceObject, Supplier<DataFetchingEnvironment> env) throws Exception {
throw new ExecutionControl.NotImplementedException(
"OperationExecutor does not implement fetching DataFetchingEnvironment using a Supplier. Use LightOperationExecutor instead.");
}

@Override
public Object get(DataFetchingEnvironment env) throws Exception {
ContextUtils.setClientMutationId(env.getContext(), env.getArgument(CLIENT_MUTATION_ID));
Expand Down Expand Up @@ -142,4 +150,4 @@ private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
public Operation getOperation() {
return operation;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
import graphql.GraphqlErrorBuilder;
import graphql.execution.ExecutionStepInfo;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
import io.leangen.graphql.generator.mapping.ArgumentInjectorParams;
import io.leangen.graphql.generator.mapping.ConverterRegistry;
import io.leangen.graphql.generator.mapping.DelegatingOutputConverter;
Expand All @@ -25,6 +22,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
* @author Bojan Tomic (kaqqao)
Expand All @@ -38,10 +36,11 @@ public class ResolutionEnvironment {
public final Resolver resolver;
public final ValueMapper valueMapper;
public final GlobalEnvironment globalEnvironment;
public final GraphQLOutputType fieldType;
public final GraphQLNamedType parentType;
public final GraphQLSchema graphQLSchema;
public final DataFetchingEnvironment dataFetchingEnvironment;

private final DataFetchingEnvironment dataFetchingEnvironment;
private final Supplier<DataFetchingEnvironment> dataFetchingEnvironmentSupplier;
private final boolean isDataFetchingEnvironmentSupplied;

public final BatchLoaderEnvironment batchLoaderEnvironment;
public final Map<String, Object> arguments;
public final List<GraphQLError> errors;
Expand All @@ -58,10 +57,11 @@ public ResolutionEnvironment(Resolver resolver, DataFetchingEnvironment env, Val
this.resolver = resolver;
this.valueMapper = valueMapper;
this.globalEnvironment = globalEnvironment;
this.fieldType = env.getFieldType();
this.parentType = (GraphQLNamedType) env.getParentType();
this.graphQLSchema = env.getGraphQLSchema();

this.dataFetchingEnvironment = env;
this.dataFetchingEnvironmentSupplier = null;
this.isDataFetchingEnvironmentSupplied = false;

this.batchLoaderEnvironment = null;
this.arguments = new HashMap<>();
this.errors = new ArrayList<>();
Expand All @@ -79,17 +79,43 @@ public ResolutionEnvironment(Resolver resolver, List<Object> keys, BatchLoaderEn
this.resolver = resolver;
this.valueMapper = valueMapper;
this.globalEnvironment = globalEnvironment;
this.fieldType = inner != null ? inner.getFieldType() : null;
this.parentType = inner != null ? (GraphQLNamedType) inner.getParentType() : null;
this.graphQLSchema = inner != null ? inner.getGraphQLSchema() : null;

this.dataFetchingEnvironment = null;
this.dataFetchingEnvironmentSupplier = null;
this.isDataFetchingEnvironmentSupplied = false;

this.batchLoaderEnvironment = env;
this.arguments = new HashMap<>();
this.errors = new ArrayList<>();
this.converters = converters;
this.derivedTypes = derivedTypes;
}

public ResolutionEnvironment(Resolver resolver, Supplier<DataFetchingEnvironment> env, ValueMapper valueMapper, GlobalEnvironment globalEnvironment,
ConverterRegistry converters, DerivedTypeRegistry derivedTypes) {

this.context = null;
this.rootContext = null;
this.batchContext = null;
this.resolver = resolver;
this.valueMapper = valueMapper;
this.globalEnvironment = globalEnvironment;

this.dataFetchingEnvironment = null;
this.dataFetchingEnvironmentSupplier = env;
this.isDataFetchingEnvironmentSupplied = true;

this.batchLoaderEnvironment = null;
this.arguments = new HashMap<>();
this.errors = new ArrayList<>();
this.converters = converters;
this.derivedTypes = derivedTypes;
}

public final DataFetchingEnvironment getDataFetchingEnvironment() {
return isDataFetchingEnvironmentSupplied ? dataFetchingEnvironmentSupplier.get() : dataFetchingEnvironment;
}

public <T, S> S convertOutput(T output, AnnotatedElement element, AnnotatedType type) {
if (output == null) {
return null;
Expand Down Expand Up @@ -120,7 +146,7 @@ public List<AnnotatedType> getDerived(AnnotatedType type) {
}

public Object getInputValue(Object input, OperationArgument argument) {
boolean argValuePresent = dataFetchingEnvironment != null && dataFetchingEnvironment.containsArgument(argument.getName());
boolean argValuePresent = getDataFetchingEnvironment() != null && getDataFetchingEnvironment().containsArgument(argument.getName());
ArgumentInjectorParams params = new ArgumentInjectorParams(input, argValuePresent, argument, this);
Object value = this.globalEnvironment.injectors.getInjector(argument.getJavaType(), argument.getParameter()).getArgumentValue(params);
if (argValuePresent) {
Expand All @@ -134,8 +160,8 @@ public void addError(String message, Object... formatArgs) {
}

public GraphQLError createError(String message, Object... formatArgs) {
GraphqlErrorBuilder<?> builder = dataFetchingEnvironment != null
? GraphqlErrorBuilder.newError(dataFetchingEnvironment)
GraphqlErrorBuilder<?> builder = getDataFetchingEnvironment() != null
? GraphqlErrorBuilder.newError(getDataFetchingEnvironment())
: GraphqlErrorBuilder.newError();
return builder
.message(message, formatArgs)
Expand All @@ -144,7 +170,7 @@ public GraphQLError createError(String message, Object... formatArgs) {
}

public Directives getDirectives(ExecutionStepInfo step) {
return new Directives(dataFetchingEnvironment, step);
return new Directives(getDataFetchingEnvironment(), step);
}

public Directives getDirectives() {
Expand All @@ -156,8 +182,8 @@ public Object getGlobalContext() {
}

public boolean isSubscription() {
return dataFetchingEnvironment != null
&& dataFetchingEnvironment.getParentType() == dataFetchingEnvironment.getGraphQLSchema().getSubscriptionType();
return getDataFetchingEnvironment() != null
&& getDataFetchingEnvironment().getParentType() == getDataFetchingEnvironment().getGraphQLSchema().getSubscriptionType();
}

private static DataFetchingEnvironment dataFetchingEnvironment(List<Object> keyContexts) {
Expand All @@ -166,4 +192,4 @@ private static DataFetchingEnvironment dataFetchingEnvironment(List<Object> keyC
}
return (DataFetchingEnvironment) keyContexts.get(0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import graphql.schema.*;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.annotations.GraphQLId;
import io.leangen.graphql.execution.LightOperationExecutor;
import io.leangen.graphql.execution.OperationExecutor;
import io.leangen.graphql.generator.mapping.TypeMapper;
import io.leangen.graphql.generator.mapping.TypeMappingEnvironment;
Expand Down Expand Up @@ -406,7 +407,11 @@ private DataFetcher<?> createResolver(String parentType, Operation operation, Bu
.filter(OperationArgument::isMappable)
.map(OperationArgument::getJavaType);
ValueMapper valueMapper = buildContext.createValueMapper(inputTypes);
OperationExecutor executor = new OperationExecutor(operation, valueMapper, buildContext.globalEnvironment, buildContext.interceptorFactory);
OperationExecutor executor = operation.getArguments().isEmpty() ?
new LightOperationExecutor(operation, valueMapper, buildContext.globalEnvironment,
buildContext.interceptorFactory) :
new OperationExecutor(operation, valueMapper, buildContext.globalEnvironment,
buildContext.interceptorFactory);
if (operation.isBatched()) {
String loaderName = parentType + ':' + operation.getName();
BatchLoaderWithContext<?, ?> batchLoader = batchloaderFactory.createBatchLoader(executor);
Expand Down
Loading