Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
7694618
WIP: Created execution result classes for incremental delivery
felipe-gdr-atlassian Dec 20, 2023
ae52efd
Merge branch 'defer-support-in-enf' into defer-support-execution
felipe-gdr-atlassian Dec 20, 2023
acb00a5
Merge branch 'defer-support-in-enf' into defer-support-execution
felipe-gdr-atlassian Dec 20, 2023
7aa8c23
WIP
felipe-gdr-atlassian Jan 2, 2024
10b0499
Merge branch 'defer-support-in-enf' into defer-support-execution
felipe-gdr-atlassian Jan 8, 2024
0025495
WIP
felipe-gdr-atlassian Jan 8, 2024
b02efd8
Revert "WIP"
felipe-gdr-atlassian Jan 9, 2024
691d138
Merge branch 'defer-support-in-enf' into defer-support-execution
felipe-gdr-atlassian Jan 9, 2024
cbd9b63
Fixes on IncrementalItem hierarchy
felipe-gdr-atlassian Jan 10, 2024
40b7dfa
Merge branch 'master' into defer-support-execution
felipe-gdr-atlassian Jan 11, 2024
ceac689
Adjustments and plenty of Javadoc
felipe-gdr-atlassian Jan 11, 2024
a8f7906
Add unit test for sanity check
felipe-gdr-atlassian Jan 11, 2024
6b52b02
Fix Javadoc
felipe-gdr-atlassian Jan 11, 2024
d13b509
Minor adjustment in test class
felipe-gdr-atlassian Jan 11, 2024
291cfbf
Reverts both merge commits that deleted the old defer stuff.
felipe-gdr-atlassian Jan 12, 2024
c7088b1
Fix compilation errors
felipe-gdr-atlassian Jan 15, 2024
e86286e
WIP: hacky defer is working, some new tests for basic stuff, but a lo…
felipe-gdr-atlassian Jan 18, 2024
36d0330
Fixed a race condition and added more tests
felipe-gdr-atlassian Jan 19, 2024
348515d
Memoize field value resolution
felipe-gdr-atlassian Jan 19, 2024
ea9726a
Remove code
felipe-gdr-atlassian Jan 19, 2024
d03c8db
Remove unnecessary code and cover more edge cases
felipe-gdr-atlassian Jan 23, 2024
ca51dcb
Add more tests for DeferContext
felipe-gdr-atlassian Jan 23, 2024
85ac363
Create IncrementalCall interface and add type abstraction
felipe-gdr-atlassian Jan 23, 2024
1c60c37
Implement error handling in the defer execution code
felipe-gdr-atlassian Jan 25, 2024
276c872
Add one more test case
felipe-gdr-atlassian Jan 25, 2024
a5aa0fd
Add test for defer in list items
felipe-gdr-atlassian Jan 29, 2024
f43849f
Remove unnecessary test code
felipe-gdr-atlassian Jan 29, 2024
98554dc
Javadoc
felipe-gdr-atlassian Jan 29, 2024
ccb7e70
Refactor IncrementalUtils to remove duplicated code
felipe-gdr-atlassian Jan 29, 2024
e0540d9
WIP
felipe-gdr-atlassian Feb 1, 2024
f912440
Fix all unit tests and start cleaning up
felipe-gdr-atlassian Feb 2, 2024
28d98d4
Simplify instrumentation code for deferred field execution
felipe-gdr-atlassian Feb 2, 2024
8cc3d13
Merge branch 'master' into defer-execution-2024
felipe-gdr-atlassian Feb 2, 2024
07bd477
Rename some things defer -> incremental
felipe-gdr-atlassian Feb 2, 2024
934f88f
Remove code duplication
felipe-gdr-atlassian Feb 2, 2024
52de911
Various small adjustments in preparation to send the PR for review
felipe-gdr-atlassian Feb 2, 2024
f1e03a6
Remove unused import
felipe-gdr-atlassian Feb 2, 2024
ef671d0
PR feedback
felipe-gdr-atlassian Feb 5, 2024
32811eb
Merge branch 'master' into defer-execution-2024
felipe-gdr-atlassian Feb 6, 2024
c6d830c
Undo renaming to NormalizedDeferExecution: will be done in another PR
felipe-gdr-atlassian Feb 7, 2024
fe710a9
Undo refactoring around IncrementalNodes: will be done on future PR
felipe-gdr-atlassian Feb 7, 2024
61ca8d1
Refactoring method to receive individual parameters instead of object
felipe-gdr-atlassian Feb 7, 2024
864bf2e
Replace synchronized with runBlocked
felipe-gdr-atlassian Feb 7, 2024
4db2122
Fix groovy syntax
felipe-gdr-atlassian Feb 9, 2024
89e741d
Move gate mechanism from ExecutionInput to GraphQLContext
felipe-gdr-atlassian Feb 9, 2024
5900e9c
Rename DelayedIncrementalExecutionResult -> DelayedIncrementalPartial…
felipe-gdr-atlassian Feb 9, 2024
6fb8e74
(temporarily) Remove support for using Data loader to fetch deferred …
felipe-gdr-atlassian Feb 9, 2024
d87a6df
Rename DeferredCall -> DeferredFragmentCall
felipe-gdr-atlassian Feb 9, 2024
bfafd99
Move DeferredExecutionSupport into its own class file
felipe-gdr-atlassian Feb 9, 2024
e96eec5
Merge branch 'master' into defer-execution-2024
felipe-gdr-atlassian Feb 9, 2024
00a1862
Adjustments after merging
felipe-gdr-atlassian Feb 9, 2024
a801d20
Move ENABLE_INCREMENTAL_SUPPORT to @ExperimentalApi
felipe-gdr-atlassian Feb 12, 2024
99c5d0f
Remove unnecessary isEmpty check
felipe-gdr-atlassian Feb 12, 2024
b126baf
Add test with label having 'null' value
felipe-gdr-atlassian Feb 12, 2024
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
Prev Previous commit
Next Next commit
WIP: hacky defer is working, some new tests for basic stuff, but a lo…
…t still missing
  • Loading branch information
felipe-gdr-atlassian committed Jan 18, 2024
commit e86286e2fd0839abb2ce3f7d174bfae18fbaf357
29 changes: 27 additions & 2 deletions src/main/java/graphql/ExecutionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ExecutionInput {
private final DataLoaderRegistry dataLoaderRegistry;
private final ExecutionId executionId;
private final Locale locale;
private final boolean incrementalSupport;


@Internal
Expand All @@ -44,6 +45,7 @@ private ExecutionInput(Builder builder) {
this.locale = builder.locale != null ? builder.locale : Locale.getDefault(); // always have a locale in place
this.localContext = builder.localContext;
this.extensions = builder.extensions;
this.incrementalSupport = builder.incrementalSupport;
}

/**
Expand Down Expand Up @@ -139,6 +141,16 @@ public Map<String, Object> getExtensions() {
return extensions;
}

/**
* TODO: Javadoc
* @return
*/
@ExperimentalApi
public boolean isIncrementalSupport() {
return incrementalSupport;
}


/**
* This helps you transform the current ExecutionInput object into another one by starting a builder with all
* the current values and allows you to transform it how you want.
Expand All @@ -159,7 +171,8 @@ public ExecutionInput transform(Consumer<Builder> builderConsumer) {
.variables(this.rawVariables.toMap())
.extensions(this.extensions)
.executionId(this.executionId)
.locale(this.locale);
.locale(this.locale)
.incrementalSupport(this.incrementalSupport);

builderConsumer.accept(builder);

Expand Down Expand Up @@ -216,6 +229,7 @@ public static class Builder {
private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY;
private Locale locale = Locale.getDefault();
private ExecutionId executionId;
public boolean incrementalSupport = false;

public Builder query(String query) {
this.query = assertNotNull(query, () -> "query can't be null");
Expand Down Expand Up @@ -379,8 +393,19 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) {
return this;
}

/**
* TODO: Javadoc
* @param incrementalSupport
* @return
*/
@ExperimentalApi
public Builder incrementalSupport(boolean incrementalSupport) {
this.incrementalSupport = incrementalSupport;
return this;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is this boolean for on input? - is it a feature flag of sorts ?

if so I think we should use GraphqlContext- we do in other cases - we not quite that but we inject specific code like graphql.execution.values.InputInterceptor say

@felipe-gdr felipe-gdr Feb 5, 2024 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

is it a feature flag of sorts ?

That's right. It allows Graphql servers to decide whether they want to enable @defer/ @stream` for a certain query.

I think we should use GraphqlContext

So the value would be store on the map under a special key? Client code would do something like

graphqlContext.put("incrementalSupport", true); 
graphqlContext.put(GraphQLContext.INCREMENTAL_SUPPORT, true); 

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

right - rather than create a viral API (ExecutionIinput gets a new member, ExecutionContext gets one, Execition gets one....

we would do as uou have above from a well known constant and the code that checks if its on or not just looks for that magic name.

I like the FF idea - but adding more API to existing well known classes for a FF that may go away is painful.

GraphqlContext can be set more easily and its not viral in how it affects class chapes

public ExecutionInput build() {
return new ExecutionInput(this);
}
}
}
}
149 changes: 139 additions & 10 deletions src/main/java/graphql/execution/AsyncExecutionStrategy.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
package graphql.execution;

import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import graphql.ExecutionResult;
import graphql.PublicApi;
import graphql.execution.defer.DeferSupport;
import graphql.execution.defer.DeferExecutionSupport;
import graphql.execution.defer.DeferredCall;
import graphql.execution.defer.DeferredErrorSupport;
import graphql.execution.instrumentation.DeferredFieldInstrumentationContext;
import graphql.execution.incremental.DeferExecution;
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import graphql.schema.GraphQLFieldDefinition;
import graphql.util.FpKit;
import graphql.util.Pair;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static graphql.execution.MergedSelectionSet.newMergedSelectionSet;

Expand Down Expand Up @@ -56,7 +62,20 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont

MergedSelectionSet fields = parameters.getFields();
List<String> fieldNames = fields.getKeys();
Async.CombinedBuilder<FieldValueInfo> futures = Async.ofExpectedSize(fields.size());

// if(true /* check if incremental support is enabled*/) {
SomethingDefer somethingDefer = new SomethingDefer(
fields,
parameters,
executionContext,
this::resolveFieldWithInfo
);

executionContext.getDeferSupport().enqueue(somethingDefer.createCalls());
// }

Async.CombinedBuilder<FieldValueInfo> futures = Async.ofExpectedSize(fields.size() - somethingDefer.deferredFields.size());

for (String fieldName : fieldNames) {
MergedField currentField = fields.getSubField(fieldName);

Expand All @@ -66,13 +85,14 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont

CompletableFuture<FieldValueInfo> future;

if (isDeferred(executionContext, newParameters, currentField)) {
if (somethingDefer.isDeferredField(currentField)) {
executionStrategyCtx.onDeferredField(currentField);
future = resolveFieldWithInfoToNull(executionContext, newParameters);
// future = resolveFieldWithInfoToNull(executionContext, newParameters);
} else {
future = resolveFieldWithInfo(executionContext, newParameters);
futures.add(future);
}
futures.add(future);

}
CompletableFuture<ExecutionResult> overallResult = new CompletableFuture<>();
executionStrategyCtx.onDispatched(overallResult);
Expand Down Expand Up @@ -104,8 +124,9 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
}

private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedField currentField) {
DeferSupport deferSupport = executionContext.getDeferSupport();
if (deferSupport.checkForDeferDirective(currentField, executionContext)) {
DeferExecutionSupport deferSupport = executionContext.getDeferSupport();

if (currentField.getDeferExecutions() != null && !currentField.getDeferExecutions().isEmpty()) {
DeferredErrorSupport errorSupport = new DeferredErrorSupport();

// with a deferred field we are really resetting where we execute from, that is from this current field onwards
Expand All @@ -122,8 +143,8 @@ private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyP
}
);

DeferredCall call = new DeferredCall(parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport);
deferSupport.enqueue(call);
// DeferredCall call = new DeferredCall(null /* TODO extract label somehow*/, parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport);
// deferSupport.enqueue(call);
return true;
}
return false;
Expand Down Expand Up @@ -160,4 +181,112 @@ private Supplier<CompletableFuture<ExecutionResult>> deferredExecutionResult(Exe
return result;
};
}


private static class SomethingDefer {
private final ImmutableListMultimap<DeferExecution, MergedField> deferExecutionToFields;
private final ImmutableSet<MergedField> deferredFields;
private final ExecutionStrategyParameters parameters;
private final ExecutionContext executionContext;
private final BiFunction<ExecutionContext, ExecutionStrategyParameters, CompletableFuture<FieldValueInfo>> resolveFieldWithInfoFn;

private SomethingDefer(
MergedSelectionSet mergedSelectionSet,
ExecutionStrategyParameters parameters,
ExecutionContext executionContext, BiFunction<ExecutionContext, ExecutionStrategyParameters, CompletableFuture<FieldValueInfo>> resolveFieldWithInfoFn
) {
this.executionContext = executionContext;
this.resolveFieldWithInfoFn = resolveFieldWithInfoFn;
ImmutableListMultimap.Builder<DeferExecution, MergedField> deferExecutionToFieldsBuilder = ImmutableListMultimap.builder();
ImmutableSet.Builder<MergedField> deferredFieldsBuilder = ImmutableSet.builder();

mergedSelectionSet.getSubFields().values().forEach(mergedField -> {
mergedField.getDeferExecutions().forEach(de -> {
deferExecutionToFieldsBuilder.put(de, mergedField);
deferredFieldsBuilder.add(mergedField);
});
});

this.deferExecutionToFields = deferExecutionToFieldsBuilder.build();
this.deferredFields = deferredFieldsBuilder.build();
this.parameters = parameters;
}

private boolean isDeferredField(MergedField mergedField) {
return deferredFields.contains(mergedField);
}

private Set<DeferredCall> createCalls() {
return deferExecutionToFields.keySet().stream().map(deferExecution -> {
DeferredErrorSupport errorSupport = new DeferredErrorSupport();

List<MergedField> mergedFields = deferExecutionToFields.get(deferExecution);

List<Supplier<CompletableFuture<DeferredCall.FieldWithExecutionResult>>> collect = mergedFields.stream()
.map(currentField -> {
Map<String, MergedField> fields = new LinkedHashMap<>();
fields.put(currentField.getName(), currentField);

ExecutionStrategyParameters callParameters = parameters.transform(builder ->
{
MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build();
builder.deferredErrorSupport(errorSupport)
.field(currentField)
.fields(mergedSelectionSet)
.parent(null); // this is a break in the parent -> child chain - its a new start effectively
}
);


return (Supplier<CompletableFuture<DeferredCall.FieldWithExecutionResult>>) () -> resolveFieldWithInfoFn
.apply(executionContext, callParameters)
.thenCompose(FieldValueInfo::getFieldValue)
.thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult));

})
.collect(Collectors.toList());

// with a deferred field we are really resetting where we execute from, that is from this current field onwards
return new DeferredCall(
deferExecution.getLabel(),
this.parameters.getPath(),
collect,
errorSupport
);
})
.collect(Collectors.toSet());
}

@SuppressWarnings("FutureReturnValueIgnored")
private Supplier<CompletableFuture<ExecutionResult>> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
return () -> {
// GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField());
// TODO: freis: This is suddenly not needed anymore
// GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();

// Instrumentation instrumentation = executionContext.getInstrumentation();

// Supplier<ExecutionStepInfo> executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null));

// InstrumentationContext<ExecutionResult> fieldCtx = instrumentation.beginDeferredField(
// new InstrumentationDeferredFieldParameters(executionContext, executionStepInfo, parameters),
// executionContext.getInstrumentationState()
// );

CompletableFuture<ExecutionResult> result = new CompletableFuture<>();
// fieldCtx.onDispatched(result);
CompletableFuture<FieldValueInfo> fieldValueInfoFuture = resolveFieldWithInfoFn.apply(executionContext, parameters);

fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> {
// TODO:
// fieldCtx.onFieldValueInfo(fieldValueInfo);

CompletableFuture<ExecutionResult> execResultFuture = fieldValueInfo.getFieldValue();
// execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted);
Async.copyResults(execResultFuture, result);
});
return result;
};
}
}
}
24 changes: 16 additions & 8 deletions src/main/java/graphql/execution/Execution.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package graphql.execution;


import graphql.DeferredExecutionResult;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQL;
import graphql.GraphQLContext;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.execution.defer.DeferSupport;
import graphql.execution.defer.DeferExecutionSupport;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.extensions.ExtensionsBuilder;
import graphql.incremental.DelayedIncrementalExecutionResult;
import graphql.incremental.IncrementalExecutionResultImpl;
import graphql.language.Document;
import graphql.language.FragmentDefinition;
import graphql.language.NodeUtil;
Expand Down Expand Up @@ -186,18 +186,26 @@ private CompletableFuture<ExecutionResult> executeOperation(ExecutionContext exe
}

/*
* Adds the deferred publisher if its needed at the end of the query. This is also a good time for the deferred code to start running
* Adds the deferred publisher if it's needed at the end of the query. This is also a good time for the deferred code to start running
*/
private CompletableFuture<ExecutionResult> deferSupport(ExecutionContext executionContext, CompletableFuture<ExecutionResult> result) {
return result.thenApply(er -> {
DeferSupport deferSupport = executionContext.getDeferSupport();
DeferExecutionSupport deferSupport = executionContext.getDeferSupport();
if (deferSupport.isDeferDetected()) {
// 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 some one subscribes)
Publisher<DeferredExecutionResult> publisher = deferSupport.startDeferredCalls();
return ExecutionResultImpl.newExecutionResult().from(er)
.addExtension(GraphQL.DEFERRED_RESULTS, publisher)
Publisher<DelayedIncrementalExecutionResult> publisher = deferSupport.startDeferredCalls();

return IncrementalExecutionResultImpl.fromExecutionResult(er)
// TODO: would `hasNext` ever be false?
.hasNext(true)
.incrementalItemPublisher(publisher)
.build();
//
//
// return ExecutionResultImpl.newExecutionResult().from(er)
// .addExtension(GraphQL.DEFERRED_RESULTS, publisher)
// .build();
}
return er;
});
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/graphql/execution/ExecutionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import graphql.GraphQLError;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import graphql.execution.defer.DeferSupport;
import graphql.execution.defer.DeferExecutionSupport;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.language.Document;
Expand Down Expand Up @@ -54,7 +54,7 @@ public class ExecutionContext {
private final Set<ResultPath> errorPaths = new HashSet<>();
private final DataLoaderRegistry dataLoaderRegistry;
private final Locale locale;
private final DeferSupport deferSupport = new DeferSupport();
private final DeferExecutionSupport deferSupport = new DeferExecutionSupport();
private final ValueUnboxer valueUnboxer;
private final ExecutionInput executionInput;
private final Supplier<ExecutableNormalizedOperation> queryTree;
Expand Down Expand Up @@ -257,7 +257,7 @@ public ExecutionStrategy getSubscriptionStrategy() {
return subscriptionStrategy;
}

public DeferSupport getDeferSupport() {
public DeferExecutionSupport getDeferSupport() {
return deferSupport;
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,11 @@ protected CompletableFuture<ExecutionResult> completeValueForObject(ExecutionCon
.variables(executionContext.getCoercedVariables().toMap())
.build();

MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField());
MergedSelectionSet subFields = fieldCollector.collectFields(
collectorParameters,
parameters.getField(),
executionContext.getExecutionInput().isIncrementalSupport()
);

ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType);
NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, newExecutionStepInfo);
Expand Down
Loading