DO NOT MERGE: #34154: refactor(health) scope the health-check fan-out so a timeout cancels it - #37528
fabrizzio-dotCMS wants to merge 1 commit into
Conversation
… it (#34154) runAllHealthChecksAndWait built a List<CompletableFuture<Void>> by hand, joined with allOf(...), and waited with get(timeout). That shape stops WAITING after the timeout. It does not stop the work. The executor is Executors.newScheduledThreadPool(THREAD_POOL_SIZE) -- bounded -- and there are 15 registered checks. So a wedged check kept one of those threads for as long as it stayed wedged, with nothing reporting it, while the method logged "some health checks did not complete" and carried on. That is the failure mode of #37038 in another subsystem: abandoning something that keeps consuming a shared, bounded resource. A StructuredTaskScope makes the block the lifetime. On timeout the subtasks are cancelled, and close() does not return until they are done, so no work escapes the method. Behaviour otherwise unchanged: same timeout arithmetic, same "continue with whatever arrived" contract -- which is why the joiner is awaitAll() and not awaitAllSuccessfulOrThrow(), so one failing check still does not cancel its siblings. waitForOngoingRefreshes is deliberately NOT converted, and says so in a comment: it waits on refreshes started elsewhere and held in a map, and a scope can only wait on subtasks it forked itself. Four unit tests, no container, pinning the difference: allOf-with-timeout leaves all four wedged tasks running; the scope leaves none; a failing subtask does not cancel its siblings; subtasks run on virtual threads. StructuredTaskScope is PREVIEW in Java 25. This compiles only because maven.compiler.enablePreview is already true and the shipped container already runs --enable-preview. Its API has changed in every release since 21, so this stays DO NOT MERGE until it is final. Verified: test-compile -pl :dotcms-core --am passes; 4/4 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 1m 55s —— View job Code Review
The refactor is clean and the intent is right: New Issues
Notes (non-blocking)
The concurrency shape is a genuine improvement, but as written the change advertises a cancellation guarantee it doesn't provide for the actual checks. Worth resolving the High finding before this stops being • |
The finding
HealthStateManager.runAllHealthChecksAndWait()fans out 15 health checks and waits for them:That timeout stops waiting. It does not stop the work. The executor is
Executors.newScheduledThreadPool(HealthCheckConfig.THREAD_POOL_SIZE)— bounded — so a wedged checkkeeps one of those threads for as long as it stays wedged, with nothing reporting it, while the
method logs "some health checks did not complete" and carries on.
It is the failure mode of #37038 in a different subsystem: abandoning something that keeps
consuming a shared, bounded resource.
Run on JDK 25.0.2, four wedged tasks, a 300 ms timeout:
The change
The block is the lifetime. On timeout the subtasks are cancelled, and
close()does not returnuntil they are done — no work escapes the method.
Everything else is held constant on purpose: the same timeout arithmetic (longest individual check
health.force.refresh.timeout-ms), and the same continue with whatever arrivedcontract. That last point is why the joiner is
awaitAll()and notawaitAllSuccessfulOrThrow()— one failing check must not cancel its siblings, and a test pins it.
What is deliberately not converted
waitForOngoingRefresheskeeps itsCompletableFuture.allOf, with a comment saying why: it waits onrefreshes started elsewhere and held in the
ongoingRefreshesmap, and a scope can only wait onsubtasks it forked itself. That is not a limitation to work around — it is the property that makes
the lifetime meaningful. Converting it would mean moving where the refreshes start, which is a
different change.
Tests
Four unit tests, no container, no registered checks — they exercise the concurrency shapes directly:
allOfwith timeout cancels nothingawaitAllkeeps siblings alive4/4 green;
test-compile -pl :dotcms-core --ampasses.Why this stays DO NOT MERGE
StructuredTaskScopeis preview in Java 25. It compiles here only becausemaven.compiler.enablePreviewis alreadytrueinparent/pom.xmland the shipped containeralready runs
--enable-preview(container/tomcat9/bin/setenv.sh). Nothing technically stops us —which is the uncomfortable part. Its API has been reworked in every release since 21
(
StructuredTaskScope.open+Joineris the JEP 505 shape and does not resemble the JDK 21 one),so shipping it means signing up to rewrite this method on each JDK upgrade until it is final.
Refs #34154