Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0a27e76
introducing of a special CompletableFuture enabling scheduling of nes…
andimarek Mar 30, 2025
f655893
replace countdown latch with an async solution
andimarek Mar 30, 2025
3675d8a
wip
andimarek Mar 30, 2025
c2d0676
refactor
andimarek Mar 31, 2025
36b2d64
refactor
andimarek Mar 31, 2025
eb8f9bb
naming
andimarek Mar 31, 2025
d1df820
comment
andimarek Mar 31, 2025
2c5da50
fix test
andimarek Mar 31, 2025
62325eb
green tests without any chain
andimarek Apr 1, 2025
89f2c48
cleanup
andimarek Apr 1, 2025
67df3a9
use ResultPath to identify a field instead DFE
andimarek Apr 1, 2025
dc8ac9a
fix a bug in the PerLevel dispatcher regarding which level to dispatch
andimarek Apr 1, 2025
3be8443
fix test
andimarek Apr 1, 2025
0686385
don't put ExecutionStrategy into context
andimarek Apr 1, 2025
bcfeff8
introduce toInternal method in DFE
andimarek Apr 1, 2025
bf0b730
Merge branch 'master' into dataloader-cfs
andimarek Apr 2, 2025
89b8c13
use new DataLoaderDelegate
andimarek Apr 2, 2025
6d02760
Merge branch 'master' into dataloader-cfs
andimarek Apr 4, 2025
5e00964
update dataloader
andimarek Apr 4, 2025
ce4d3f6
make batch window configurable and make one test more stable
andimarek Apr 4, 2025
401d09d
scheduled executor can be configured for delayed dispatching
andimarek Apr 4, 2025
2a29686
new handling of Dataloders can be disabled
andimarek Apr 4, 2025
e08ca66
Merge branch 'master' into dataloader-cfs
andimarek Apr 7, 2025
42d1d1d
more stuff
andimarek Apr 7, 2025
24d4687
Merge branch 'master' into dataloader-cfs
andimarek Apr 7, 2025
51efbfc
fix mutations with DataLoader
andimarek Apr 7, 2025
6e0da52
testing and cleanup
andimarek Apr 7, 2025
12fe329
green tests
andimarek Apr 8, 2025
e3539c7
disable new chaining algo by default
andimarek Apr 8, 2025
2369db5
cleanup
andimarek Apr 8, 2025
861a317
cleanup
andimarek Apr 8, 2025
1f4d18e
some performance,naming,cleanup, docs
andimarek Apr 9, 2025
efa8fb0
fix an engine state edge case
andimarek Apr 10, 2025
f70630d
fix an engine state edge case
andimarek Apr 10, 2025
e759137
change list implementation
andimarek Apr 10, 2025
c20ed90
Merge branch 'master' into dataloader-cfs
andimarek Apr 11, 2025
bcfb9ef
fix dispatching edge case
andimarek Apr 11, 2025
572fbdf
data loader performance test with chaining on
andimarek Apr 11, 2025
6c3d14c
data loader performance test with chaining on
andimarek Apr 11, 2025
ee1541d
make result path to String faster
andimarek Apr 14, 2025
0d76d31
Merge branch 'master' into dataloader-cfs
andimarek Apr 15, 2025
47300cb
fix merge problem
andimarek Apr 15, 2025
fed7456
Merge branch 'master' into dataloader-cfs
andimarek May 1, 2025
d57d4fd
naming
andimarek May 1, 2025
34bc3d7
add helper methods to configure new dispatching strategy
andimarek May 1, 2025
604f679
implement toInternal by default
andimarek May 1, 2025
6328062
formatting
andimarek May 1, 2025
65cc42a
cleanup
andimarek May 1, 2025
51f9344
cleanup
andimarek May 2, 2025
d1384e1
cleanup
andimarek May 2, 2025
5b04714
cleanup
andimarek May 2, 2025
a622d0d
PR feedback
andimarek May 2, 2025
cdb335f
PR feedback
andimarek May 2, 2025
81c4b09
PR feedback
andimarek May 2, 2025
4e0c26a
Merge branch 'master' into dataloader-cfs
andimarek May 16, 2025
c9a3ff3
merging
andimarek May 16, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import graphql.Internal;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;

import java.util.List;
import java.util.function.Supplier;

