diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5e749360a..b81a8f18b 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -17,6 +17,7 @@ import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.incremental.DeferredExecutionSupport; import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +import graphql.execution.instrumentation.FieldFetchingInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -487,7 +488,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); - InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, + FieldFetchingInstrumentationContext fetchCtx = FieldFetchingInstrumentationContext.nonNullCtx(instrumentation.beginFieldFetching(instrumentationFieldFetchParams, executionContext.getInstrumentationState()) ); @@ -496,6 +497,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); + fetchCtx.onFetchedValue(fetchedObject); if (fetchedObject instanceof CompletableFuture) { @SuppressWarnings("unchecked") CompletableFuture fetchedValue = (CompletableFuture) fetchedObject; diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 6decb929c..f0577aeab 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -1,7 +1,6 @@ package graphql.execution.instrumentation; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExperimentalApi; @@ -159,7 +158,7 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument } BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState); ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; - if (instrumentations.size() == 1) { + if (instrumentations.size() == 1) { return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); } return new ChainedExecuteObjectInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper)); @@ -182,11 +181,26 @@ public InstrumentationContext beginSubscribedFieldEvent(Instrum return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldExecution(parameters, specificState)); } + @SuppressWarnings("deprecation") @Override public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState)); } + @Override + public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return FieldFetchingInstrumentationContext.NOOP; + } + BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginFieldFetching(parameters, specificState); + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); + } + ImmutableList objects = chainedMapAndDropNulls(chainedInstrumentationState, mapper); + return new ChainedFieldFetchingInstrumentationContext(objects); + } + @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); @@ -344,8 +358,33 @@ public void onFieldValuesException() { } } + private static class ChainedFieldFetchingInstrumentationContext implements FieldFetchingInstrumentationContext { + + private final ImmutableList contexts; + + ChainedFieldFetchingInstrumentationContext(ImmutableList contexts) { + this.contexts = contexts; + } + + @Override + public void onDispatched() { + contexts.forEach(FieldFetchingInstrumentationContext::onDispatched); + } + + @Override + public void onFetchedValue(Object fetchedValue) { + contexts.forEach(context -> context.onFetchedValue(fetchedValue)); + } + + @Override + public void onCompleted(Object result, Throwable t) { + contexts.forEach(context -> context.onCompleted(result, t)); + } + } + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext { + private final List> contexts; ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { diff --git a/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java new file mode 100644 index 000000000..d8e51269b --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java @@ -0,0 +1,68 @@ +package graphql.execution.instrumentation; + +import graphql.Internal; +import graphql.PublicSpi; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@PublicSpi +public interface FieldFetchingInstrumentationContext extends InstrumentationContext { + + @Internal + FieldFetchingInstrumentationContext NOOP = new FieldFetchingInstrumentationContext() { + @Override + public void onDispatched() { + } + + @Override + public void onCompleted(Object result, Throwable t) { + } + + @Override + public void onFetchedValue(Object fetchedValue) { + } + }; + + /** + * This creates a no-op {@link InstrumentationContext} if the one pass in is null + * + * @param nullableContext a {@link InstrumentationContext} that can be null + * + * @return a non null {@link InstrumentationContext} that maybe a no-op + */ + @NotNull + @Internal + static FieldFetchingInstrumentationContext nonNullCtx(FieldFetchingInstrumentationContext nullableContext) { + return nullableContext == null ? NOOP : nullableContext; + } + + @Internal + static FieldFetchingInstrumentationContext adapter(@Nullable InstrumentationContext context) { + if (context == null) { + return null; + } + return new FieldFetchingInstrumentationContext() { + @Override + public void onDispatched() { + context.onDispatched(); + } + + @Override + public void onCompleted(Object result, Throwable t) { + context.onCompleted(result, t); + } + + @Override + public void onFetchedValue(Object fetchedValue) { + } + }; + } + + /** + * This is called back with value fetched for the field. + * + * @param fetchedValue a value that a field's {@link graphql.schema.DataFetcher} returned + */ + default void onFetchedValue(Object fetchedValue) { + } +} diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 977422e56..11819347a 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -196,12 +196,31 @@ default InstrumentationContext beginFieldExecution(InstrumentationFieldP * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + * + * @deprecated use {@link #beginFieldFetching(InstrumentationFieldFetchParameters, InstrumentationState)} instead */ + @Deprecated(since = "2024-04-18") @Nullable default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return noOp(); } + /** + * This is called just before a field {@link DataFetcher} is invoked. The {@link FieldFetchingInstrumentationContext#onFetchedValue(Object)} + * callback will be invoked once a value is returned by a {@link DataFetcher} but perhaps before + * its value is completed if it's a {@link CompletableFuture} value. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + InstrumentationContext ctx = beginFieldFetch(parameters, state); + return FieldFetchingInstrumentationContext.adapter(ctx); + } + /** * This is called just before the complete field is started. * diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java index 1928df84f..8d8d3cd95 100644 --- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java @@ -98,6 +98,11 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState)); } + @Override + public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetching(parameters, specificState)); + } + @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java index dfffa5b72..b46cedf9c 100644 --- a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -102,7 +102,6 @@ public class SimplePerformantInstrumentation implements Instrumentation { return noOp(); } - @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return noOp(); diff --git a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy index 5d6fbb1d8..822d08f5a 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy @@ -86,9 +86,9 @@ class ModernTestingInstrumentation implements Instrumentation { } @Override - InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { assert state == instrumentationState - return new TestingInstrumentContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) + return new TestingFieldFetchingInstrumentationContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy new file mode 100644 index 000000000..02cbf9d92 --- /dev/null +++ b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy @@ -0,0 +1,9 @@ +package graphql.execution.instrumentation + +class TestingFieldFetchingInstrumentationContext extends TestingInstrumentContext> implements FieldFetchingInstrumentationContext { + + TestingFieldFetchingInstrumentationContext(Object op, Object executionList, Object throwableList, Boolean useOnDispatch) { + super(op, executionList, throwableList, useOnDispatch) + } +} +