From dd8313b7580120c78c76322c4d50557e08ee66ac Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Mon, 17 Nov 2025 09:48:28 +1100 Subject: [PATCH 1/6] GQLGW-5297-optimise-incremental-part-execution-for-defer-requests --- .../java/graphql/execution/Execution.java | 32 ++++ .../incremental/IncrementalCallState.java | 20 +++ .../IncrementalExecutionContextKeys.java | 33 +++++ ...eferExecutionSupportIntegrationTest.groovy | 139 ++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 38c82f5a53..7b6083db04 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -26,6 +26,7 @@ import graphql.extensions.ExtensionsBuilder; import graphql.incremental.DelayedIncrementalPartialResult; import graphql.incremental.IncrementalExecutionResultImpl; +import graphql.execution.incremental.IncrementalExecutionContextKeys; import graphql.language.Directive; import graphql.language.Document; import graphql.language.NodeUtil; @@ -48,6 +49,7 @@ import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; import static graphql.execution.ExecutionStrategyParameters.newParameters; +import static graphql.execution.incremental.IncrementalExecutionContextKeys.EAGER_DEFER_PUBLISHER; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -218,6 +220,27 @@ private CompletableFuture executeOperation(ExecutionContext exe DataLoaderDispatchStrategy dataLoaderDispatchStrategy = createDataLoaderDispatchStrategy(executionContext, executionStrategy); executionContext.setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); result = executionStrategy.execute(executionContext, parameters); + + if (executionContext.hasIncrementalSupport() && + executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { + final CompletableFuture mainResult = result; + CompletableFuture.runAsync(() -> { + IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); + CompletableFuture.anyOf(incrementalCallState.getIncrementalCallsDetectedFuture(), mainResult).join(); + + if (incrementalCallState.getIncrementalCallsDetected()) { + InstrumentationReactiveResultsParameters resultsParameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.DEFER); + InstrumentationContext ctx = nonNullCtx(executionContext.getInstrumentation().beginReactiveResults(resultsParameters, executionContext.getInstrumentationState())); + Publisher publisher = incrementalCallState.startDeferredCalls(); + ctx.onDispatched(); + + publisher = ReactiveSupport.whenPublisherFinishes(publisher, throwable -> ctx.onCompleted(null, throwable)); + executionContext.getGraphQLContext().put(EAGER_DEFER_PUBLISHER, publisher); + + incrementalCallState.startDrainingNow(); + } + }); + } } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type // up to the root object type and there was a null value somewhere. @@ -245,6 +268,15 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture incrementalSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { + // If we've aready started the deferred publisher early, attach it now without re-instrumenting. + Object maybePublisher = executionContext.getGraphQLContext().get(EAGER_DEFER_PUBLISHER); + if (maybePublisher instanceof Publisher) { + Publisher publisher = (Publisher) maybePublisher; + return IncrementalExecutionResultImpl.fromExecutionResult(er) + .hasNext(true) + .incrementalItemPublisher(publisher) + .build(); + } IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); if (incrementalCallState.getIncrementalCallsDetected()) { InstrumentationReactiveResultsParameters parameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.DEFER); diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index f2c0b9dbc7..a305a29073 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -16,6 +16,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import java.util.concurrent.CompletableFuture; + import static graphql.incremental.DelayedIncrementalPartialResultImpl.newIncrementalExecutionResult; /** @@ -30,6 +32,9 @@ public class IncrementalCallState { private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); + // Completed when the very first incremental call is detected/enqueued + private final CompletableFuture incrementalCallsDetectedFuture = new CompletableFuture<>(); + @SuppressWarnings("FutureReturnValueIgnored") private void drainIncrementalCalls() { IncrementalCall incrementalCall = incrementalCalls.poll(); @@ -76,6 +81,9 @@ public void enqueue(IncrementalCall incrementalCal incrementalCallsDetected.set(true); incrementalCalls.offer(incrementalCall); pendingCalls.incrementAndGet(); + if (!incrementalCallsDetectedFuture.isDone()) { + incrementalCallsDetectedFuture.complete(null); + } }); } @@ -87,6 +95,13 @@ public boolean getIncrementalCallsDetected() { return incrementalCallsDetected.get(); } + /** + * @return a future that completes when the first incremental call is detected. + */ + public CompletableFuture getIncrementalCallsDetectedFuture() { + return incrementalCallsDetectedFuture; + } + private Supplier> createPublisher() { // this will be created once and once only - any extra calls to .get() will return the previously created // singleton object @@ -104,4 +119,9 @@ public Publisher startDeferredCalls() { return publisher.get(); } + public void startDrainingNow() { + publisher.get(); + drainIncrementalCalls(); + } + } diff --git a/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java b/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java new file mode 100644 index 0000000000..d685516a99 --- /dev/null +++ b/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java @@ -0,0 +1,33 @@ +package graphql.execution.incremental; + + +import graphql.GraphQLContext; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +/** + * GraphQLContext keys for controlling incremental execution behavior. + */ +@Internal +@NullMarked +public final class IncrementalExecutionContextKeys { + private IncrementalExecutionContextKeys() { + } + + /** + * Enables eager start of @defer processing so defered work runs before the initial result is computed. + * Defaults to false. + *

