Fix thread-safety issue in DefaultModelValidator (#11618)#11734
Conversation
|
Hi maintainers 👋 Friendly ping on this PR - it's been open for about a week. This is a minimal fix (3 lines of production code) for the thread-safety issue in The fix replaces HashSet with ConcurrentHashMap.newKeySet() - the same approach already used in Maven 4's maven-impl module. I notice the mergeable state shows as "unstable" - happy to rebase if needed. Would appreciate a review when you have a moment. Thanks! |
The DefaultModelValidator class uses a shared HashSet (validIds) as an instance field while being a @singleton. When multiple threads validate models concurrently (e.g., using breadth-first dependency collector with `aether.dependencyCollector.impl=bf`), concurrent read/write operations on the HashSet cause ClassCastException: java.lang.ClassCastException: class java.util.HashMap$Node cannot be cast to class java.util.HashMap$TreeNode This occurs when the HashMap underlying the HashSet resizes and tree-ifies buckets while another thread is reading. The fix aligns with the Maven 4 implementation (maven-impl module) which already uses ConcurrentHashMap.newKeySet() for thread-safety. Changes: - Replace HashSet with ConcurrentHashMap.newKeySet() for validIds - Add concurrent validation test to prevent regression Fixes apache#11618
dcf4071 to
9f7446b
Compare
gnodet
left a comment
There was a problem hiding this comment.
Claude Code on behalf of Guillaume Nodet
The fix is correct: replacing HashSet with ConcurrentHashMap.newKeySet() on a @Singleton field that's accessed concurrently is the right approach, and it mirrors what maven-impl already does (lines 296-298). The production change is minimal and low-risk.
Two observations on the test — one stylistic, one substantive (the unbounded cache is a pre-existing concern, not introduced by this PR).
| * Validates thread-safety of DefaultModelValidator during concurrent model validation. | ||
| * | ||
| * <p>This test addresses GitHub issue #11618 where concurrent access to a shared | ||
| * {@code HashSet} in {@code DefaultModelValidator} could cause {@code ClassCastException}. |
There was a problem hiding this comment.
Nit: the rest of the file uses top-level imports. These fully-qualified inline references work, but adding proper imports would be more consistent with the existing style:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;| try { | ||
| startLatch.await(); // Wait for all threads to be ready | ||
| for (int i = 0; i < iterationsPerThread; i++) { | ||
| Model model = new Model(); |
There was a problem hiding this comment.
Minor: the thread is created with new Thread(...).start() but never joined or named. doneLatch.await(30, ...) handles termination, but if a thread hangs, the unnamed daemon-less threads will keep the JVM alive after the test times out. Consider Thread.ofVirtual() or at minimum setting the thread as daemon / giving it a name for debuggability. Not a blocker.
|
|
||
| private final Set<String> validIds = new HashSet<>(); | ||
| // Thread-safe set required because class is @Singleton and validIds is accessed concurrently | ||
| // See: https://github.com/apache/maven/issues/11618 |
There was a problem hiding this comment.
The fix itself is correct. As a pre-existing observation (not introduced by this PR): validIds grows unboundedly across all validations for the lifetime of the singleton — every unique groupId/artifactId/version ever validated is cached forever. In a long-running embedded scenario (IDE, daemon), this is a slow memory leak. The maven-impl module has the same pattern. Might be worth a follow-up issue to scope the cache per-session or use a bounded/weak-ref structure, but that's out of scope here.
gnodet
left a comment
There was a problem hiding this comment.
LGTM — the ConcurrentHashMap.newKeySet() fix is correct and minimal.
Claude Code on behalf of Guillaume Nodet
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
ClassCastExceptionProblem
DefaultModelValidatoris a@Singletonwith a shared mutableHashSet(validIds). When multiple threads validate models concurrently (e.g., using breadth-first dependency collector withaether.dependencyCollector.impl=bf), concurrent read/write operations on the HashSet cause:This affects users of:
aether.dependencyCollector.impl=bfFix Approach
Replace
new HashSet<>()withConcurrentHashMap.newKeySet()- identical to the solution already implemented in Maven 4'smaven-implmodule (lines 296, 298).Files Changed
compat/maven-model-builder/.../DefaultModelValidator.javaHashSet→ConcurrentHashMap.newKeySet()+ explanatory commentcompat/maven-model-builder/.../DefaultModelValidatorTest.javaTest Plan
testConcurrentValidation()addedmvn -pl compat/maven-model-builder testmvn -Prun-its verify(optional, if reviewer deems necessary)Compatibility
ConcurrentHashMap.newKeySet()provides sameSetinterfacemaven-implmoduleRisk Assessment
Low risk:
Fixes #11618