Skip to content

Commit c16cf96

Browse files
authored
Merge pull request #3872 from graphql-java/dataloader-cfs
Enable DataLoader nesting/chaining
2 parents cb65b70 + c9a3ff3 commit c16cf96

29 files changed

+3914
-2606
lines changed

performance-results/2025-04-07T01:27:42Z-154bb89d14e9701d1b9f0764cbf813ba3a3c050a-jdk17.json

Lines changed: 1262 additions & 1262 deletions
Large diffs are not rendered by default.

performance-results/2025-04-07T02:23:04Z-27b9defd98c4d044b6142c18773188f0dc9113c2-jdk17.json

Lines changed: 1262 additions & 1262 deletions
Large diffs are not rendered by default.

src/jmh/java/performance/DataLoaderPerformance.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import graphql.ExecutionInput;
55
import graphql.ExecutionResult;
66
import graphql.GraphQL;
7+
import graphql.execution.instrumentation.dataloader.DataLoaderDispatchingContextKeys;
78
import graphql.schema.DataFetcher;
89
import graphql.schema.GraphQLSchema;
910
import graphql.schema.idl.RuntimeWiring;
@@ -184,6 +185,7 @@ public void executeRequestWithDataLoaders(MyState myState, Blackhole blackhole)
184185
DataLoaderRegistry registry = DataLoaderRegistry.newRegistry().register(ownerDLName, ownerDL).register(petDLName, petDL).build();
185186

186187
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(myState.query).dataLoaderRegistry(registry).build();
188+
executionInput.getGraphQLContext().put(DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_CHAINING, true);
187189
ExecutionResult execute = myState.graphQL.execute(executionInput);
188190
Assert.assertTrue(execute.isDataPresent());
189191
Assert.assertTrue(execute.getErrors().isEmpty());

src/main/java/graphql/execution/DataLoaderDispatchStrategy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import graphql.Internal;
44
import graphql.schema.DataFetcher;
5+
import graphql.schema.DataFetchingEnvironment;
56

67
import java.util.List;
8+
import java.util.function.Supplier;
79

810
@Internal
911
public interface DataLoaderDispatchStrategy {
@@ -44,7 +46,8 @@ default void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyP
4446
default void fieldFetched(ExecutionContext executionContext,
4547
ExecutionStrategyParameters executionStrategyParameters,
4648
DataFetcher<?> dataFetcher,
47-
Object fetchedValue) {
49+
Object fetchedValue,
50+
Supplier<DataFetchingEnvironment> dataFetchingEnvironment) {
4851

4952
}
5053

src/main/java/graphql/execution/Execution.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class Execution {
6060
private final ResponseMapFactory responseMapFactory;
6161
private final boolean doNotAutomaticallyDispatchDataLoader;
6262

63+
6364
public Execution(ExecutionStrategy queryStrategy,
6465
ExecutionStrategy mutationStrategy,
6566
ExecutionStrategy subscriptionStrategy,

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
400400
}
401401

402402
MergedField field = parameters.getField();
403+
String pathString = parameters.getPath().toString();
403404
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
404405

405406
// if the DF (like PropertyDataFetcher) does not use the arguments or execution step info then dont build any
@@ -450,7 +451,7 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
450451
dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState());
451452
dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher);
452453
Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher);
453-
executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject);
454+
executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject, dataFetchingEnvironment);
454455
fetchCtx.onDispatched();
455456
fetchCtx.onFetchedValue(fetchedObject);
456457
// if it's a subscription, leave any reactive objects alone

