Skip to content

fix!: stop static caches from pinning classes, class loaders and data sources - #436

Merged
zantvoort merged 1 commit into
mainfrom
fix/weak-static-caches
Aug 10, 2026
Merged

fix!: stop static caches from pinning classes, class loaders and data sources#436
zantvoort merged 1 commit into
mainfrom
fix/weak-static-caches

Conversation

@zantvoort

Copy link
Copy Markdown
Collaborator

Twenty-one unbounded static caches in storm-core held strong references to Class, Method, Constructor and DataSource keys for the life of the JVM, pinning entity classes, their class loaders and connection pools in redeployable containers and in test suites that spin many application contexts.

Type-keyed caches

Every cache keyed by a class, method, constructor or type-derived composite key now lives in a ClassValue, so the cached value shares the lifetime of the class it describes:

  • DefaultORMReflectionImpl: record type, primary key field, canonical constructor, record components, and the method accessor cache (nested per declaring class).
  • ObjectMapperFactory: constructor metadata, nested per declaring class.
  • RecordReflection: Ref primary-key and data types (nested per declaring class, keyed by field name), sealed pattern detection, discriminator mappings.
  • MetamodelFactory: root metamodels, and path-keyed metamodels nested per root table.
  • RecordValidation: validation messages nested per type, keyed by the require-primary-key flag.
  • ModelImpl: sealed subtype field-index maps.
  • RecordMapper: compiled plans nested per record class, keyed by fetch plan; sealed plans in a ClassValue-held holder because compilation needs the RefFactory.
  • ModelFactory: models nested per record class, keyed by fetch plan.

Class-loader-keyed caches

Providers.PROVIDER_CACHE and Instantiators.INSTANTIATOR_CACHE keyed strong maps by class loader, and the cached provider instances themselves keep their loader reachable. The new ClassLoaderCache holds the loader through a weak identity key with a drained reference queue and the per-loader value through a SoftReference, which is what breaks the value-to-key cycle: once a loader is otherwise unreachable the collector clears the soft reference and reclaims the loader; a cleared entry for a live loader is recomputed on the next access.

DataSource-keyed cache

Providers.DATABASE_PRODUCT_NAMES pinned every DataSource (and its pool) ever inspected. It now uses weak identity keys with a drained reference queue, following the ConcurrencyDetector.ConnectionIdentity idiom.

Provider ordering

OrderableHelper.CLASS_ORDER_CACHE pinned provider classes through both its keys and values. The cache is gone: Providers sorts each provider list once when it is loaded into the loader-scoped cache, and enabled() preserves that order, since a filtered subset of a valid topological order is itself a valid topological order for the subset. The Orderable.sort overloads with the cache flag are removed with it.

Breaking: Orderable.sort(List, boolean) and Orderable.sort(Stream, boolean) no longer exist; the single-argument overloads remain.

Audited, no change needed

  • JdbcConnectionProviderImpl.ConcurrencyDetector.OWNERS already uses weak ConnectionIdentity keys with a drained queue and removes entries deterministically after access.
  • The template caches (SqlTemplateImpl, TemplateFragments) are bounded LRUs and outside the scope of this issue.

Tests

  • StaticCacheUnloadingTest loads an entity record through a child-first class loader, drives it through the reflection, metamodel and validation caches, and asserts the class is collected once the loader is dropped.
  • DatabaseProductNameCacheTest verifies the product name is resolved once per data source identity and that a discarded data source is collected.
  • ClassLoaderCacheTest covers identity keying, value reuse and stale-entry cleanup.

All three fail against the previous cache implementations.

Fixes #394

… sources

Type-keyed caches move to ClassValue so cached reflection artifacts,
metamodels, models and compiled plans share the lifetime of the class
they describe. Provider and instantiator registries hold their class
loader through a weak identity key and their per-loader value through a
soft reference, breaking the value-to-key cycle that pinned redeployed
applications. The database product name cache holds data sources
weakly. The class-order cache is removed: provider lists are sorted
once at load, and the Orderable.sort overloads with the cache flag are
removed with it.

Fixes #394
@zantvoort zantvoort added this to the 1.14.0 milestone Aug 10, 2026
@zantvoort
zantvoort merged commit 29e981d into main Aug 10, 2026
7 checks passed
@zantvoort
zantvoort deleted the fix/weak-static-caches branch August 10, 2026 12:15
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.

Audit the unbounded static caches keyed by Class, Method, Constructor and DataSource

1 participant