perf(cache): migrate H22 cache async commits to virtual threads#35992
perf(cache): migrate H22 cache async commits to virtual threads#35992wezell wants to merge 1 commit into
Conversation
Replace the fixed 5-thread ThreadPoolExecutor that handles async H22 cache commits (put/remove to the embedded H2 store) with a virtual-thread-per-task executor. Each commit now runs on its own virtual thread that blocks cheaply on JDBC/disk I/O instead of contending for a small pool of platform threads. Java 25's JEP 491 means synchronized blocks in Hikari/H2 no longer pin the carrier thread, so the classic "virtual threads + JDBC pinning" concern does not apply. Behavior preserved: - The real backpressure was always the shouldAsync() pre-check, which falls back to a synchronous commit on the caller; that path is unchanged. The old CallerRunsPolicy was effectively dead code (unbounded queue never rejected). - Since a virtual-thread-per-task executor has no shared queue, the queue-depth backpressure metric now reads an AtomicInteger in-flight counter instead of asyncTaskQueue.size(); isAllocationWithinTolerance()/shouldAsync() semantics are otherwise unchanged (and the @ignore'd H22CacheTest still compiles). - A Semaphore (sized by the existing cache_h22_async_threads knob, default 5) acquired inside each task bounds how many commits hit the Hikari pool at once, preventing connection-timeout storms and spurious shard rebuilds under load. Part of #35991. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @wezell's task in 3m 8s —— View job Code Review
Overall the migration is sound. The counter-balancing logic in 1. Shutdown ordering: pools close before executor drains ( The pools are set to 2. The old 3. With a thread-per-task executor there is no queue to observe back-pressure at the OS scheduler level. The only throttle is: (a) the tolerance check (soft threshold at 9,800 in-flight tasks), and (b) the semaphore (hard limit at 5 concurrent DB operations). Under a write burst, the path is: tasks pile up blocked on the semaphore → 4. Minor:
|
Proposed Changes
Migrates the H22 cache async commit executor from a fixed pool of 5 platform threads to virtual threads.
Async cache writes (
put/removeagainst the embedded H2 store) are I/O-bound, so they're a natural fit for virtual threads now that we're on Java 25 — JEP 491 meanssynchronizedblocks in Hikari/H2 no longer pin the carrier thread, so the classic "virtual threads + JDBC pinning" concern does not apply.What changed (
H22Cache.java)spawnNewThreadPool()now returnsExecutors.newThreadPerTaskExecutor(...)with a named virtual-thread factory (H22-ASYNC-COMMIT-*). Removed the GuavaThreadFactoryBuilder,ThreadPoolExecutor, andLinkedBlockingQueue.submitAsync(Runnable)helper now backs bothputAsyncandremoveAsync.Behavior preserved (not a behavior change)
shouldAsync()check, which falls back to a synchronous commit on the caller. Unchanged. The oldCallerRunsPolicywas effectively dead code — the unbounded queue never rejected.isAllocationWithinTolerance()now reads anAtomicIntegerin-flight counter instead ofasyncTaskQueue.size(). Same semantics; the@IgnoredH22CacheTeststill compiles.Semaphore(sized by the existingcache_h22_async_threadsknob, default 5) acquired inside each task caps how many commits hit the Hikari pool at once — preventing connection-timeout storms and spurious shard rebuilds under burst load. Virtual threads block cheaply on the semaphore.RejectedExecutionExceptionat shutdown; executor shutdown path unchanged.The obsolete
cache.h22.async.caller.runs.policyconfig is no longer read.This fixes / relates to
Part of #35991 (Java 25 Performance Improvements) — first child PR.
Testing
./mvnw compile -pl :dotcms-core→ BUILD SUCCESS.H22CacheTestand a write-heavy cache smoke test.Checklist
BUILD SUCCESS)🤖 Generated with Claude Code