@Internal
public interface DataLoaderDispatchStrategy {
Expand Down Expand Up @@ -44,7 +46,7 @@ default void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyP
default void fieldFetched(ExecutionContext executionContext,
ExecutionStrategyParameters executionStrategyParameters,
DataFetcher<?> dataFetcher,
Object fetchedValue) {
Object fetchedValue, Supplier<DataFetchingEnvironment> dataFetchingEnvironment) {
Comment thread
andimarek marked this conversation as resolved.
Outdated

}

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/graphql/execution/Execution.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.impl.SchemaUtil;
import org.jspecify.annotations.NonNull;
import graphql.util.FpKit;
import org.jspecify.annotations.NonNull;
import org.reactivestreams.Publisher;

import java.util.Collections;
Expand Down Expand Up @@ -58,6 +58,8 @@ public class Execution {
private final ValueUnboxer valueUnboxer;
private final boolean doNotAutomaticallyDispatchDataLoader;

public static final String EXECUTION_CONTEXT_KEY = "__GraphQL_Java_ExecutionContext";

public Execution(ExecutionStrategy queryStrategy,
ExecutionStrategy mutationStrategy,
ExecutionStrategy subscriptionStrategy,
Expand Down Expand Up @@ -114,6 +116,8 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
.build();

executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo());
executionContext.getGraphQLContext().put(EXECUTION_CONTEXT_KEY, executionContext);


InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(
executionInput, graphQLSchema
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
}

MergedField field = parameters.getField();
String pathString = parameters.getPath().toString();
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();

// if the DF (like PropertyDataFetcher) does not use the arguments or execution step info then dont build any
Expand Down Expand Up @@ -496,7 +497,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState());
dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher);
Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher);
executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject);
executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject, dataFetchingEnvironment);
fetchCtx.onDispatched();
fetchCtx.onFetchedValue(fetchedObject);
// if it's a subscription, leave any reactive objects alone
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package graphql.execution.instrumentation.dataloader;

import graphql.Internal;
import graphql.execution.DataLoaderDispatchStrategy;
import graphql.execution.ExecutionContext;
import graphql.schema.DataFetchingEnvironment;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;

import static graphql.execution.Execution.EXECUTION_CONTEXT_KEY;


