Skip to content
10 changes: 10 additions & 0 deletions src/main/java/graphql/execution/Execution.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import graphql.execution.instrumentation.dataloader.PerLevelDataLoaderDispatchStrategy;
import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationReactiveResultsParameters;
import graphql.execution.reactive.ReactiveSupport;
import graphql.extensions.ExtensionsBuilder;
import graphql.incremental.DelayedIncrementalPartialResult;
import graphql.incremental.IncrementalExecutionResultImpl;
Expand Down Expand Up @@ -245,9 +247,17 @@ private CompletableFuture<ExecutionResult> incrementalSupport(ExecutionContext e
return result.thenApply(er -> {
IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState();
if (incrementalCallState.getIncrementalCallsDetected()) {
InstrumentationReactiveResultsParameters parameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.DEFER);
InstrumentationContext<Void> ctx = nonNullCtx(executionContext.getInstrumentation().beginReactiveResults(parameters, executionContext.getInstrumentationState()));

// we start the rest of the query now to maximize throughput. We have the initial important results,
// and now we can start the rest of the calls as early as possible (even before someone subscribes)
Publisher<DelayedIncrementalPartialResult> publisher = incrementalCallState.startDeferredCalls();
ctx.onDispatched();

//
// wrap this Publisher into one that can call us back when the publishing is done either in error or successful
publisher = ReactiveSupport.whenPublisherFinishes(publisher, throwable -> ctx.onCompleted(null, throwable));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - we delegate to the original Publisher but the callback here is done when the Publisher is finished


return IncrementalExecutionResultImpl.fromExecutionResult(er)
// "hasNext" can, in theory, be "false" when all the incremental items are delivered in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.execution.instrumentation.parameters.InstrumentationReactiveResultsParameters;
import graphql.execution.reactive.SubscriptionPublisher;
import graphql.language.Field;
import graphql.schema.GraphQLFieldDefinition;
Expand Down Expand Up @@ -77,7 +78,13 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
}
Function<Object, CompletionStage<ExecutionResult>> mapperFunction = eventPayload -> executeSubscriptionEvent(executionContext, parameters, eventPayload);
boolean keepOrdered = keepOrdered(executionContext.getGraphQLContext());
SubscriptionPublisher mapSourceToResponse = new SubscriptionPublisher(publisher, mapperFunction, keepOrdered);

InstrumentationReactiveResultsParameters instrumentationReactiveResultsParameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.SUBSCRIPTION);
InstrumentationContext<Void> reactiveCtx = nonNullCtx(executionContext.getInstrumentation().beginReactiveResults(instrumentationReactiveResultsParameters, executionContext.getInstrumentationState()));
reactiveCtx.onDispatched();

SubscriptionPublisher mapSourceToResponse = new SubscriptionPublisher(publisher, mapperFunction, keepOrdered,
throwable -> reactiveCtx.onCompleted(null, throwable));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we pass in the call back here. Why?

We want the SubscriptionPublisher to be the implementation we pass back - we have tests for this.

So I changed SubscriptionPublisher internally to know when its finished

return new ExecutionResultImpl(mapSourceToResponse, executionContext.getErrors());
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.execution.instrumentation.parameters.InstrumentationReactiveResultsParameters;
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
import graphql.language.Document;
import graphql.schema.DataFetcher;
Expand Down Expand Up @@ -137,6 +138,10 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrumenta
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState));
}

@Override
public @Nullable InstrumentationContext<Void> beginReactiveResults(InstrumentationReactiveResultsParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginReactiveResults(parameters, specificState));
}

@Override
public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.execution.instrumentation.parameters.InstrumentationReactiveResultsParameters;
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
import graphql.language.Document;
import graphql.schema.DataFetcher;
Expand Down Expand Up @@ -120,6 +121,21 @@ default InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrument
return noOp();
}

/**
* This is called just before the execution of any reactive results, namely incremental deferred results or subscriptions. When the {@link org.reactivestreams.Publisher}
* finally ends (with either a {@link Throwable} or none) then the {@link InstrumentationContext} wil be called back to say the reactive results
* have finished.
*
* @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 InstrumentationContext<Void> beginReactiveResults(InstrumentationReactiveResultsParameters parameters, InstrumentationState state) {
return noOp();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how its Void - the end of a Publisher has no result object - it has many objects but there is no actual result at the end


/**
* This is called each time an {@link graphql.execution.ExecutionStrategy} is invoked, which may be multiple times
* per query as the engine recursively descends over the query.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.execution.instrumentation.parameters.InstrumentationReactiveResultsParameters;
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
import graphql.language.Document;
import graphql.validation.ValidationError;
Expand Down Expand Up @@ -73,6 +74,11 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrumenta
return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState));
}

@Override
public @Nullable InstrumentationContext<Void> beginReactiveResults(InstrumentationReactiveResultsParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginReactiveResults(parameters, specificState));
}

@Override
public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecutionStrategy(parameters, specificState));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package graphql.execution.instrumentation.parameters;

import graphql.PublicApi;
import graphql.execution.ExecutionContext;
import graphql.execution.instrumentation.Instrumentation;
import org.jspecify.annotations.NullMarked;

/**
* Parameters sent to {@link Instrumentation} methods
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@PublicApi
@NullMarked
public class InstrumentationReactiveResultsParameters {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New parameters for the Instrumentation method


/**
* What type of reactive results was the {@link org.reactivestreams.Publisher}
*/
public enum ResultType {
DEFER, SUBSCRIPTION
}

private final ExecutionContext executionContext;
private final ResultType resultType;

public InstrumentationReactiveResultsParameters(ExecutionContext executionContext, ResultType resultType) {
this.executionContext = executionContext;
this.resultType = resultType;
}


public ExecutionContext getExecutionContext() {
return executionContext;
}

public ResultType getResultType() {
return resultType;
}
}
54 changes: 54 additions & 0 deletions src/main/java/graphql/execution/reactive/ReactiveSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import graphql.Internal;
import org.reactivestreams.FlowAdapters;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
* This provides support for a DataFetcher to be able to
Expand Down Expand Up @@ -141,4 +144,55 @@ public void onComplete() {
onCompleteImpl();
}
}

/**
* Our reactive {@link SingleSubscriberPublisher} supports only a single subscription
* so this can be used a delegate to perform a call back when the given Publisher
* actually finishes without adding an extra subscription to the delegate Publisher
*
* @param publisher the publisher to wrap
* @param atTheEndCallback the callback when the {@link Publisher} has finished
* @param <T> for two
*/
public static <T> Publisher<T> whenPublisherFinishes(Publisher<T> publisher, Consumer<? super Throwable> atTheEndCallback) {
return new AtTheEndPublisher<>(publisher, atTheEndCallback);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper allows us to wrap a Publisher so that a callback is made when its finished.


static class AtTheEndPublisher<T> implements Publisher<T> {

private final Publisher<T> delegatePublisher;
private final Consumer<? super Throwable> atTheEndCallback;

public AtTheEndPublisher(Publisher<T> delegatePublisher, Consumer<? super Throwable> atTheEndCallback) {
this.delegatePublisher = delegatePublisher;
this.atTheEndCallback = atTheEndCallback;
}

@Override
public void subscribe(Subscriber<? super T> originalSubscriber) {
delegatePublisher.subscribe(new Subscriber<>() {
@Override
public void onSubscribe(Subscription s) {
originalSubscriber.onSubscribe(s);
}

@Override
public void onNext(T t) {
originalSubscriber.onNext(t);
}

@Override
public void onError(Throwable t) {
originalSubscriber.onError(t);
atTheEndCallback.accept(t);
}

@Override
public void onComplete() {
originalSubscriber.onComplete();
atTheEndCallback.accept(null);
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.reactivestreams.Subscriber;

import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;


Expand All @@ -25,6 +26,7 @@
public class SubscriptionPublisher implements Publisher<ExecutionResult> {

private final CompletionStageMappingPublisher<ExecutionResult, Object> mappingPublisher;
private final Publisher<ExecutionResult> publisher;

/**
* Subscription consuming code is not expected to create instances of this class
Expand All @@ -34,12 +36,13 @@ public class SubscriptionPublisher implements Publisher<ExecutionResult> {
* @param keepOrdered this indicates that the order of results should be kep in the same order as the source events arrive
*/
@Internal
public SubscriptionPublisher(Publisher<Object> upstreamPublisher, Function<Object, CompletionStage<ExecutionResult>> mapper, boolean keepOrdered) {
public SubscriptionPublisher(Publisher<Object> upstreamPublisher, Function<Object, CompletionStage<ExecutionResult>> mapper, boolean keepOrdered, Consumer<Throwable> whenDone) {
if (keepOrdered) {
mappingPublisher = new CompletionStageMappingOrderedPublisher<>(upstreamPublisher, mapper);
} else {
mappingPublisher = new CompletionStageMappingPublisher<>(upstreamPublisher, mapper);
}
publisher = ReactiveSupport.whenPublisherFinishes(mappingPublisher, whenDone);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we now delegate and invoke the whenDone callback

}

/**
Expand All @@ -52,6 +55,6 @@ public Publisher<Object> getUpstreamPublisher() {

@Override
public void subscribe(Subscriber<? super ExecutionResult> subscriber) {
mappingPublisher.subscribe(subscriber);
publisher.subscribe(subscriber);
}
}
75 changes: 75 additions & 0 deletions src/test/groovy/graphql/TestUtil.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,81 @@ class TestUtil {
}


/**
* Helper to say that a sub list is contained inside rhe master list in order for its entire length
*
* @param source the source list to check
* @param target the target list
* @return true if the target lists are inside the source list in order
*/
static <T> boolean listContainsInOrder(List<T> source, List<T> target, List<T>... targets) {
def index = indexOfSubListFrom(0, source, target)
if (index == -1) {
return false
}
for (List<T> list : targets) {
index = indexOfSubListFrom(index, source, list)
if (index == -1) {
return false
}
}
return true
}

/**
* Finds the index of the target list inside the source list starting from the specified index
*
* @param startIndex the starting index
* @param source the source list
* @param target the target list
* @return the index of the target list or -1
*/
static <T> int indexOfSubListFrom(int startIndex, List<T> source, List<T> target) {
def subListSize = target.size()
def masterListSize = source.size()
if (masterListSize < subListSize) {
return -1
}
if (target.isEmpty() || source.isEmpty()) {
return -1
}
for (int i = startIndex; i < masterListSize; i++) {
// starting at each index look for the sub list
if (i + subListSize > masterListSize) {
return -1
}

boolean matches = true
for (int j = 0; j < subListSize; j++) {
T sub = target.get(j)
T m = source.get(i + j)
if (!eq(sub, m)) {
matches = false
break
}
}
if (matches) {
return i
}
}
return -1
}

private static <T> boolean eq(T t1, T t2) {
if (t1 == null && t2 == null) {
return true
}
if (t1 != null && t2 != null) {
return t1 == t2
}
return false
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make it easier to check large list of values for ordered things. Helpful with Instrumentation "step" testing



static <T> T last(List<T> list) {
return list.get(list.size()-1)
}

static List<Map<String, Object>> getIncrementalResults(IncrementalExecutionResult initialResult) {
Publisher<DelayedIncrementalPartialResult> deferredResultStream = initialResult.incrementalItemPublisher

Expand Down
Loading
Loading