← 输入框|github.com|刷新
Skip to content

Skip OneOf input validation walk when no @oneOf type is reachable - #4516

Draft
joshgord wants to merge 1 commit into
graphql-java:masterfrom
joshgord:oneof-validation-short-circuit
Draft

joshgord wants to merge 1 commit into
graphql-java:masterfrom
joshgord:oneof-validation-short-circuit

Conversation

@joshgord

Copy link
Copy Markdown

Problem

ValuesResolverOneOfValidation.validateOneOfInputTypes runs on every argument coercion (ValuesResolver.getArgumentValuesImpl) and every variable coercion (ValuesResolverConversion). It recursively walks the full input object tree of every argument value — allocating a stream pipeline and a collected List per input object field — solely to find @oneOf types and enforce their single-key constraint.

On schemas that use no @oneOf types at all this walk can never do anything, but it still runs for every input argument of every field of every request. In allocation profiles of high-throughput services this walk shows up as a measurable share of total allocated bytes.

Change

Memoize, per GraphQLInputObjectType, whether the type itself or any transitively reachable input object type is a @oneOf type, and skip the walk when the answer is no:

  • Input types are immutable once built, so the answer is stable per type instance.
  • The reachability computation is cycle-safe for self-referencing input types (identity-based visiting set).
  • When a @oneOf type is reachable, the existing walk runs unchanged.
  • No public API changes (ValuesResolverOneOfValidation is @Internal), no new dependencies.

Benchmark

Synthetic execution benchmark: SDL schema with nested lists, ~5.5k fetched fields/op, 501 fields/op carrying nested input-object arguments (input SubFilter { x: Int, y: Int, c: SubFilter } style). Bytes measured via ThreadMXBean.getThreadAllocatedBytes, 300 ops after 200 warmup, 3 runs each, ParallelGC:

Mode master master + patch Δ bytes/op
default instrumentation 2,894,848 2,790,361 −3.6%
8 chained instrumentations 6,603,929 6,499,937 −1.6%

A consistent ~104 KB/op reduction across all runs; wall time unchanged within noise. The absolute win scales with the size and depth of input argument trees in the schema.

Tests

Two new Spock tests in ValuesResolverTest:

  • oneOf reachable through a self-referencing input type is still validated — the guard does not over-skip: a @oneOf violation nested inside a recursive input type is still rejected, on both the literal and the variable coercion paths.
  • self-referencing input types without oneOf execute without infinite recursion — the reachability computation terminates on cyclic input types and the values execute normally.

Verification: ./gradlew test — 5863 tests, BUILD SUCCESSFUL (includes the full graphql.execution.* package and the existing oneOf validation tests).

Notes for reviewers

  • CONTAINS_ONE_OF keys on GraphQLInputObjectType identity. Schemas are long-lived singletons in the intended usage, but an application that rebuilds schemas per request would grow this map. If that is a concern I can switch to weak keys or compute the flag at schema build time instead — happy to take guidance on the preferred form.
  • The walk's defensive Assert.assertTrue(inputValue instanceof Map) no longer runs on skipped subtrees; coerced input object values are maps by construction.

Generated by Josh-Bot 9000

ValuesResolverOneOfValidation walks the full input object tree of every
argument on every argument coercion and every variable coercion, and
allocates a stream pipeline plus a collected list per input object
field, solely to find @OneOf types and enforce their single-key
constraint. On schemas that use no @OneOf types this walk can never do
anything but still runs for every input argument of every field of
every request.

Memoize per GraphQLInputObjectType whether its transitively reachable
input types include a @OneOf type, and skip the walk when none is
reachable. The check is cycle-safe for self-referencing input types.
When a @OneOf type is reachable the existing walk runs unchanged.

Synthetic execution benchmark (5.5k fetched fields/op, 501 fields/op
carrying nested input-object arguments, ThreadMXBean allocated bytes):
-3.6% bytes/op with default instrumentation, -1.6% bytes/op with 8
chained instrumentations.

Generated by Josh-Bot 9000
* Per input object type: true if the type itself or any transitively reachable input object type is a
* @oneOf type. Input types are immutable once built, so this answer is stable for a given type instance.
*/
private static final Map<GraphQLInputObjectType, Boolean> CONTAINS_ONE_OF = new ConcurrentHashMap<>();

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.

While the idea here has merit I think the static is the wrong place to cache the object types.

A graphql server MAY use many different schemas over its life time. However map is cached JVM wide. So its every growing.

I think this question (does an input type have the @OneOf directive would be better cached inside the schema

schema.inputTypeContainsOneOf( inputType)

This could then be computed at schema build time and cached in a map that does not change.

That way if the schema goes away - the knowledge of the types goes with it.

As it stands today if the schema goes away, the static map here conitnues to have a reference to the object type and hence the schema.

So the schema can never be garbage collected and schemas are very large objects

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.

Ahh just saw

If that is a concern I can switch to weak keys or compute the flag at schema build time instead — happy to take guidance on the preferred form.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I understand the concerns @bbakerman, I'm still running through some performance tests on my side before I move this out of draft at which point I'll also address the concerns about a static cache

@bbakerman bbakerman left a comment

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.

This needs to cache to be inside the schema object - not in a JVM static

@github-actions

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 6053 (+2 🟢) 5997 (+2 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 17 6053 (+2 🟢) 5996 (+2 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 21 6053 (+2 🟢) 5996 (+2 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 25 6053 (+2 🟢) 5996 (+2 🟢) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 24244 (+8 🟢) 24017 (+8 🟢) 0 (±0) 0 (±0) 227 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 30229 3087 90.7% ±0.0%
Branches 8877 1511 85.5% ±0.0%
Methods 8076 1188 87.2% ±0.0%

Changed Class Coverage (1 class)

Class Line Branch Method
g.e.ValuesResolverOneOfValidation +0.4% 🟢 -1.8% 🔴 +8.3% 🟢
ValuesResolverOneOfValidation — method details
Method Line Branch
validateOneOfInputTypes 100.0% (+2.8% 🟢) 100.0% (+7.7% 🟢)
mayContainOneOf new 100.0% 100.0%
computeContainsOneOf new 100.0% —
containsOneOf new 100.0% 100.0%
unwrapToNamed new 100.0% 100.0%
validateOneOfInputTypesImpl new 91.7% 84.6%
lambda$validateOneOfInputTypesImpl$0 new 100.0% —
<clinit> new 100.0% —
lambda$validateOneOfInputTypes$0 removed removed

Full HTML report: build artifact jacoco-html-report

Updated: 2026-10-28 03:37:54 UTC

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants