Skip to content

fix: switch reference-metadata L2 cache regions to NONSTRICT_READ_WRITE (2.41) - #24827

Draft
jason-p-pickering wants to merge 2 commits into
2.41from
fix/2.41-l2-cache-nonstrict-read-write
Draft

fix: switch reference-metadata L2 cache regions to NONSTRICT_READ_WRITE (2.41)#24827
jason-p-pickering wants to merge 2 commits into
2.41from
fix/2.41-l2-cache-nonstrict-read-write

Conversation

@jason-p-pickering

@jason-p-pickering jason-p-pickering commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

2.41 port of the cache-strategy portion of #24810 (Morten Svanæs), scoped down per Morten's head-to-head comment on #24815 confirming the region-lock mechanism (and NONSTRICT_READ_WRITE as the fix) holds up under real concurrent load on master.

Why this applies to 2.41 too

2.41 runs the legacy hibernate-ehcache module (Ehcache2, net.sf.ehcache 2.10.9.2) rather than master's Ehcache3/JCache stack, so it was worth checking whether Morten's finding — Hibernate's READ_WRITE access strategy takes one ReentrantReadWriteLock per region, not per key, so every cache miss blocks readers of every other key in that region — is specific to the JCache integration before porting blind.

It isn't. In Hibernate 5.6, hibernate-ehcache's StorageAccessImpl implements the same org.hibernate.cache.spi.support.DomainDataStorageAccess SPI that the JCache region factory does (verified by decompiling hibernate-ehcache-5.6.15.Final.jar) — the actual READ_WRITE/NONSTRICT_READ_WRITE locking logic, including AbstractReadWriteAccess's per-region lock, lives in Hibernate's generic cache.spi.support package and is shared by both region factories. Ehcache2 vs Ehcache3 changes only the storage backend underneath; the lock granularity problem #24810 fixes is unchanged.

What's ported vs. deliberately left out

Ported: flips the same reference-metadata regions #24810 flips — Category*, CategoryOption*, DataElement*, Indicator*, Legend*, Option*, OrganisationUnit*, PeriodType — from read-write to nonstrict-read-write. 2.41 has no Java @Cache annotations (all L2 mapping is still .hbm.xml-based), so this is a .hbm.xml attribute change rather than a CacheConcurrencyStrategy enum change. Period/RelativePeriods intentionally excluded, matching #24810.

Three entity-row regions — CategoryCombo, LegendSet, and OptionSet itself (i.e. the entity's own row: id/name/etc., cached at the <class> level) — are cached on 2.41 but have no equivalent @Cache annotation on master's CategoryCombo/LegendSet/OptionSet classes at all; master only caches (a subset of) their collections. Since #24810 never touches a region that doesn't exist, these three entity-row regions are left as read-write here rather than expanding scope beyond what's actually been reviewed/tested upstream.

To be explicit about the one that matters most for the Option N+1 investigation this stack is part of: OptionSet.options — the collection region, not the entity-row region above — is flipped to nonstrict-read-write here, matching #24810's OptionSet.java change exactly. Only the unrelated OptionSet entity-row cache (line-level metadata like name/valueType, nothing to do with the options list) is the one left untouched.

Deliberately not ported:

Testing

.hbm.xml changes validated with xmllint --noout; dhis-service-core compiles clean. This is a config-only change (no Java touched), so no new unit tests. No 2.41-specific load test has been run yet — the case for this PR is the mechanism argument above plus 2.41's own earlier canary (#24755, draft/do-not-merge) which validated NONSTRICT_READ_WRITE combined with two other changes and found no correctness regression, though that run didn't isolate this change's effect under concurrency on its own. Opening as a draft pending that isolation, or reviewer sign-off that the mechanism argument is sufficient.

Related: #24810, #24815, #24773, #24803.

🤖 AI Assisted

jason-p-pickering and others added 2 commits August 10, 2026 07:47
…TE (2.41)

2.41 port of the cache-strategy change in #24810. Hibernate 5.6's generic
cache SPI (org.hibernate.cache.spi.support.AbstractReadWriteAccess) backs
READ_WRITE region locking for both Ehcache3/JCache (master) and the legacy
Ehcache2 hibernate-ehcache module 2.41 still runs (confirmed:
StorageAccessImpl implements the same DomainDataStorageAccess SPI) - the
per-region, non-striped ReentrantReadWriteLock that #24810 fixes on master
applies here too, not just under JCache.

Flips the same reference-metadata regions #24810 flips - Category*,
CategoryOption*, DataElement*, Indicator*, Legend*, Option*,
OrganisationUnit*, PeriodType - from read-write to nonstrict-read-write in
2.41's .hbm.xml mappings (2.41 has no Java @Cache annotations; all L2
mapping is hbm.xml-based). Period/RelativePeriods intentionally excluded,
matching #24810.

Three entity-level regions - CategoryCombo, LegendSet, OptionSet - exist
only on 2.41 (master doesn't cache those entities at all, only their
collections), so #24810 never touches them; left as read-write here to
match its exact scope rather than expanding it unreviewed.

Out of scope, deliberately: #24810's ehcache.xml predefined-region/
store-by-reference changes address JCache's jsr107 MissingCacheStrategy
default (store-by-value via SerializingCopier) - 2.41's Ehcache2 caches
are already store-by-reference by default, so that part doesn't apply.
The HibernateJobConfigurationStore synchronized-query-space fix is a
separate concern (#24803's wipe fix) not requested here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Master's #24810 predefines hot Hibernate L2 regions as ehcache-native
caches to escape hibernate-jcache's forced store-by-value on
JSR-107-created caches. That specific problem doesn't apply here:
Ehcache 2's native API is always store-by-reference regardless of
whether a region is predefined or created on demand via
CacheManager#addCacheIfAbsent, so 2.41 never paid the SerializingCopier
cost #24810 fixes.

What does carry over: without an explicit entry, each of these regions
falls back to defaultCache and shares its single 1,000,000-entry cap
with everything else. Porting #24810's measured hot-region list and
heap bounds into Ehcache 2's native XML syntax gives the busiest
regions their own sized, bounded cache instead of competing for
headroom in the shared default.

org.hisp.dhis.option.OptionSet.options is deliberately excluded: that
collection is intentionally left uncached on 2.41 (see OptionSet.hbm.xml)
to avoid an N+1 where a cached id list combined with an entity-cache
miss makes Hibernate resolve ids one at a time. Predefining the region
here would be dead configuration at best.

Verified by loading the file through net.sf.ehcache's own
ConfigurationFactory/CacheManager: all 29 regions parse and register.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant