-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Adding Instrumentation for reactive results and when they finish #4084
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
Changes from all commits
ca20654
4b85ef6
9bd6f02
aff94ba
878049a
d064c7f
ea5defc
598be6a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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)); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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(); | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
| 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 { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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); | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| import org.reactivestreams.Subscriber; | ||
|
|
||
| import java.util.concurrent.CompletionStage; | ||
| import java.util.function.Consumer; | ||
| import java.util.function.Function; | ||
|
|
||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we now delegate and invoke the whenDone callback |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -52,6 +55,6 @@ public Publisher<Object> getUpstreamPublisher() { | |
|
|
||
| @Override | ||
| public void subscribe(Subscriber<? super ExecutionResult> subscriber) { | ||
| mappingPublisher.subscribe(subscriber); | ||
| publisher.subscribe(subscriber); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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