+ * Expects a boolean value. + */ + public static final String ENABLE_EAGER_DEFER_START = "__GJ_enable_eager_defer_start"; + + + /** + * Stores the Publisher used for incremental delivery when eager defer is enabled. + * Value type: org.reactivestreams.Publisher + */ + public static final String EAGER_DEFER_PUBLISHER = "__GJ_eager_defer_publisher"; +} + + diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index b3b522d90b..b86e972c64 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -7,6 +7,7 @@ import graphql.ExecutionResult import graphql.ExperimentalApi import graphql.GraphQL import graphql.GraphqlErrorBuilder +import graphql.GraphQLContext import graphql.TestUtil import graphql.execution.DataFetcherResult import graphql.execution.pubsub.CapturingSubscriber @@ -27,6 +28,8 @@ import spock.lang.Specification import spock.lang.Unroll import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring @@ -1726,6 +1729,142 @@ class DeferExecutionSupportIntegrationTest extends Specification { } + def "eager defer starts before initial result completes when ENABLE_EAGER_DEFER_START"() { + given: + def deferStarted = new CountDownLatch(1) + def allowDeferredComplete = new CountDownLatch(1) + + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(newTypeWiring("Query") + .dataFetcher("post", resolve([id: "1001"])) + ) + .type(newTypeWiring("Query").dataFetcher("hello", resolve("world", 4000))) + .type(newTypeWiring("Post").dataFetcher("summary", { env -> + deferStarted.countDown() + allowDeferredComplete.await(2, TimeUnit.SECONDS) + CompletableFuture.completedFuture("A summary") + } as DataFetcher)) + .type(newTypeWiring("Item").typeResolver(itemTypeResolver())) + .build() + + def schema = TestUtil.schema(schemaSpec, runtimeWiring) + .transform({ b -> b.additionalDirective(Directives.DeferDirective) }) + def testGraphQL = GraphQL.newGraphQL(schema).build() + + def ctx = GraphQLContext.newContext().build() + ctx.put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) + ctx.put(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, true) + + def query = ''' + query { + hello + ... @defer { post { summary } } + } + ''' + + when: + def executionInput = ExecutionInput.newExecutionInput() + .graphQLContext([(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT): true, (IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START): true]) + .query(query) + .build() + def execFuture = CompletableFuture.supplyAsync { + testGraphQL.execute(executionInput) + } + + then: + // Deferred fetcher starts while initial result is still computing + assert deferStarted.await(2000, TimeUnit.MILLISECONDS) + assert !execFuture.isDone() + + when: + allowDeferredComplete.countDown() + def initialResult = execFuture.join() as IncrementalExecutionResult + + then: + assert initialResult.toSpecification() == [ + data : [hello: "world"], + hasNext: true + ] + + when: + def sub = new CapturingSubscriber() + initialResult.incrementalItemPublisher.subscribe(sub) + + then: + Awaitility.await().untilTrue(sub.isDone()) + } + + + def "incremental starts only after initial result when eager start disabled"() { + given: + def deferStarted = new CountDownLatch(1) + def allowDeferredComplete = new CountDownLatch(1) + + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(newTypeWiring("Query") + .dataFetcher("post", resolve([id: "1001"])) + ) + .type(newTypeWiring("Query").dataFetcher("hello", resolve("world", 300))) + .type(newTypeWiring("Post").dataFetcher("summary", { env -> + deferStarted.countDown() + allowDeferredComplete.await(2, TimeUnit.SECONDS) + CompletableFuture.completedFuture("A summary") + } as DataFetcher)) + .type(newTypeWiring("Item").typeResolver(itemTypeResolver())) + .build() + + def schema = TestUtil.schema(schemaSpec, runtimeWiring) + .transform({ b -> b.additionalDirective(Directives.DeferDirective) }) + def testGraphQL = GraphQL.newGraphQL(schema).build() + + def query = ''' + query { + hello + ... @defer { post { summary } } + } + ''' + + when: + def executionInput = ExecutionInput.newExecutionInput() + .graphQLContext([(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT): true]) // no eager flag + .query(query) + .build() + def execFuture = CompletableFuture.supplyAsync { + testGraphQL.execute(executionInput) + } + + then: + assert !deferStarted.await(100, TimeUnit.MILLISECONDS) + assert !execFuture.isDone() + + when: + def initialResult = execFuture.join() as IncrementalExecutionResult + + then: + assert initialResult.toSpecification() == [ + data : [hello: "world"], + hasNext: true + ] + assert deferStarted.count == 1 // still not started, no subscriber yet + + when: + allowDeferredComplete.countDown() + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: [], + data: [post: [summary: "A summary"]] + ] + ] + ] + ] + } + private ExecutionResult executeQuery(String query) { return this.executeQuery(query, true, [:]) From 7e8d69ac2d47046f7c3539c21c43d83bd006cae5 Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Mon, 17 Nov 2025 14:57:00 +1100 Subject: [PATCH 2/6] GQLGW-666-do-not-start-on-separate-thread --- .../java/graphql/execution/Execution.java | 32 ------------------- .../graphql/execution/ExecutionStrategy.java | 9 ++++++ .../incremental/IncrementalCallState.java | 15 --------- .../IncrementalExecutionContextKeys.java | 7 ---- 4 files changed, 9 insertions(+), 54 deletions(-) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 7b6083db04..38c82f5a53 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -26,7 +26,6 @@ import graphql.extensions.ExtensionsBuilder; import graphql.incremental.DelayedIncrementalPartialResult; import graphql.incremental.IncrementalExecutionResultImpl; -import graphql.execution.incremental.IncrementalExecutionContextKeys; import graphql.language.Directive; import graphql.language.Document; import graphql.language.NodeUtil; @@ -49,7 +48,6 @@ import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; import static graphql.execution.ExecutionStrategyParameters.newParameters; -import static graphql.execution.incremental.IncrementalExecutionContextKeys.EAGER_DEFER_PUBLISHER; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -220,27 +218,6 @@ private CompletableFuture executeOperation(ExecutionContext exe DataLoaderDispatchStrategy dataLoaderDispatchStrategy = createDataLoaderDispatchStrategy(executionContext, executionStrategy); executionContext.setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); result = executionStrategy.execute(executionContext, parameters); - - if (executionContext.hasIncrementalSupport() && - executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { - final CompletableFuture mainResult = result; - CompletableFuture.runAsync(() -> { - IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); - CompletableFuture.anyOf(incrementalCallState.getIncrementalCallsDetectedFuture(), mainResult).join(); - - if (incrementalCallState.getIncrementalCallsDetected()) { - InstrumentationReactiveResultsParameters resultsParameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.DEFER); - InstrumentationContext ctx = nonNullCtx(executionContext.getInstrumentation().beginReactiveResults(resultsParameters, executionContext.getInstrumentationState())); - Publisher publisher = incrementalCallState.startDeferredCalls(); - ctx.onDispatched(); - - publisher = ReactiveSupport.whenPublisherFinishes(publisher, throwable -> ctx.onCompleted(null, throwable)); - executionContext.getGraphQLContext().put(EAGER_DEFER_PUBLISHER, publisher); - - incrementalCallState.startDrainingNow(); - } - }); - } } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type // up to the root object type and there was a null value somewhere. @@ -268,15 +245,6 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture incrementalSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - // If we've aready started the deferred publisher early, attach it now without re-instrumenting. - Object maybePublisher = executionContext.getGraphQLContext().get(EAGER_DEFER_PUBLISHER); - if (maybePublisher instanceof Publisher) { - Publisher publisher = (Publisher) maybePublisher; - return IncrementalExecutionResultImpl.fromExecutionResult(er) - .hasNext(true) - .incrementalItemPublisher(publisher) - .build(); - } IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); if (incrementalCallState.getIncrementalCallsDetected()) { InstrumentationReactiveResultsParameters parameters = new InstrumentationReactiveResultsParameters(executionContext, InstrumentationReactiveResultsParameters.ResultType.DEFER); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 39d4b08701..5545092ca6 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -15,6 +15,7 @@ import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.incremental.DeferredExecutionSupport; +import graphql.execution.incremental.IncrementalExecutionContextKeys; import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.FieldFetchingInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; @@ -309,6 +310,14 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi executionContext.getIncrementalCallState().enqueue(deferredExecutionSupport.createCalls()); + if (executionContext.hasIncrementalSupport() + && deferredExecutionSupport.deferredFieldsCount() > 0 + && executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { + + executionContext.getIncrementalCallState().startDeferredCalls(); + executionContext.getIncrementalCallState().startDrainingNow(); + } + // Only non-deferred fields should be considered for calculating the expected size of futures. Async.CombinedBuilder futures = Async .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index a305a29073..4f61958187 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -16,8 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import java.util.concurrent.CompletableFuture; - import static graphql.incremental.DelayedIncrementalPartialResultImpl.newIncrementalExecutionResult; /** @@ -32,9 +30,6 @@ public class IncrementalCallState { private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); - // Completed when the very first incremental call is detected/enqueued - private final CompletableFuture incrementalCallsDetectedFuture = new CompletableFuture<>(); - @SuppressWarnings("FutureReturnValueIgnored") private void drainIncrementalCalls() { IncrementalCall incrementalCall = incrementalCalls.poll(); @@ -81,9 +76,6 @@ public void enqueue(IncrementalCall incrementalCal incrementalCallsDetected.set(true); incrementalCalls.offer(incrementalCall); pendingCalls.incrementAndGet(); - if (!incrementalCallsDetectedFuture.isDone()) { - incrementalCallsDetectedFuture.complete(null); - } }); } @@ -95,13 +87,6 @@ public boolean getIncrementalCallsDetected() { return incrementalCallsDetected.get(); } - /** - * @return a future that completes when the first incremental call is detected. - */ - public CompletableFuture getIncrementalCallsDetectedFuture() { - return incrementalCallsDetectedFuture; - } - private Supplier> createPublisher() { // this will be created once and once only - any extra calls to .get() will return the previously created // singleton object diff --git a/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java b/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java index d685516a99..293a4ca4fb 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java +++ b/src/main/java/graphql/execution/incremental/IncrementalExecutionContextKeys.java @@ -1,7 +1,6 @@ package graphql.execution.incremental; -import graphql.GraphQLContext; import graphql.Internal; import org.jspecify.annotations.NullMarked; @@ -22,12 +21,6 @@ private IncrementalExecutionContextKeys() { */ public static final String ENABLE_EAGER_DEFER_START = "__GJ_enable_eager_defer_start"; - - /** - * Stores the Publisher used for incremental delivery when eager defer is enabled. - * Value type: org.reactivestreams.Publisher - */ - public static final String EAGER_DEFER_PUBLISHER = "__GJ_eager_defer_publisher"; } From a5c458ee64a3c12564154937de385a63b6cf6c67 Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Mon, 17 Nov 2025 15:36:42 +1100 Subject: [PATCH 3/6] GQLGW-666-move-start-to-after-initial-fields-resolve-with-info --- .../graphql/execution/ExecutionStrategy.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5545092ca6..c8e87c6323 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -310,14 +310,6 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi executionContext.getIncrementalCallState().enqueue(deferredExecutionSupport.createCalls()); - if (executionContext.hasIncrementalSupport() - && deferredExecutionSupport.deferredFieldsCount() > 0 - && executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { - - executionContext.getIncrementalCallState().startDeferredCalls(); - executionContext.getIncrementalCallState().startDrainingNow(); - } - // Only non-deferred fields should be considered for calculating the expected size of futures. Async.CombinedBuilder futures = Async .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); @@ -334,7 +326,17 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi Object fieldValueInfo = resolveFieldWithInfo(executionContext, newParameters); futures.addObject(fieldValueInfo); } + } + + if (executionContext.hasIncrementalSupport() + && deferredExecutionSupport.deferredFieldsCount() > 0 + && executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { + + executionContext.getIncrementalCallState().startDeferredCalls(); + executionContext.getIncrementalCallState().startDrainingNow(); + } + return futures; } From 50169f0b9ff1fe697797494a9c9a33d7bcb6c936 Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Tue, 18 Nov 2025 11:52:02 +1100 Subject: [PATCH 4/6] GQLGW-5297-assert-on-result-and-remove-redundant-call --- .../java/graphql/execution/ExecutionStrategy.java | 1 - .../incremental/IncrementalCallState.java | 2 +- .../DeferExecutionSupportIntegrationTest.groovy | 15 ++++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index c8e87c6323..06d3b644b1 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -333,7 +333,6 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi && deferredExecutionSupport.deferredFieldsCount() > 0 && executionContext.getGraphQLContext().getBoolean(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, false)) { - executionContext.getIncrementalCallState().startDeferredCalls(); executionContext.getIncrementalCallState().startDrainingNow(); } diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index 4f61958187..658fc566ed 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -105,7 +105,7 @@ public Publisher startDeferredCalls() { } public void startDrainingNow() { - publisher.get(); + startDeferredCalls(); drainIncrementalCalls(); } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index b86e972c64..487caee669 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -1787,11 +1787,20 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] when: - def sub = new CapturingSubscriber() - initialResult.incrementalItemPublisher.subscribe(sub) + def incrementalResults = getIncrementalResults(initialResult) then: - Awaitility.await().untilTrue(sub.isDone()) + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: [], + data: [post: [summary: "A summary"]] + ] + ] + ] + ] } From 0f365b57e768a4ec8c6ed8d1116dba42db29292f Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Thu, 20 Nov 2025 07:38:13 +1100 Subject: [PATCH 5/6] GQLGW-5297-add-unusual-config --- src/main/java/graphql/GraphQLUnusualConfiguration.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/graphql/GraphQLUnusualConfiguration.java b/src/main/java/graphql/GraphQLUnusualConfiguration.java index 3e3443c07d..e96b0b7380 100644 --- a/src/main/java/graphql/GraphQLUnusualConfiguration.java +++ b/src/main/java/graphql/GraphQLUnusualConfiguration.java @@ -1,6 +1,7 @@ package graphql; import graphql.execution.ResponseMapFactory; +import graphql.execution.incremental.IncrementalExecutionContextKeys; import graphql.introspection.GoodFaithIntrospection; import graphql.parser.ParserOptions; import graphql.schema.PropertyDataFetcherHelper; @@ -337,6 +338,15 @@ public IncrementalSupportConfig enableIncrementalSupport(boolean enable) { contextConfig.put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, enable); return this; } + + /** + * This controls whether @defer field execution starts as early as possible. + */ + @ExperimentalApi + public IncrementalSupportConfig enableEarlyIncrementalFieldExecution(boolean enable) { + contextConfig.put(IncrementalExecutionContextKeys.ENABLE_EAGER_DEFER_START, enable); + return this; + } } public static class DataloaderConfig extends BaseContextConfig { From 18207de039851d9de4a209dcacd8782b359ac135 Mon Sep 17 00:00:00 2001 From: Linda Lin Date: Thu, 20 Nov 2025 08:50:18 +1100 Subject: [PATCH 6/6] GQLGW-5297-remove-start-defer-calls --- .../java/graphql/execution/incremental/IncrementalCallState.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index 658fc566ed..fc1f352ca3 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -105,7 +105,6 @@ public Publisher startDeferredCalls() { } public void startDrainingNow() { - startDeferredCalls(); drainIncrementalCalls(); }