@Internal
@NullMarked
public class DataLoaderCompletableFuture<T> extends CompletableFuture<T> {
final DataFetchingEnvironment dfe;
final String dataLoaderName;
final Object key;
private final CompletableFuture<Object> underlyingDataLoaderCompletableFuture;

final CompletableFuture<Void> finishedSyncDependents = new CompletableFuture<>();

public DataLoaderCompletableFuture(DataFetchingEnvironment dfe, String dataLoaderName, Object key) {
this.dfe = dfe;
this.dataLoaderName = dataLoaderName;
this.key = key;
if (dataLoaderName != null) {
underlyingDataLoaderCompletableFuture = dfe.getDataLoaderRegistry().getDataLoader(dataLoaderName).load(key);
underlyingDataLoaderCompletableFuture.whenComplete((value, throwable) -> {
if (throwable != null) {
completeExceptionally(throwable);
} else {
complete((T) value); // causing all sync dependent code to run
}
// post completion hook
finishedSyncDependents.complete(null);
});
} else {
underlyingDataLoaderCompletableFuture = null;
}
}

DataLoaderCompletableFuture() {
this.dfe = null;
this.dataLoaderName = null;
this.key = null;
underlyingDataLoaderCompletableFuture = null;
}


@Override
public <U> CompletableFuture<U> newIncompleteFuture() {
return new DataLoaderCompletableFuture<>();
}

public static boolean isDataLoaderCompletableFuture(Object object) {
return object instanceof DataLoaderCompletableFuture;
}

public static <T> CompletableFuture<T> newDLCF(DataFetchingEnvironment dfe, String dataLoaderName, Object key) {
DataLoaderCompletableFuture<T> result = new DataLoaderCompletableFuture<>(dfe, dataLoaderName, key);
ExecutionContext executionContext = dfe.getGraphQlContext().get(EXECUTION_CONTEXT_KEY);
DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy();
if (dataLoaderDispatcherStrategy instanceof PerLevelDataLoaderDispatchStrategy) {
((PerLevelDataLoaderDispatchStrategy) dataLoaderDispatcherStrategy).newDataLoaderCF(result);
}
return result;
}


public static <U> CompletableFuture<U> supplyDLCF(DataFetchingEnvironment env, Supplier<U> supplier, @Nullable Executor executor) {
DataLoaderCompletableFuture<U> d = new DataLoaderCompletableFuture<>(env, null, null);
if (executor == null) {
executor = d.defaultExecutor();
}
executor.execute(() -> {
d.complete(supplier.get());
});
return d;

}


public static <U> CompletableFuture<U> wrap(DataFetchingEnvironment env, CompletableFuture<U> completableFuture) {
if (completableFuture instanceof DataLoaderCompletableFuture) {
return completableFuture;
}
DataLoaderCompletableFuture<U> d = new DataLoaderCompletableFuture<>(env, null, null);
completableFuture.whenComplete((u, ex) -> {
if (ex != null) {
d.completeExceptionally(ex);
} else {
d.complete(u);
}
});
return d;
}

public static CompletableFuture<Void> waitUntilAllSyncDependentsComplete(List<DataLoaderCompletableFuture<?>> dataLoaderCompletableFutureList) {
CompletableFuture<?>[] finishedSyncArray = dataLoaderCompletableFutureList
.stream()
.map(dataLoaderCompletableFuture -> dataLoaderCompletableFuture.finishedSyncDependents)
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(finishedSyncArray);
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,35 @@
import graphql.execution.ExecutionStrategyParameters;
import graphql.execution.FieldValueInfo;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.util.LockKit;
import org.dataloader.DataLoaderRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

@Internal
public class PerLevelDataLoaderDispatchStrategy implements DataLoaderDispatchStrategy {

private static final Logger log = LoggerFactory.getLogger(PerLevelDataLoaderDispatchStrategy.class);
private final CallStack callStack;
private final ExecutionContext executionContext;

static final ScheduledExecutorService isolatedDLCFBatchWindowScheduler = Executors.newSingleThreadScheduledExecutor();
static final int BATCH_WINDOW_NANO_SECONDS = 500_000;


private static class CallStack {

Expand All @@ -34,10 +50,27 @@ private static class CallStack {

private final Set<Integer> dispatchedLevels = new LinkedHashSet<>();

// fields only relevant when a DataLoaderCF is involved
private final List<DataLoaderCompletableFuture<?>> allDataLoaderCompletableFuture = new CopyOnWriteArrayList<>();
//TODO: maybe this should be cleaned up once the CF returned by these fields are completed
// otherwise this will stick around until the whole request is finished
private final Set<DataFetchingEnvironment> fieldsFinishedDispatching = ConcurrentHashMap.newKeySet();
private final Map<Integer, Set<DataFetchingEnvironment>> levelToDFEWithDataLoaderCF = new ConcurrentHashMap<>();

private final Set<DataFetchingEnvironment> batchWindowOfIsolatedDfeToDispatch = ConcurrentHashMap.newKeySet();

private boolean batchWindowOpen = false;


public CallStack() {
expectedExecuteObjectCallsPerLevel.set(1, 1);
}

public void addDataLoaderDFE(int level, DataFetchingEnvironment dfe) {
levelToDFEWithDataLoaderCF.computeIfAbsent(level, k -> new LinkedHashSet<>()).add(dfe);
}


void increaseExpectedFetchCount(int level, int count) {
expectedFetchCountPerLevel.increment(level, count);
}
Expand Down Expand Up @@ -234,9 +267,13 @@ private int getObjectCountForList(List<FieldValueInfo> fieldValueInfos) {
public void fieldFetched(ExecutionContext executionContext,
ExecutionStrategyParameters executionStrategyParameters,
DataFetcher<?> dataFetcher,
Object fetchedValue) {
Object fetchedValue,
Supplier<DataFetchingEnvironment> dataFetchingEnvironment) {
int level = executionStrategyParameters.getPath().getLevel();
boolean dispatchNeeded = callStack.lock.callLocked(() -> {
if (DataLoaderCompletableFuture.isDataLoaderCompletableFuture(fetchedValue)) {
callStack.addDataLoaderDFE(level, dataFetchingEnvironment.get());
}
callStack.increaseFetchCount(level);
return dispatchIfNeeded(level);
});
Expand Down Expand Up @@ -275,9 +312,84 @@ private boolean levelReady(int level) {
}

void dispatch(int level) {
DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry();
dataLoaderRegistry.dispatchAll();
// if we have any DataLoaderCFs => use new Algorithm
if (callStack.levelToDFEWithDataLoaderCF.get(level) != null) {
dispatchDLCFImpl(callStack.levelToDFEWithDataLoaderCF.get(level), true);
Comment thread
andimarek marked this conversation as resolved.
Outdated
} else {
// otherwise dispatch all DataLoaders
DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry();
dataLoaderRegistry.dispatchAll();
}
}


public void dispatchDLCFImpl(Set<DataFetchingEnvironment> dfeToDispatchSet, boolean dispatchAll) {

// filter out all DataLoaderCFS that are matching the fields we want to dispatch
List<DataLoaderCompletableFuture<?>> relevantDataLoaderCompletableFutures = new ArrayList<>();
for (DataLoaderCompletableFuture<?> dataLoaderCompletableFuture : callStack.allDataLoaderCompletableFuture) {
if (dfeToDispatchSet.contains(dataLoaderCompletableFuture.dfe)) {
relevantDataLoaderCompletableFutures.add(dataLoaderCompletableFuture);
}
}
// we are cleaning up the list of all DataLoadersCFs
callStack.allDataLoaderCompletableFuture.removeAll(relevantDataLoaderCompletableFutures);

// means we are all done dispatching the fields
if (relevantDataLoaderCompletableFutures.size() == 0) {
callStack.fieldsFinishedDispatching.addAll(dfeToDispatchSet);
return;
}
// we are dispatching all data loaders and waiting for all dataLoaderCFs to complete
// and to finish their sync actions
DataLoaderCompletableFuture.waitUntilAllSyncDependentsComplete(relevantDataLoaderCompletableFutures)
.whenComplete((unused, throwable) ->
dispatchDLCFImpl(dfeToDispatchSet, false)
);
if (dispatchAll) {
// if we have a mixed world with old and new DataLoaderCFs we dispatch all DataLoaders to retain compatibility
executionContext.getDataLoaderRegistry().dispatchAll();
} else {
// Only dispatching relevant data loaders
for (DataLoaderCompletableFuture<?> dlCF : relevantDataLoaderCompletableFutures) {
dlCF.dfe.getDataLoader(dlCF.dataLoaderName).dispatch();
}
}
}


public void newDataLoaderCF(DataLoaderCompletableFuture<?> dataLoaderCompletableFuture) {
System.out.println("newDataLoaderCF");
callStack.lock.runLocked(() -> {
callStack.allDataLoaderCompletableFuture.add(dataLoaderCompletableFuture);
});
if (callStack.fieldsFinishedDispatching.contains(dataLoaderCompletableFuture.dfe)) {
System.out.println("isolated dispatch");
dispatchIsolatedDataLoader(dataLoaderCompletableFuture);
}

}

private void dispatchIsolatedDataLoader(DataLoaderCompletableFuture<?> dlCF) {
callStack.lock.runLocked(() -> {
callStack.batchWindowOfIsolatedDfeToDispatch.add(dlCF.dfe);
if (!callStack.batchWindowOpen) {
callStack.batchWindowOpen = true;
AtomicReference<Set<DataFetchingEnvironment>> dfesToDispatch = new AtomicReference<>();
Runnable runnable = () -> {
callStack.lock.runLocked(() -> {
dfesToDispatch.set(new LinkedHashSet<>(callStack.batchWindowOfIsolatedDfeToDispatch));
callStack.batchWindowOfIsolatedDfeToDispatch.clear();
callStack.batchWindowOpen = false;
});
dispatchDLCFImpl(dfesToDispatch.get(), false);
};
isolatedDLCFBatchWindowScheduler.schedule(runnable, BATCH_WINDOW_NANO_SECONDS, TimeUnit.NANOSECONDS);
}

});
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import graphql.execution.ExecutionStrategyParameters;
import graphql.execution.FieldValueInfo;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.util.LockKit;
import org.dataloader.DataLoaderRegistry;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

/**
* The execution of a query can be divided into 2 phases: first, the non-deferred fields are executed and only once
Expand Down Expand Up @@ -173,7 +175,7 @@ public void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyPa
public void fieldFetched(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
DataFetcher<?> dataFetcher,
Object fetchedValue) {
Object fetchedValue, Supplier<DataFetchingEnvironment> dataFetchingEnvironment) {
Comment thread
andimarek marked this conversation as resolved.
Outdated

final boolean dispatchNeeded;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/graphql/schema/DataFetchingEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro
@Nullable
<K, V> DataLoader<K, V> getDataLoader(String dataLoaderName);


DataLoaderChain getDataLoaderChain();

/**
* @return the {@link org.dataloader.DataLoaderRegistry} in play
*/
Expand Down Expand Up @@ -270,4 +273,6 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro
* @return the coerced variables that have been passed to the query that is being executed
*/
Map<String, Object> getVariables();


}
Loading