Detector: StructuredTaskScopeMisuseDetector (standalone — not pipeline-wired)
JDK feature: java.util.concurrent.StructuredTaskScope — JEP 505, fifth preview in
JDK 25, on track to finalize in JDK 26
The JDK 25 API reshapes structured concurrency around a factory and a pluggable Joiner,
and enforces a strict lifecycle:
open → fork* → join → get* → close (try-with-resources)
Breaking it does not merely produce a wrong result — the runtime throws, leaks subtasks, or returns a value read from an incomplete subtask:
| Misuse | Result |
|---|---|
fork() after join() |
IllegalStateException — scope no longer accepts work |
Subtask.get() before join() |
IllegalStateException — partial / unpublished result |
fork() / join() off-owner thread |
WrongThreadException — scope is confined to its owner |
close() without join() |
running subtasks are cancelled; their work is abandoned |
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
Subtask<String> a = scope.fork(() -> fetchA());
String early = a.get(); // ✗ read before join() → IllegalStateException
scope.join();
scope.fork(() -> fetchB()); // ✗ fork after join() → IllegalStateException
}
StructuredTaskScope.open(Joiner)is a preview API not on the Java 21 baseline this example targets, soParallelFetchServiceperforms the fan-out with a virtual-thread-per-taskExecutorService. The lifecycle rules are identical; the test models them with the detector.
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
Subtask<String> a = scope.fork(() -> fetchA());
Subtask<String> b = scope.fork(() -> fetchB());
scope.join(); // wait first
return combine(a.get(), b.get()); // then read
}StructuredTaskScopeMisuseDetector is standalone — record lifecycle events, then assert:
var d = new StructuredTaskScopeMisuseDetector();
Thread owner = Thread.currentThread();
d.recordScopeOpened("s", owner);
d.recordFork("s", "a", owner);
d.recordJoin("s", owner);
d.recordFork("s", "late", owner); // fork after join → flagged
assertTrue(d.analyze().hasIssues());See ParallelFetchServiceTest
for all four lifecycle violations plus the clean path.
mvn -f ../../pom.xml install -DskipTests -Dlicense.mock.mode=true
mvn -f pom.xml test