fix: avoid heap exhaustion resolving cyclic beans under a system package - #5297
Open
fgrilli wants to merge 1 commit into
Open
fix: avoid heap exhaustion resolving cyclic beans under a system package#5297fgrilli wants to merge 1 commit into
fgrilli wants to merge 1 commit into
Conversation
…age (swagger-api#5292) ReflectionUtils.isSystemType() treats any javax.* class as a JDK "system" type, so a third-party JSR API living under javax.* (e.g. the JCR API) never gets a component name and can never be collapsed to a $ref. The resolver's cache still correctly reuses, by reference, the one already-built Schema instance for a given class across every differently-named property that reaches it - but it then had to deep-clone that shared instance (via a JSON round-trip, AnnotationsUtils.clone) to give each property occurrence its own name. Cloning a value that is itself shared/cyclic this way re-expands every shared subtree at every occurrence, blowing up combinatorially with depth and exhausting the heap - confirmed via the exact repro from the issue (ModelConverters.getInstance().readAll(javax.jcr.Node.class), -Xmx512m). This is a different mechanism than the one the issue's own analysis suggested (AnnotatedType.isSubtype fragmenting the cache, per swagger-api#5004): direct instrumentation showed isSubtype never becomes true during this resolution, and the resolve() call count stays modest (~127 calls) - it's a single value's object graph that explodes when cloned, not a call-count explosion like swagger-api#5091. ModelResolver.cloneResolvedProperty() detects this case (blank name, no $ref) and temporarily swaps nested schema/collection-valued fields for shape-preserving-but-empty stand-ins before cloning, so the JSON round-trip still sees enough shape for ModelDeserializer's type discrimination (e.g. a non-empty allOf list still reads as ComposedSchema) without carrying the huge/shared content underneath, then reattaches the real nested values by reference afterward. This is narrower than the existing clone() helper (still used, unchanged, at every other call site) since it relies on `property` being the resolver's own already-normalized cached value, not a freshly-built one-off wrapper. Adds a javax.jcr:jcr test dependency and Issue5292Test reproducing the exact types from the issue. Known residual limitation: the resolved schema is still, internally, a graph with shared/cyclic references. Serializing it directly (or as part of a full OpenAPI document) can still exhaust the heap, since ordinary Jackson bean serialization has no notion of shared references. Fixing that would mean changing general-purpose Schema serialization itself, not just the resolver's internal bookkeeping - left as a follow-up.
fgrilli
force-pushed
the
fix/5292-subtype-cache-oom
branch
from
August 25, 2026 09:00
09a8457 to
e253190
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes #5292.
Root cause
ReflectionUtils.isSystemType()treats anyjavax.*class as a JDK "system" type, so a third-party JSR API living underjavax.*(e.g. the JCR API) never gets a component name and can never be collapsed to a$ref. The resolver's cache still correctly reuses, by reference, the one already-builtSchemainstance for a given class across every differently-named property that reaches it — but it then had to deep-clone that shared instance (via a JSON round-trip,AnnotationsUtils.clone) to give each property occurrence its ownname. Cloning a value that is itself shared/cyclic this way re-expands every shared subtree at every occurrence, blowing up combinatorially with depth and exhausting the heap.Confirmed via the exact repro from the issue (
ModelConverters.getInstance().readAll(javax.jcr.Node.class),-Xmx512m): OOMs onmaster(v2.2.53), resolves in well under a second with this fix.This is a different mechanism than the issue's own analysis suggests (
AnnotatedType.isSubtypefragmenting the cache, per #5004): I instrumentedAnnotatedType.subtype(boolean)directly and confirmedisSubtypenever becomestrueduring this resolution. Theresolve()call count also stays modest (~127 calls total, matching the issue's own instrumentation) — this is a single value's object graph exploding when cloned, not a call-count explosion like #5091.Fix
ModelResolver.cloneResolvedProperty()detects this specific case (blank name, no$ref) and temporarily swaps nested schema/collection-valued fields for shape-preserving-but-empty stand-ins before cloning, so the JSON round-trip still sees enough shape forModelDeserializer's type discrimination (e.g. a non-emptyallOflist still reads asComposedSchema, non-nulladditionalPropertiesstill reads asMapSchema) without carrying the huge/shared content underneath — then reattaches the real nested values by reference afterward.This is deliberately narrower than the existing
clone()helper (left unchanged, still used at every other call site): it relies onpropertybeing the resolver's own already-normalized cached value for this exact property resolution, not a freshly-built one-off wrapper (e.g. theallOfenvelope built forSchemaResolution.ALL_OFstill needs the plain, full round-trip, since it's never been through this normalization before).Testing
Issue5292Testwith ajavax.jcr:jcr:2.0test dependency, covering the exact types from the issue's repro table (Node,Session,Workspace,Item), plus theresolveAsRef(true)path used byio.swagger.v3.jaxrs2.Reader.swagger-coremodule test suite: 750/750 passing, no regressions.Known residual limitation
The resolved schema is still, internally, a graph with shared/cyclic object references (unavoidable without giving
javax.*types a component name, which would defeat the "system types are inlined, not$ref'd" convention). Handing that result to a generic JSON serializer — e.g.Json.pretty()directly on the resolved schema, or on a full OpenAPI document that embeds it — can still exhaust the heap, since ordinary Jackson bean serialization has no notion of shared references and re-expands every occurrence. Fixing that would mean changing general-purposeSchemaserialization itself, not just the resolver's internal bookkeeping, so it's left as a follow-up rather than folded into this fix.