GeezerVison is showing github.com — Support variable batching by andimarek · Pull Request #4520 · graphql-java/graphql-java
Conversation
andimarek
commented
Oct 31, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Adds variable batching to the existing ExecutionInput: execute the same selected operation once per variable map. This is execution-layer support for the variable batching proposal; it adds no HTTP or request batching behavior.
ExecutionInput input = ExecutionInput.newExecutionInput()
.query("query($id: ID!) { product(id: $id) { name } }")
.variables(List.of(Map.of("id", "a"), Map.of("id", "b")))
.dataLoaderRegistry(registry)
.build();
VariableBatchExecutionResult batch = (VariableBatchExecutionResult) graphQL.execute(input);
CompletableFuture<List<ExecutionResult>> results = batch.getResultsAsync();
variables(Map) becomes a singleton list. Empty lists execute once with empty variables; singletons retain ordinary execution. Larger lists execute once per complete map, including duplicates. getVariables() and getRawVariables() assert that there is at most one map; use getVariablesList() for batches.
- A plain loop calls
executeAsync for every entry, without waiting for earlier futures. There are no batch size/concurrency limits, scheduling, or backpressure. getResults() exposes the futures in input order; getResultsAsync() collects all results in that order. No threads are created by batching.
- Entries share the supplied DataLoader registry and caches, including mutations, with ordinary automatic/manual dispatch behavior. Dispatch is not coordinated across entries. Mutation root fields remain serial within each entry; entries have no ordering guarantee. Writers must clear/prime affected cache entries.
- Coercion, errors, instrumentation state, execution IDs, and context copies are separate per entry. Cancelling the input requests cancellation of every entry. Existing incremental results and their publishers pass through unchanged.
- The default configuration parses and validates once. Custom instrumentation and preparsed providers retain their per-entry behavior, including persisted queries and document/variable transformations.
- Multi-map subscriptions fail with
IllegalArgumentException as soon as the selected operation is known, before coercion or resolvers. Literal, cached, persisted, and rewritten documents are covered. Empty/singleton lists still support subscriptions.
The new APIs are experimental. Serialize each individual ExecutionResult; the batch has no single GraphQL response map.
Validation: 158 focused tests pass across VariableBatchExecutionTest, ExecutionInputTest, GraphQLTest, and ParseAndValidateTest, including all 44 batching cases and the empty/singleton/multi-map accessor checks. Javadoc passes. The JMH benchmark compares batching with repeated executions sharing a registry.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Test Report
Test Results
| Java Version |
Total |
Passed |
Failed |
Errors |
Skipped |
| Java 11 |
6108 (+51 🟢) |
6052 (+51 🟢) |
0 (±0) |
0 (±0) |
56 (±0) |
| Java 17 |
6108 (+51 🟢) |
6051 (+51 🟢) |
0 (±0) |
0 (±0) |
57 (±0) |
| Java 21 |
6108 (+51 🟢) |
6051 (+51 🟢) |
0 (±0) |
0 (±0) |
57 (±0) |
| Java 25 |
6108 (+51 🟢) |
6051 (+51 🟢) |
0 (±0) |
0 (±0) |
57 (±0) |
| jcstress |
32 (±0) |
32 (±0) |
0 (±0) |
0 (±0) |
0 (±0) |
| Total |
24464 (+204 🟢) |
24237 (+204 🟢) |
0 (±0) |
0 (±0) |
227 (±0) |
Code Coverage (Java 25)
| Metric |
Covered |
Missed |
Coverage |
vs Master |
| Lines |
30301 |
3077 |
90.8% |
±0.0% |
| Branches |
8895 |
1507 |
85.5% |
+0.1% 🟢 |
| Methods |
8098 |
1185 |
87.2% |
+0.1% 🟢 |
Changed Class Coverage (6 classes)
| Class |
Line |
Branch |
Method |
| g.e.Execution |
±0.0% |
+2.9% 🟢 |
±0.0% |
| g.e.i.p.InstrumentationExecutionParameters |
+6.3% 🟢 |
±0.0% |
+12.5% 🟢 |
| g.ExecutionInput |
±0.0% |
+3.8% 🟢 |
±0.0% |
| g.GraphQL |
+0.4% 🟢 |
+2.5% 🟢 |
+0.3% 🟢 |
g.GraphQL $Builder |
+10.5% 🟢 |
±0.0% |
+16.7% 🟢 |
| g.VariableBatchExecutionResultImpl |
+100.0% 🟢 |
±0.0% |
+100.0% 🟢 |
Full HTML report: build artifact jacoco-html-report
Updated: 2026-10-31 23:35:10 UTC
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
andimarek
changed the title
Support variable batching with shared DataLoaders
Support variable batching
Oct 31, 2026
|
Hey @andimarek, thanks for working on this! I started looking at this for the spring-graphql adoption. I have a question about DataLoader dispatch.
If I understand correctly, all list of variables share the same batch loading registry, but it looks like the actual loading doesn't end up in the same batch. Entries start one after another and each dispatches on its own, so entry 0 has already dispatched by the time entry 1 starts. In the "shared dataloaders use ordinary dispatch and cache repeated keys" it looks like there are two independent dispatches. I thought that the goal here would be to group them all?
Is it possible to coordinate the dispatch across entries or maybe this doesn't fit the intent here? Thanks!
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Another think I've noticed.
Calling ExecutionInput.Builder#variables(List<Map<String, Object>>) will produce a result that depends on how many maps the list holds. With 0 or 1 sized maps, executeAsync returns an ExecutionResult; with 2 or more it returns a VariableBatchExecutionResult. Library users have to do "instanceof" checks all the time.
Could variables(List) always mark the input as a batch, whatever the size, and variables(Map) keep the current behavior? Library users then know the result type from the method they called, and "[]" would give an empty batch.
In the graphql-over-http PR, the spec says "one execution per map", so in theory an empty list should produce zero executions and a singleton list should still get a batch response. Today an empty list runs the operation once with no variables (as far as I understand).
I haven't implemented support yet, this comes just from a first round of review.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
|
|
|
|
/**
|
|
|
* Collects the individual results in input order. This does not start new executions.
|
|
|
* Cancelling the returned future does not cancel the executions; use {@link ExecutionInput#cancel()} instead.
|
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.
use {@link ExecutionInput#cancel()} instead.
*
Possible thing to look into - do we even start execution today if the EI is cancelled to start with?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This branch has not been deployed
No deployments
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|