fix: synchronize ReflectionValueExtractor class-introspection cache for thread safety - #12732
fix: synchronize ReflectionValueExtractor class-introspection cache for thread safety#12732waterWang wants to merge 1 commit into
Conversation
gnodet
left a comment
There was a problem hiding this comment.
Correct minimal fix for a real production hang. Wrapping the unsynchronized WeakHashMap with Collections.synchronizedMap() prevents concurrent resize()/transfer() corruption under -T parallel builds.
The remaining TOCTOU race in getClassMap (non-atomic get-null-check-put) is benign since ClassMap construction is idempotent — a duplicate creation is wasted work but not a correctness issue.
Non-blocking observation:
The deprecated compat copy at compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/reflection/ReflectionValueExtractor.java (line 54) has the identical unsynchronized WeakHashMap pattern. While the main code path goes through the impl version (which this PR fixes), the compat copy is still reachable via ObjectBasedValueSource and could theoretically trigger the same hang under -T. Consider applying the same fix there for completeness.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
|
Thank you :-) |
|
Reporter of #12731 here. Two things that might help this land in a release. Milestone. This PR is currently unmilestoned — would 4.0.0-rc-7 (milestone 131) be reasonable? The failure mode is an indefinite hang with no output and no exception under Completing the fix. @gnodet's review flagged that the deprecated compat copy at
@waterWang — your call; say which you'd prefer and I'll open it. I can also attach the full thread dump from the original incident to #12731 if that is useful for the record. |
|
I'd rather have a single PR |
|
@gnodet understood — single PR it is. I've sent the compat copy to @waterWang's branch as waterWang#1. Merging it there folds both files into this PR, so apache/maven still sees one PR rather than a follow-up. @waterWang — it's the same two-line change as yours, applied to |
gnodet
left a comment
There was a problem hiding this comment.
COMMENT — The synchronization fix for the impl copy is correct and minimal. Wrapping the unsynchronized WeakHashMap with Collections.synchronizedMap() prevents the infinite-loop hang caused by concurrent resize/transfer corruption under -T parallel builds.
However, as @gnodet noted, the PR remains incomplete:
Missing compat copy fix (high): The identical unsynchronized WeakHashMap exists in compat/maven-model-builder/.../ReflectionValueExtractor.java at line 54, with the same non-thread-safe getClassMap() pattern at lines 292-303. This copy is still reachable via ObjectBasedValueSource and can trigger the same infinite-loop hang under -T. @gnodet requested both fixes in a single PR ("I'd rather have a single PR"). @kec sent the fix to waterWang#1, but that PR is still open and has not been merged into this PR's branch.
Benign TOCTOU race (low, no action needed): The getClassMap() method does CLASS_MAPS.get() → null check → CLASS_MAPS.put(). These are individually atomic under synchronizedMap, but the compound operation is not. This is benign because ClassMap construction is deterministic and idempotent — the worst case is duplicate construction (wasted CPU, not a correctness issue).
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
Problem
ReflectionValueExtractorkeeps a staticWeakHashMapclass-introspection cache that is read and written concurrently from multiple builder threads under-T(parallel builds).WeakHashMapis not thread-safe — concurrentresizefrom multiple threads can create circular bucket chains, causing the build to hang indefinitely at 100% CPU.Observed symptom:
mvn -T1Changs with one core pinned at 100%, thread dump showsWeakHashMap.transferin a RUNNABLE state insidegetClassMap().Fix
Wrap the
WeakHashMapwithCollections.synchronizedMap()to serialize all reads and writes. This preserves the weak-reference semantics (classloader GC is unaffected) while providing thread safety.Changes
ReflectionValueExtractor.java: wrapCLASS_MAPSwithCollections.synchronizedMap(), addimport java.util.CollectionsFixes #12731