Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -89,7 +90,9 @@ public class DefaultModelValidator implements ModelValidator {

private static final String EMPTY = "";

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
private final Set<String> validIds = ConcurrentHashMap.newKeySet();

private ModelVersionProcessor versionProcessor;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -902,4 +902,61 @@ void profileActivationPropertyWithProjectExpression() throws Exception {
+ "${project.version} expressions are not supported during profile activation.",
result.getWarnings().get(1));
}

/**
* 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}.
* The underlying issue occurs when multiple threads access a non-thread-safe {@code HashSet}
* (backed by {@code HashMap}) during internal restructuring operations.
*
* <p>The fix replaces {@code HashSet} with {@code ConcurrentHashMap.newKeySet()} to provide
* thread-safe concurrent access without external synchronization.
*
* @see <a href="https://github.com/apache/maven/issues/11618">GitHub #11618</a>
*/
@Test
void testConcurrentValidation() throws Exception {
int threadCount = 10;
int iterationsPerThread = 100;
java.util.concurrent.CountDownLatch startLatch = new java.util.concurrent.CountDownLatch(1);
java.util.concurrent.CountDownLatch doneLatch = new java.util.concurrent.CountDownLatch(threadCount);
java.util.concurrent.atomic.AtomicReference<Throwable> failure = new java.util.concurrent.atomic.AtomicReference<>();

// Create multiple threads that will validate models concurrently
for (int t = 0; t < threadCount; t++) {
final int threadId = t;
new Thread(() -> {
try {
startLatch.await(); // Wait for all threads to be ready
for (int i = 0; i < iterationsPerThread; i++) {
Model model = new Model();
model.setModelVersion("4.0.0");
model.setGroupId("test.group" + threadId);
model.setArtifactId("test-artifact-" + threadId + "-" + i);
model.setVersion("1.0.0");

SimpleProblemCollector problems = new SimpleProblemCollector(model);
validator.validateEffectiveModel(model, new DefaultModelBuildingRequest(), problems);
}
} catch (Throwable e) {
failure.compareAndSet(null, e);
} finally {
doneLatch.countDown();
}
}).start();
}

// Start all threads simultaneously
startLatch.countDown();

// Wait for all threads to complete
assertTrue(doneLatch.await(30, java.util.concurrent.TimeUnit.SECONDS), "Threads did not complete in time");

// Check if any thread encountered an error
if (failure.get() != null) {
throw new AssertionError("Concurrent validation failed: " + failure.get().getMessage(), failure.get());
}
}
}