Revert "perf(cache): migrate H22 cache async commits to virtual threads" (#35992) - #36900
Conversation
…ds (#35992)" This reverts commit 98f8d66. Virtual threads do not unmount on file I/O, and embedded H2 is file I/O. Each commit holding a dbWorkPermits permit pins its carrier thread for the duration of the MVStore write, so the 5 concurrent writers contend for a carrier pool sized to availableProcessors(). The old model used 5 dedicated platform threads, which are unaffected by carrier availability. The submission path also changed cost profile: newThreadPerTaskExecutor starts a virtual thread immediately on submit, so a backlogged put became a live thread parked on the semaphore rather than a LinkedBlockingQueue node. Measured, 3000 upserts of a 4KB payload through 5 concurrent writers against H2 2.2.224: carriers | platform pool | virtual+semaphore | ratio ---------+---------------+-------------------+------- 2 | 7042 p/s | 20 p/s | 350x 4 | 6977 p/s | 889 p/s | 7.8x 8 | 6000 p/s | 1304 p/s | 4.6x 24 | 7059 p/s | 1293 p/s | 5.5x Platform throughput is flat regardless of carrier count; the virtual-thread path is 5x slower even with 24 carriers and collapses on a 2-CPU container. That throughput loss is what surfaces the pre-existing caller-runs fallback in isAllocationWithinTolerance(), which has been in place since 2020 and was harmless at ~7000 puts/sec. At 20-1300 puts/sec the async backlog reaches the 9800 threshold during startup, callers begin writing inline, and the main thread parks on the MVStore fair lock — see issue #36892. Reverting restores the pre-migration behavior exactly; the file is now byte-identical to 98f8d66^. Hardening the overflow path so it sheds writes instead of running them on the caller is tracked separately in #36892. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @wezell's task in 1m 46s —— View job Code ReviewReviewed the pure revert of #35992 plus the new docs. I verified the reverted No issues found. This is a clean, well-scoped revert. What I checked
One observation (not blocking, and not introduced here)The restored pool uses an unbounded The revert restores the known-good behavior; the follow-up scope is the right place for the shedding fix. · |
Records why the H22 cache migration was reverted so the same reasoning error does not recur: JEP 491 removed synchronized pinning, but virtual threads still do not unmount on file I/O, so embedded H2 writers hold their carrier. Includes the measured throughput table, the carrier-starvation gotcha that hides on a many-core dev machine, and a pre-migration checklist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Tick the box to add this pull request to the merge queue (same as
|
Reverts #35992.
Original issue: #35991 (Java 25 Performance Improvements)
Startup failure this fixes: #36892
The problem
A customer instance could not complete startup.
mainparked indefinitely inMVStore.commitunderH22Cache.put→doUpsert, driven fromInitServlet.init()→populateAllVanityURLsCache→ESContentFactoryImpl.findContentlets:H22Cache.java:176is the synchronous branch ofput()— the caller was doing the H2 write itself.Why the migration caused it
#35992 rested on this premise:
That is true for
synchronized, but it is not the pinning that matters here. Virtual threads do not unmount on file I/O, and embedded H2 is file I/O, not socket I/O. Every commit holding adbWorkPermitspermit pins its carrier for the duration of the MVStore write, so the 5 concurrent writers contend for a carrier pool sized toavailableProcessors(). The old model used 5 dedicated platform threads, which are unaffected by carrier availability.The submission path also changed cost profile:
newThreadPerTaskExecutorstarts a virtual thread immediately on submit, so a backlogged put became a live thread parked on a semaphore rather than aLinkedBlockingQueuenode.Measurements
3000 upserts of a 4KB payload through 5 concurrent writers, H2 2.2.224, using the same statement shape H22 uses (
MERGE INTO … key(cache_id)):Platform throughput is flat regardless of carrier count. The virtual-thread path is 5x slower even with 24 carriers, and collapses on a 2-CPU container.
Why that throughput loss becomes a hang
The caller-runs fallback in
isAllocationWithinTolerance()is not a regression from #35992 — it dates toe3b58b3694(2020-09-16) with the same10000/0.98defaults. It was simply unreachable at ~7000 puts/sec, because the backlog never approached 9800.At 20–1300 puts/sec it is reached routinely during startup. Once crossed, every caller writes inline, and hundreds of threads queue on H2's single-writer fair
ReentrantLock. Throughput drops further, the backlog never drains, and the threshold never clears — it latches rather than sheds.Scope
H22Cache.javais byte-identical to98f8d66fae^, verified by diff.Follow-up (deliberately not in this PR)
Tracked in #36892, kept out to keep the revert clean and reviewable:
putmarks the key inDONT_CACHE_MEbefore writing anddoSelecthonorsexclude(), so a dropped put reads as a missDONT_CACHE_MEentry expiressetQueryTimeouton the fail-safe statementscache_h22_async_task_queue/cache_h22_async_tolerance, which no longer describe a real queueNote for #35991
Any future virtual-thread migration in this codebase should distinguish socket I/O from file I/O. JEP 491 removed
synchronizedpinning; it did not make file I/O unmount. Workloads that block on the filesystem — embedded H2, local disk, FUSE mounts — still hold their carrier and should stay on platform threads.Verification
./mvnw test-compile -pl :dotcms-core— cleanH22CacheTest— OK (4 tests)🤖 Generated with Claude Code
This PR fixes: #36892