From 33aedfc28bfa38b10cb57302b404d8597d3971db Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 17 Dec 2021 09:18:40 +0100 Subject: [PATCH] [MNG-7156][MNG-7285] Add locking in MojoExecutor (#627) --- .../lifecycle/internal/MojoExecutor.java | 83 +++++++++++++++++++ .../maven/project/ProjectBuilderTest.java | 13 +++ 2 files changed, 96 insertions(+) diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java index 946bffb0a104..a06cd9f40efd 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java @@ -27,6 +27,11 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.inject.Inject; import javax.inject.Named; @@ -50,6 +55,7 @@ import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.StringUtils; +import org.eclipse.aether.SessionData; /** *

@@ -72,6 +78,8 @@ public class MojoExecutor private final LifecycleDependencyResolver lifeCycleDependencyResolver; private final ExecutionEventCatapult eventCatapult; + private final ReadWriteLock aggregatorLock = new ReentrantReadWriteLock(); + @Inject public MojoExecutor( BuildPluginManager pluginManager, @@ -202,6 +210,81 @@ private void execute( MavenSession session, MojoExecution mojoExecution, Project } } + try ( ProjectLock lock = new ProjectLock( session, mojoDescriptor, aggregatorLock ) ) + { + doExecute( session, mojoExecution, projectIndex, dependencyContext ); + } + } + + /** + * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use + * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block + * all other executions until finished. + * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project. + * TODO: ideally, the builder should take care of the ordering in a smarter way + * TODO: and concurrency issues fixed with MNG-7157 + */ + private static class ProjectLock implements AutoCloseable + { + final Lock acquiredAggregatorLock; + final Lock acquiredProjectLock; + + ProjectLock( MavenSession session, MojoDescriptor mojoDescriptor, ReadWriteLock aggregatorLock ) + { + if ( session.getRequest().getDegreeOfConcurrency() > 1 ) + { + boolean aggregator = mojoDescriptor.isAggregator(); + acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock(); + acquiredProjectLock = getProjectLock( session ); + acquiredAggregatorLock.lock(); + acquiredProjectLock.lock(); + } + else + { + acquiredAggregatorLock = null; + acquiredProjectLock = null; + } + } + + @Override + public void close() + { + // release the lock in the reverse order of the acquisition + if ( acquiredProjectLock != null ) + { + acquiredProjectLock.unlock(); + } + if ( acquiredAggregatorLock != null ) + { + acquiredAggregatorLock.unlock(); + } + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private Lock getProjectLock( MavenSession session ) + { + SessionData data = session.getRepositorySession().getData(); + // TODO: when resolver 1.7.3 is released, the code below should be changed to + // TODO: Map locks = ( Map ) ((Map) data).computeIfAbsent( + // TODO: ProjectLock.class, l -> new ConcurrentHashMap<>() ); + Map locks = ( Map ) data.get( ProjectLock.class ); + // initialize the value if not already done (in case of a concurrent access) to the method + if ( locks == null ) + { + // the call to data.set(k, null, v) is effectively a call to data.putIfAbsent(k, v) + data.set( ProjectLock.class, null, new ConcurrentHashMap<>() ); + locks = ( Map ) data.get( ProjectLock.class ); + } + return locks.computeIfAbsent( session.getCurrentProject(), p -> new ReentrantLock() ); + } + } + + private void doExecute( MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex, + DependencyContext dependencyContext ) + throws LifecycleExecutionException + { + MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor(); + List forkedProjects = executeForkedExecutions( mojoExecution, session, projectIndex ); ensureDependenciesAreResolved( mojoDescriptor, session, dependencyContext ); diff --git a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java index fbe1a4e2b799..53b0af88b09a 100644 --- a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.List; import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.maven.AbstractCoreMavenComponentTestCase; import org.apache.maven.execution.MavenSession; @@ -127,6 +128,18 @@ public void testResolveDependencies() assertEquals( 1, results.size() ); MavenProject mavenProject = results.get( 0 ).getProject(); assertEquals( 1, mavenProject.getArtifacts().size() ); + + final MavenProject project = mavenProject; + final AtomicInteger artifactsResultInAnotherThread = new AtomicInteger(); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + artifactsResultInAnotherThread.set(project.getArtifacts().size()); + } + }); + t.start(); + t.join(); + assertEquals( project.getArtifacts().size(), artifactsResultInAnotherThread.get() ); } @Test