src/main/java/graphql/execution/ResultPath.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,36 @@ public static ResultPath rootPath() {
3838

3939
// hash is effective immutable but lazily initialized similar to the hash code of java.lang.String
4040
private int hash;
41+
private final String toStringValue;
4142

4243
private ResultPath() {
4344
parent = null;
4445
segment = null;
46+
this.toStringValue = initString();
4547
}
4648

4749
private ResultPath(ResultPath parent, String segment) {
4850
this.parent = assertNotNull(parent, () -> "Must provide a parent path");
4951
this.segment = assertNotNull(segment, () -> "Must provide a sub path");
52+
this.toStringValue = initString();
5053
}
5154

5255
private ResultPath(ResultPath parent, int segment) {
5356
this.parent = assertNotNull(parent, () -> "Must provide a parent path");
5457
this.segment = segment;
58+
this.toStringValue = initString();
59+
}
60+
61+
private String initString() {
62+
if (parent == null) {
63+
return "";
64+
}
65+
66+
if (ROOT_PATH.equals(parent)) {
67+
return segmentToString();
68+
}
69+
return parent + segmentToString();
70+
5571
}
5672

5773
public int getLevel() {
@@ -294,15 +310,7 @@ public List<String> getKeysOnly() {
294310
*/
295311
@Override
296312
public String toString() {
297-
if (parent == null) {
298-
return "";
299-
}
300-
301-
if (ROOT_PATH.equals(parent)) {
302-
return segmentToString();
303-
}
304-
305-
return parent.toString() + segmentToString();
313+
return toStringValue;
306314
}
307315

308316
public String segmentToString() {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package graphql.execution.instrumentation.dataloader;
2+
3+
4+
import graphql.ExperimentalApi;
5+
import graphql.GraphQLContext;
6+
import org.jspecify.annotations.NullMarked;
7+
8+
import java.time.Duration;
9+
10+
/**
11+
* GraphQLContext keys related to DataLoader dispatching.
12+
*/
13+
@ExperimentalApi
14+
@NullMarked
15+
public final class DataLoaderDispatchingContextKeys {
16+
private DataLoaderDispatchingContextKeys() {
17+
}
18+
19+
/**
20+
* In nano seconds, the batch window size for delayed DataLoaders.
21+
* That is for DataLoaders, that are not batched as part of the normal per level
22+
* dispatching, because they were created after the level was already dispatched.
23+
* <p>
24+
* Expect Long values
25+
* <p>
26+
* Default is 500_000 (0.5 ms)
27+
*/
28+
public static final String DELAYED_DATA_LOADER_BATCH_WINDOW_SIZE_NANO_SECONDS = "__GJ_delayed_data_loader_batch_window_size_nano_seconds";
29+
30+
/**
31+
* An instance of {@link DelayedDataLoaderDispatcherExecutorFactory} that is used to create the
32+
* {@link java.util.concurrent.ScheduledExecutorService} for the delayed DataLoader dispatching.
33+
* <p>
34+
* Default is one static executor thread pool with a single thread.
35+
*/
36+
public static final String DELAYED_DATA_LOADER_DISPATCHING_EXECUTOR_FACTORY = "__GJ_delayed_data_loader_dispatching_executor_factory";
37+
38+
39+
/**
40+
* Enables the ability to chain DataLoader dispatching.
41+
* <p>
42+
* Because this requires that all DataLoaders are accessed via DataFetchingEnvironment.getLoader()
43+
* this is not completely backwards compatible and therefore disabled by default.
44+
* <p>
45+
* Expects a boolean value.
46+
*/
47+
public static final String ENABLE_DATA_LOADER_CHAINING = "__GJ_enable_data_loader_chaining";
48+
49+
50+
/**
51+
* Enables the ability that chained DataLoaders are dispatched automatically.
52+
*
53+
* @param graphQLContext
54+
*/
55+
public static void setEnableDataLoaderChaining(GraphQLContext graphQLContext, boolean enabled) {
56+
graphQLContext.put(ENABLE_DATA_LOADER_CHAINING, enabled);
57+
}
58+
59+
60+
/**
61+
* Sets nanoseconds the batch window duration size for delayed DataLoaders.
62+
* That is for DataLoaders, that are not batched as part of the normal per level
63+
* dispatching, because they were created after the level was already dispatched.
64+
*
65+
* @param graphQLContext
66+
* @param batchWindowSize
67+
*/
68+
public static void setDelayedDataLoaderBatchWindowSize(GraphQLContext graphQLContext, Duration batchWindowSize) {
69+
graphQLContext.put(DELAYED_DATA_LOADER_BATCH_WINDOW_SIZE_NANO_SECONDS, batchWindowSize.toNanos());
70+
}
71+
72+
/**
73+
* Sets the instance of {@link DelayedDataLoaderDispatcherExecutorFactory} that is used to create the
74+
* {@link java.util.concurrent.ScheduledExecutorService} for the delayed DataLoader dispatching.
75+
* <p>
76+
*
77+
* @param graphQLContext
78+
* @param delayedDataLoaderDispatcherExecutorFactory
79+
*/
80+
public static void setDelayedDataLoaderDispatchingExecutorFactory(GraphQLContext graphQLContext, DelayedDataLoaderDispatcherExecutorFactory delayedDataLoaderDispatcherExecutorFactory) {
81+
graphQLContext.put(DELAYED_DATA_LOADER_DISPATCHING_EXECUTOR_FACTORY, delayedDataLoaderDispatcherExecutorFactory);
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package graphql.execution.instrumentation.dataloader;
2+
3+
import graphql.ExperimentalApi;
4+
import graphql.GraphQLContext;
5+
import graphql.execution.ExecutionId;
6+
import org.jspecify.annotations.NullMarked;
7+
8+
import java.util.concurrent.ScheduledExecutorService;
9+
10+
/**
11+
* See {@link DataLoaderDispatchingContextKeys} for how to set it.
12+
*/
13+
@ExperimentalApi
14+
@NullMarked
15+
@FunctionalInterface
16+
public interface DelayedDataLoaderDispatcherExecutorFactory {
17+
18+
/**
19+
* Called once per execution to create the {@link ScheduledExecutorService} for the delayed DataLoader dispatching.
20+
*
21+
* Will only called if needed, i.e. if there are delayed DataLoaders.
22+
*
23+
* @param executionId
24+
* @param graphQLContext
25+
*
26+
* @return
27+
*/
28+
ScheduledExecutorService createExecutor(ExecutionId executionId, GraphQLContext graphQLContext);
29+
}

0 commit comments

Comments
 (0)