You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mi...@apache.org on 2021/04/24 21:59:33 UTC
[maven-resolver] branch master updated: [MRESOLVER-145] Introduce
more SyncContext implementations
This is an automated email from the ASF dual-hosted git repository.
michaelo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new d5ba6e0 [MRESOLVER-145] Introduce more SyncContext implementations
d5ba6e0 is described below
commit d5ba6e0d126e466a618c08d5249de76c305ec6be
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Wed Dec 2 22:27:58 2020 +0100
[MRESOLVER-145] Introduce more SyncContext implementations
Introduce pluggable "named" SyncContext implementation
and add several backing implementations.
This closes #77
---
maven-resolver-impl/pom.xml | 14 +
.../eclipse/aether/impl/DefaultServiceLocator.java | 2 +-
.../eclipse/aether/impl/guice/AetherModule.java | 80 ++++-
.../internal/impl/DefaultUpdateCheckManager.java | 2 +-
.../synccontext/DefaultSyncContextFactory.java | 86 +++++
.../synccontext/GlobalSyncContextFactory.java | 27 +-
.../impl/synccontext/NamedSyncContextFactory.java | 124 +++++++
.../NoLockSyncContextFactory.java} | 25 +-
.../synccontext/SyncContextFactoryDelegate.java | 29 ++
.../named/DiscriminatingNameMapper.java | 139 ++++++++
.../impl/synccontext/named/GAVNameMapper.java | 82 +++++
.../named/NameMapper.java} | 48 +--
.../synccontext/named/NamedLockFactoryAdapter.java | 184 ++++++++++
.../named/SessionAwareNamedLockFactory.java | 40 +++
.../impl/synccontext/named/StaticNameMapper.java | 74 ++++
.../src/site/markdown/synccontextfactory.md.vm | 95 +++++
maven-resolver-impl/src/site/site.xml | 1 +
.../synccontext/LocalReadWriteLockAdapterTest.java | 33 ++
.../synccontext/LocalSemaphoreAdapterTest.java | 33 ++
.../NamedLockFactoryAdapterTestSupport.java | 250 +++++++++++++
.../pom.xml | 268 +++++++-------
.../HazelcastCPSemaphoreNamedLockFactory.java | 50 +++
...HazelcastClientCPSemaphoreNamedLockFactory.java | 50 +++
.../HazelcastSemaphoreNamedLockFactory.java | 117 +++++++
.../src/site/markdown/index.md.vm | 51 +++
.../src/site/site.xml | 2 +-
.../hazelcast/HazelcastCPSemaphoreAdapterIT.java | 31 ++
.../HazelcastCPSemaphoreNamedLockFactoryIT.java | 39 +++
.../HazelcastClientCPSemaphoreAdapterIT.java | 42 +++
...zelcastClientCPSemaphoreNamedLockFactoryIT.java | 45 +++
.../named/hazelcast/HazelcastClientUtils.java | 88 +++++
.../NamedLockFactoryAdapterTestSupport.java | 246 +++++++++++++
.../hazelcast/NamedLockFactoryTestSupport.java | 196 +++++++++++
.../src/test/resources/hazelcast-client.xml | 37 ++
.../src/test/resources/hazelcast.xml | 40 +++
.../pom.xml | 57 ++-
.../redisson/RedissonNamedLockFactorySupport.java | 118 +++++++
.../RedissonReadWriteLockNamedLockFactory.java | 47 +++
.../RedissonSemaphoreNamedLockFactory.java | 71 ++++
.../src/site/markdown/index.md.vm | 72 ++++
.../src/site/site.xml | 2 +-
.../pom.xml | 44 ++-
.../java/org/eclipse/aether/named/NamedLock.java | 75 ++++
.../org/eclipse/aether/named/NamedLockFactory.java | 40 +++
.../LocalReadWriteLockNamedLockFactory.java | 49 +++
.../providers/LocalSemaphoreNamedLockFactory.java | 72 ++++
.../named/support/AdaptedSemaphoreNamedLock.java | 127 +++++++
.../named/support/NamedLockFactorySupport.java | 134 +++++++
.../aether/named/support/NamedLockSupport.java | 54 +++
.../named/support/ReadWriteLockNamedLock.java | 108 ++++++
.../src/site/markdown/index.md.vm | 33 ++
.../src/site/site.xml | 2 +-
.../LocalReadWriteLockNamedLockFactoryTest.java | 32 ++
.../named/LocalSemaphoreNamedLockFactoryTest.java | 32 ++
.../aether/named/NamedLockFactoryTestSupport.java | 199 +++++++++++
.../aether/internal/impl/TrackingFileManager.java | 151 --------
.../src/site/markdown/index.md.vm | 57 ---
.../aether/internal/impl/TrackingFileManager.java | 151 --------
.../synccontext/RedissonSyncContextFactory.java | 390 ---------------------
.../src/site/markdown/index.md.vm | 103 ------
pom.xml | 37 +-
61 files changed, 3828 insertions(+), 1099 deletions(-)
diff --git a/maven-resolver-impl/pom.xml b/maven-resolver-impl/pom.xml
index 7528884..a51b11d 100644
--- a/maven-resolver-impl/pom.xml
+++ b/maven-resolver-impl/pom.xml
@@ -51,6 +51,10 @@
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-named-locks</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-util</artifactId>
</dependency>
<dependency>
@@ -105,10 +109,20 @@
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-test-util</artifactId>
<scope>test</scope>
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
index 1f29616..a80b3a0 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
@@ -45,7 +45,6 @@ import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
-import org.eclipse.aether.internal.impl.DefaultSyncContextFactory;
import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
@@ -53,6 +52,7 @@ import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManagerFactory;
import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
import org.eclipse.aether.internal.impl.slf4j.Slf4jLoggerFactory;
+import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 0121839..0558f28 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -8,9 +8,9 @@ package org.eclipse.aether.impl.guice;
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -20,7 +20,9 @@ package org.eclipse.aether.impl.guice;
*/
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import javax.inject.Named;
@@ -38,6 +40,18 @@ import org.eclipse.aether.impl.OfflineController;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.RepositoryConnectorProvider;
import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
+import org.eclipse.aether.internal.impl.synccontext.GlobalSyncContextFactory;
+import org.eclipse.aether.internal.impl.synccontext.NamedSyncContextFactory;
+import org.eclipse.aether.internal.impl.synccontext.NoLockSyncContextFactory;
+import org.eclipse.aether.internal.impl.synccontext.SyncContextFactoryDelegate;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
+import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.impl.UpdateCheckManager;
import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
@@ -55,7 +69,6 @@ import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
-import org.eclipse.aether.internal.impl.DefaultSyncContextFactory;
import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
@@ -81,7 +94,7 @@ import com.google.inject.name.Names;
* for all components from this library. To acquire a complete repository system, clients need to bind an artifact
* descriptor reader, a version resolver, a version range resolver, zero or more metadata generator factories, some
* repository connector and transporter factories to access remote repositories.
- *
+ *
* @noextend This class must not be extended by clients and will eventually be marked {@code final} without prior
* notice.
*/
@@ -146,12 +159,71 @@ public class AetherModule
bind( LocalRepositoryManagerFactory.class ).annotatedWith( Names.named( "enhanced" ) ) //
.to( EnhancedLocalRepositoryManagerFactory.class ).in( Singleton.class );
+ bind( SyncContextFactoryDelegate.class ).annotatedWith( Names.named( NoLockSyncContextFactory.NAME ) )
+ .to( NoLockSyncContextFactory.class ).in( Singleton.class );
+ bind( SyncContextFactoryDelegate.class ).annotatedWith( Names.named( GlobalSyncContextFactory.NAME ) )
+ .to( GlobalSyncContextFactory.class ).in( Singleton.class );
+ bind( SyncContextFactoryDelegate.class ).annotatedWith( Names.named( NamedSyncContextFactory.NAME ) )
+ .to( NamedSyncContextFactory.class ).in( Singleton.class );
+
+ bind( NameMapper.class ).annotatedWith( Names.named( StaticNameMapper.NAME ) )
+ .to( StaticNameMapper.class ).in( Singleton.class );
+ bind( NameMapper.class ).annotatedWith( Names.named( GAVNameMapper.NAME ) )
+ .to( GAVNameMapper.class ).in( Singleton.class );
+ bind( NameMapper.class ).annotatedWith( Names.named( DiscriminatingNameMapper.NAME ) )
+ .to( DiscriminatingNameMapper.class ).in( Singleton.class );
+
+ bind( NamedLockFactory.class ).annotatedWith( Names.named( LocalReadWriteLockNamedLockFactory.NAME ) )
+ .to( LocalReadWriteLockNamedLockFactory.class ).in( Singleton.class );
+ bind( NamedLockFactory.class ).annotatedWith( Names.named( LocalSemaphoreNamedLockFactory.NAME ) )
+ .to( LocalSemaphoreNamedLockFactory.class ).in( Singleton.class );
+
install( new Slf4jModule() );
}
@Provides
@Singleton
+ Map<String, SyncContextFactoryDelegate> provideSyncContextFactoryDelegates(
+ @Named( NoLockSyncContextFactory.NAME ) SyncContextFactoryDelegate nolock,
+ @Named( GlobalSyncContextFactory.NAME ) SyncContextFactoryDelegate global,
+ @Named( NamedSyncContextFactory.NAME ) SyncContextFactoryDelegate named )
+ {
+ Map<String, SyncContextFactoryDelegate> factories = new HashMap<>();
+ factories.put( NoLockSyncContextFactory.NAME, nolock );
+ factories.put( GlobalSyncContextFactory.NAME, global );
+ factories.put( NamedSyncContextFactory.NAME, named );
+ return Collections.unmodifiableMap( factories );
+ }
+
+ @Provides
+ @Singleton
+ Map<String, NameMapper> provideNameMappers(
+ @Named( StaticNameMapper.NAME ) NameMapper staticNameMapper,
+ @Named( GAVNameMapper.NAME ) NameMapper gavNameMapper,
+ @Named( DiscriminatingNameMapper.NAME ) NameMapper discriminatingNameMapper )
+ {
+ Map<String, NameMapper> nameMappers = new HashMap<>();
+ nameMappers.put( StaticNameMapper.NAME, staticNameMapper );
+ nameMappers.put( GAVNameMapper.NAME, gavNameMapper );
+ nameMappers.put( DiscriminatingNameMapper.NAME, discriminatingNameMapper );
+ return Collections.unmodifiableMap( nameMappers );
+ }
+
+ @Provides
+ @Singleton
+ Map<String, NamedLockFactory> provideNamedLockFactories(
+ @Named( LocalReadWriteLockNamedLockFactory.NAME ) NamedLockFactory localRwLock,
+ @Named( LocalSemaphoreNamedLockFactory.NAME ) NamedLockFactory localSemaphore )
+ {
+ Map<String, NamedLockFactory> factories = new HashMap<>();
+ factories.put( LocalReadWriteLockNamedLockFactory.NAME, localRwLock );
+ factories.put( LocalSemaphoreNamedLockFactory.NAME, localSemaphore );
+ return Collections.unmodifiableMap( factories );
+ }
+
+ @Provides
+ @Singleton
Set<LocalRepositoryManagerFactory> provideLocalRepositoryManagerFactories(
@Named( "simple" ) LocalRepositoryManagerFactory simple,
@Named( "enhanced" ) LocalRepositoryManagerFactory enhanced )
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
index df6f3c4..8d1bb20 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
@@ -337,7 +337,7 @@ public class DefaultUpdateCheckManager
}
else
{
- return new MetadataTransferException( metadata, repository, metadata + "failed to transfer from "
+ return new MetadataTransferException( metadata, repository, metadata + " failed to transfer from "
+ repository.getUrl() + " during a previous attempt."
+ " This failure was cached in the local repository and"
+ " resolution will not be reattempted until the update interval of " + repository.getId()
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java
new file mode 100644
index 0000000..f100b7b
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java
@@ -0,0 +1,86 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.spi.synccontext.SyncContextFactory;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Default {@link SyncContextFactory} implementation that delegates to some {@link SyncContextFactoryDelegate}
+ * implementation.
+ */
+@Singleton
+@Named
+public final class DefaultSyncContextFactory
+ implements SyncContextFactory, org.eclipse.aether.impl.SyncContextFactory
+{
+ private static final String SYNC_CONTEXT_FACTORY_NAME = System.getProperty(
+ "aether.syncContext.impl", NamedSyncContextFactory.NAME
+ );
+
+ private final SyncContextFactoryDelegate delegate;
+
+ /**
+ * Constructor used with DI, where factories are injected and selected based on key.
+ */
+ @Inject
+ public DefaultSyncContextFactory( final Map<String, SyncContextFactoryDelegate> delegates )
+ {
+ Objects.requireNonNull( delegates );
+ this.delegate = selectDelegate( delegates );
+ }
+
+ /**
+ * Default constructor
+ */
+ public DefaultSyncContextFactory()
+ {
+ Map<String, SyncContextFactoryDelegate> delegates = new HashMap<>( 3 );
+ delegates.put( NoLockSyncContextFactory.NAME, new NoLockSyncContextFactory() );
+ delegates.put( GlobalSyncContextFactory.NAME, new GlobalSyncContextFactory() );
+ delegates.put( NamedSyncContextFactory.NAME, new NamedSyncContextFactory() );
+ this.delegate = selectDelegate( delegates );
+ }
+
+ private SyncContextFactoryDelegate selectDelegate( final Map<String, SyncContextFactoryDelegate> delegates )
+ {
+ SyncContextFactoryDelegate delegate = delegates.get( SYNC_CONTEXT_FACTORY_NAME );
+ if ( delegate == null )
+ {
+ throw new IllegalArgumentException( "Unknown SyncContextFactory impl: " + SYNC_CONTEXT_FACTORY_NAME
+ + ", known ones: " + delegates.keySet() );
+ }
+ return delegate;
+ }
+
+ @Override
+ public SyncContext newInstance( final RepositorySystemSession session, final boolean shared )
+ {
+ return delegate.newInstance( session, shared );
+ }
+}
diff --git a/maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/synccontext/GlobalSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/GlobalSyncContextFactory.java
similarity index 81%
rename from maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/synccontext/GlobalSyncContextFactory.java
rename to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/GlobalSyncContextFactory.java
index 2889c80..047132e 100644
--- a/maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/synccontext/GlobalSyncContextFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/GlobalSyncContextFactory.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.synccontext;
+package org.eclipse.aether.internal.impl.synccontext;
/*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -23,14 +23,12 @@ import java.util.Collection;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
-import javax.annotation.Priority;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.SyncContext;
import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.metadata.Metadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,20 +39,22 @@ import org.slf4j.LoggerFactory;
* <p>
* <strong>Note: This component is still considered to be experimental, use with caution!</strong>
*/
-@Named
-@Priority( Integer.MAX_VALUE )
@Singleton
+@Named( GlobalSyncContextFactory.NAME )
public class GlobalSyncContextFactory
- implements SyncContextFactory
+ implements SyncContextFactoryDelegate
{
+ public static final String NAME = "global";
+
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
- public SyncContext newInstance( RepositorySystemSession session, boolean shared )
+ @Override
+ public SyncContext newInstance( final RepositorySystemSession session, final boolean shared )
{
return new GlobalSyncContext( shared ? lock.readLock() : lock.writeLock(), shared );
}
- static class GlobalSyncContext
+ private static class GlobalSyncContext
implements SyncContext
{
private static final Logger LOGGER = LoggerFactory.getLogger( GlobalSyncContext.class );
@@ -63,13 +63,15 @@ public class GlobalSyncContextFactory
private final boolean shared;
private int lockHoldCount;
- private GlobalSyncContext( Lock lock, boolean shared )
+ private GlobalSyncContext( final Lock lock, final boolean shared )
{
this.lock = lock;
this.shared = shared;
}
- public void acquire( Collection<? extends Artifact> artifact, Collection<? extends Metadata> metadata )
+ @Override
+ public void acquire( final Collection<? extends Artifact> artifact,
+ final Collection<? extends Metadata> metadata )
{
LOGGER.trace( "Acquiring global {} lock (currently held: {})",
shared ? "read" : "write", lockHoldCount );
@@ -77,6 +79,7 @@ public class GlobalSyncContextFactory
lockHoldCount++;
}
+ @Override
public void close()
{
while ( lockHoldCount > 0 )
@@ -87,7 +90,5 @@ public class GlobalSyncContextFactory
lockHoldCount--;
}
}
-
}
-
-}
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NamedSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NamedSyncContextFactory.java
new file mode 100644
index 0000000..ba47c8d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NamedSyncContextFactory.java
@@ -0,0 +1,124 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactoryAdapter;
+import org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
+import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
+
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Named {@link SyncContextFactoryDelegate} implementation that selects underlying {@link NamedLockFactory}
+ * implementation at creation.
+ */
+@Singleton
+@Named( NamedSyncContextFactory.NAME )
+public final class NamedSyncContextFactory
+ implements SyncContextFactoryDelegate
+{
+ public static final String NAME = "named";
+
+ private static final String FACTORY_NAME = System.getProperty(
+ "aether.syncContext.named.factory", LocalReadWriteLockNamedLockFactory.NAME
+ );
+
+ private static final String NAME_MAPPER = System.getProperty(
+ "aether.syncContext.named.nameMapper", GAVNameMapper.NAME
+ );
+
+ private static final long TIME = Long.getLong(
+ "aether.syncContext.named.time", 30L
+ );
+
+ private static final TimeUnit TIME_UNIT = TimeUnit.valueOf( System.getProperty(
+ "aether.syncContext.named.time.unit", TimeUnit.SECONDS.name()
+ ) );
+
+ private final NamedLockFactoryAdapter namedLockFactoryAdapter;
+
+ /**
+ * Constructor used with DI, where factories are injected and selected based on key.
+ */
+ @Inject
+ public NamedSyncContextFactory( final Map<String, NameMapper> nameMappers,
+ final Map<String, NamedLockFactory> factories )
+ {
+ this.namedLockFactoryAdapter = selectAndAdapt( nameMappers, factories );
+ }
+
+ /**
+ * Default constructor
+ */
+ public NamedSyncContextFactory()
+ {
+ Map<String, NameMapper> nameMappers = new HashMap<>();
+ nameMappers.put( StaticNameMapper.NAME, new StaticNameMapper() );
+ nameMappers.put( GAVNameMapper.NAME, new GAVNameMapper() );
+ nameMappers.put( DiscriminatingNameMapper.NAME, new DiscriminatingNameMapper( new GAVNameMapper() ) );
+ Map<String, NamedLockFactory> factories = new HashMap<>();
+ factories.put( LocalReadWriteLockNamedLockFactory.NAME, new LocalReadWriteLockNamedLockFactory() );
+ factories.put( LocalSemaphoreNamedLockFactory.NAME, new LocalSemaphoreNamedLockFactory() );
+ this.namedLockFactoryAdapter = selectAndAdapt( nameMappers, factories );
+ }
+
+ private static NamedLockFactoryAdapter selectAndAdapt( final Map<String, NameMapper> nameMappers,
+ final Map<String, NamedLockFactory> factories )
+ {
+ NameMapper nameMapper = nameMappers.get( NAME_MAPPER );
+ if ( nameMapper == null )
+ {
+ throw new IllegalArgumentException( "Unknown NameMapper name: " + NAME_MAPPER
+ + ", known ones: " + nameMappers.keySet() );
+ }
+ NamedLockFactory factory = factories.get( FACTORY_NAME );
+ if ( factory == null )
+ {
+ throw new IllegalArgumentException( "Unknown NamedLockFactory name: " + FACTORY_NAME
+ + ", known ones: " + factories.keySet() );
+ }
+ return new NamedLockFactoryAdapter( nameMapper, factory, TIME, TIME_UNIT );
+ }
+
+ @Override
+ public SyncContext newInstance( final RepositorySystemSession session, final boolean shared )
+ {
+ return namedLockFactoryAdapter.newInstance( session, shared );
+ }
+
+ @PreDestroy
+ public void shutdown()
+ {
+ namedLockFactoryAdapter.shutdown();
+ }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NoLockSyncContextFactory.java
similarity index 71%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NoLockSyncContextFactory.java
index 6210a9e..10564d7 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/NoLockSyncContextFactory.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl;
+package org.eclipse.aether.internal.impl.synccontext;
/*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -27,7 +27,6 @@ import javax.inject.Singleton;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.SyncContext;
import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.metadata.Metadata;
/**
@@ -35,28 +34,30 @@ import org.eclipse.aether.metadata.Metadata;
* synchronization but merely completes the repository system.
*/
@Singleton
-@Named
-public class DefaultSyncContextFactory
- implements SyncContextFactory, org.eclipse.aether.impl.SyncContextFactory
+@Named( NoLockSyncContextFactory.NAME )
+public class NoLockSyncContextFactory
+ implements SyncContextFactoryDelegate
{
+ public static final String NAME = "nolock";
- public SyncContext newInstance( RepositorySystemSession session, boolean shared )
+ @Override
+ public SyncContext newInstance( final RepositorySystemSession session, final boolean shared )
{
return new DefaultSyncContext();
}
- static class DefaultSyncContext
+ private static class DefaultSyncContext
implements SyncContext
{
-
- public void acquire( Collection<? extends Artifact> artifact, Collection<? extends Metadata> metadata )
+ @Override
+ public void acquire( final Collection<? extends Artifact> artifact,
+ final Collection<? extends Metadata> metadata )
{
}
+ @Override
public void close()
{
}
-
}
-
-}
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/SyncContextFactoryDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/SyncContextFactoryDelegate.java
new file mode 100644
index 0000000..9f768f3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/SyncContextFactoryDelegate.java
@@ -0,0 +1,29 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.spi.synccontext.SyncContextFactory;
+
+/**
+ * Internal marker interface to mark internal implementations for {@link SyncContextFactory}.
+ */
+public interface SyncContextFactoryDelegate extends SyncContextFactory
+{
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
new file mode 100644
index 0000000..862403e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
@@ -0,0 +1,139 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.util.ChecksumUtils;
+import org.eclipse.aether.util.ConfigUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.io.File;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Discriminating {@link NameMapper}, that wraps another {@link NameMapper} and adds a "discriminator" as prefix, that
+ * makes lock names unique including the hostname and local repository (by default). The discriminator may be passed
+ * in via {@link RepositorySystemSession} or is automatically calculated based on the local hostname and repository
+ * path. The implementation retains order of collection elements as it got it from
+ * {@link NameMapper#nameLocks(RepositorySystemSession, Collection, Collection)} method.
+ * <p>
+ * The default setup wraps {@link GAVNameMapper}, but manually may be created any instance needed.
+ */
+@Singleton
+@Named( DiscriminatingNameMapper.NAME )
+public class DiscriminatingNameMapper implements NameMapper
+{
+ public static final String NAME = "discriminating";
+
+ /**
+ * Configuration property to pass in discriminator
+ */
+ private static final String CONFIG_PROP_DISCRIMINATOR = "aether.syncContext.named.discriminating.discriminator";
+
+ /**
+ * Configuration property to pass in hostname
+ */
+ private static final String CONFIG_PROP_HOSTNAME = "aether.syncContext.named.discriminating.hostname";
+
+ private static final String DEFAULT_DISCRIMINATOR_DIGEST = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
+
+ private static final String DEFAULT_HOSTNAME = "localhost";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger( DiscriminatingNameMapper.class );
+
+ private final NameMapper nameMapper;
+
+ private final String hostname;
+
+ @Inject
+ public DiscriminatingNameMapper( @Named( GAVNameMapper.NAME ) final NameMapper nameMapper )
+ {
+ this.nameMapper = Objects.requireNonNull( nameMapper );
+ this.hostname = getHostname();
+ }
+
+ @Override
+ public Collection<String> nameLocks( final RepositorySystemSession session,
+ final Collection<? extends Artifact> artifacts,
+ final Collection<? extends Metadata> metadatas )
+ {
+ String discriminator = createDiscriminator( session );
+ return nameMapper.nameLocks( session, artifacts, metadatas ).stream().map( s -> discriminator + ":" + s )
+ .collect( toList() );
+ }
+
+ private String getHostname()
+ {
+ try
+ {
+ return InetAddress.getLocalHost().getHostName();
+ }
+ catch ( UnknownHostException e )
+ {
+ LOGGER.warn( "Failed to get hostname, using '{}'", DEFAULT_HOSTNAME, e );
+ return DEFAULT_HOSTNAME;
+ }
+ }
+
+ private String createDiscriminator( final RepositorySystemSession session )
+ {
+ String discriminator = ConfigUtils.getString( session, null, CONFIG_PROP_DISCRIMINATOR );
+
+ if ( discriminator == null || discriminator.isEmpty() )
+ {
+ String hostname = ConfigUtils.getString( session, this.hostname, CONFIG_PROP_HOSTNAME );
+ File basedir = session.getLocalRepository().getBasedir();
+ discriminator = hostname + ":" + basedir;
+ try
+ {
+ Map<String, Object> checksums = ChecksumUtils
+ .calc( discriminator.getBytes( StandardCharsets.UTF_8 ), Collections.singletonList( "SHA-1" ) );
+ Object checksum = checksums.get( "SHA-1" );
+
+ if ( checksum instanceof Exception )
+ {
+ throw (Exception) checksum;
+ }
+
+ return String.valueOf( checksum );
+ }
+ catch ( Exception e )
+ {
+ LOGGER.warn( "Failed to calculate discriminator digest, using '{}'", DEFAULT_DISCRIMINATOR_DIGEST, e );
+ return DEFAULT_DISCRIMINATOR_DIGEST;
+ }
+ }
+ return discriminator;
+ }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
new file mode 100644
index 0000000..ea0149c
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.Collection;
+import java.util.TreeSet;
+
+/**
+ * Artifact GAV {@link NameMapper}, uses artifact and metadata coordinates to name their corresponding locks. Is not
+ * considering local repository, only the artifact coordinates.
+ */
+@Singleton
+@Named( GAVNameMapper.NAME )
+public class GAVNameMapper implements NameMapper
+{
+ public static final String NAME = "gav";
+
+ @Override
+ public Collection<String> nameLocks( final RepositorySystemSession session,
+ final Collection<? extends Artifact> artifacts,
+ final Collection<? extends Metadata> metadatas )
+ {
+ // Deadlock prevention: https://stackoverflow.com/a/16780988/696632
+ // We must acquire multiple locks always in the same order!
+ Collection<String> keys = new TreeSet<>();
+ if ( artifacts != null )
+ {
+ for ( Artifact artifact : artifacts )
+ {
+ String key = "artifact:" + artifact.getGroupId()
+ + ":" + artifact.getArtifactId()
+ + ":" + artifact.getBaseVersion();
+ keys.add( key );
+ }
+ }
+
+ if ( metadatas != null )
+ {
+ for ( Metadata metadata : metadatas )
+ {
+ StringBuilder key = new StringBuilder( "metadata:" );
+ if ( !metadata.getGroupId().isEmpty() )
+ {
+ key.append( metadata.getGroupId() );
+ if ( !metadata.getArtifactId().isEmpty() )
+ {
+ key.append( ':' ).append( metadata.getArtifactId() );
+ if ( !metadata.getVersion().isEmpty() )
+ {
+ key.append( ':' ).append( metadata.getVersion() );
+ }
+ }
+ }
+ keys.add( key.toString() );
+ }
+ }
+ return keys;
+ }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
similarity index 52%
rename from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
rename to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
index 6210a9e..b5fd2f0 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl;
+package org.eclipse.aether.internal.impl.synccontext.named;
/*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -19,44 +19,24 @@ package org.eclipse.aether.internal.impl;
* under the License.
*/
-import java.util.Collection;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
import org.eclipse.aether.RepositorySystemSession;
-import org.eclipse.aether.SyncContext;
import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.metadata.Metadata;
+import java.util.Collection;
+
/**
- * A factory to create synchronization contexts. This default implementation does not provide any real
- * synchronization but merely completes the repository system.
+ * Component mapping lock names to passed in artifacts and metadata as required.
*/
-@Singleton
-@Named
-public class DefaultSyncContextFactory
- implements SyncContextFactory, org.eclipse.aether.impl.SyncContextFactory
+public interface NameMapper
{
-
- public SyncContext newInstance( RepositorySystemSession session, boolean shared )
- {
- return new DefaultSyncContext();
- }
-
- static class DefaultSyncContext
- implements SyncContext
- {
-
- public void acquire( Collection<? extends Artifact> artifact, Collection<? extends Metadata> metadata )
- {
- }
-
- public void close()
- {
- }
-
- }
-
+ /**
+ * Creates (opaque) names for passed in artifacts and metadata. Returned collection has max size of sum of the
+ * passed in artifacts and metadata collections, or less. If an empty collection is returned, there will be no
+ * locking happening. Never returns {@code null}. The resulting collection MUST BE "stable" (always sorted by
+ * same criteria) to avoid deadlocks by acquiring locks in same order, essentially disregarding the order of
+ * the input collections.
+ */
+ Collection<String> nameLocks( RepositorySystemSession session, Collection<? extends Artifact> artifacts,
+ Collection<? extends Metadata> metadatas );
}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java
new file mode 100644
index 0000000..6fb68d3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java
@@ -0,0 +1,184 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.named.NamedLock;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Adapter to adapt {@link NamedLockFactory} and {@link NamedLock} to {@link SyncContext}.
+ */
+public final class NamedLockFactoryAdapter
+{
+ private final NameMapper nameMapper;
+
+ private final NamedLockFactory namedLockFactory;
+
+ private final long time;
+
+ private final TimeUnit timeUnit;
+
+ public NamedLockFactoryAdapter( final NameMapper nameMapper, final NamedLockFactory namedLockFactory,
+ final long time, final TimeUnit timeUnit )
+ {
+ this.nameMapper = Objects.requireNonNull( nameMapper );
+ this.namedLockFactory = Objects.requireNonNull( namedLockFactory );
+ if ( time < 0L )
+ {
+ throw new IllegalArgumentException( "time cannot be negative" );
+ }
+ this.time = time;
+ this.timeUnit = Objects.requireNonNull( timeUnit );
+ }
+
+ public SyncContext newInstance( final RepositorySystemSession session, final boolean shared )
+ {
+ return new AdaptedLockSyncContext( session, shared, nameMapper, namedLockFactory, time, timeUnit );
+ }
+
+ public void shutdown()
+ {
+ namedLockFactory.shutdown();
+ }
+
+ private static class AdaptedLockSyncContext implements SyncContext
+ {
+ private static final Logger LOGGER = LoggerFactory.getLogger( AdaptedLockSyncContext.class );
+
+ private final RepositorySystemSession session;
+
+ private final boolean shared;
+
+ private final NameMapper lockNaming;
+
+ private final SessionAwareNamedLockFactory sessionAwareNamedLockFactory;
+
+ private final NamedLockFactory namedLockFactory;
+
+ private final long time;
+
+ private final TimeUnit timeUnit;
+
+ private final Deque<NamedLock> locks;
+
+ private AdaptedLockSyncContext( final RepositorySystemSession session, final boolean shared,
+ final NameMapper lockNaming, final NamedLockFactory namedLockFactory,
+ final long time, final TimeUnit timeUnit )
+ {
+ this.session = session;
+ this.shared = shared;
+ this.lockNaming = lockNaming;
+ this.sessionAwareNamedLockFactory = namedLockFactory instanceof SessionAwareNamedLockFactory
+ ? (SessionAwareNamedLockFactory) namedLockFactory : null;
+ this.namedLockFactory = namedLockFactory;
+ this.time = time;
+ this.timeUnit = timeUnit;
+ this.locks = new ArrayDeque<>();
+ }
+
+ @Override
+ public void acquire( Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas )
+ {
+ Collection<String> keys = lockNaming.nameLocks( session, artifacts, metadatas );
+ if ( keys.isEmpty() )
+ {
+ return;
+ }
+
+ LOGGER.trace( "Need {} {} lock(s) for {}", keys.size(), shared ? "read" : "write", keys );
+ int acquiredLockCount = 0;
+ for ( String key : keys )
+ {
+ NamedLock namedLock = sessionAwareNamedLockFactory != null ? sessionAwareNamedLockFactory
+ .getLock( session, key ) : namedLockFactory.getLock( key );
+ try
+ {
+ LOGGER.trace( "Acquiring {} lock for '{}'",
+ shared ? "read" : "write", key );
+
+ boolean locked;
+ if ( shared )
+ {
+ locked = namedLock.lockShared( time, timeUnit );
+ }
+ else
+ {
+ locked = namedLock.lockExclusively( time, timeUnit );
+ }
+
+ if ( !locked )
+ {
+ LOGGER.trace( "Failed to acquire {} lock for '{}'",
+ shared ? "read" : "write", key );
+
+ namedLock.close();
+ throw new IllegalStateException(
+ "Could not acquire " + ( shared ? "read" : "write" )
+ + " lock for '" + namedLock.name() + "'" );
+ }
+
+ locks.push( namedLock );
+ acquiredLockCount++;
+ }
+ catch ( InterruptedException e )
+ {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException( e );
+ }
+ }
+ LOGGER.trace( "Total locks acquired: {}", acquiredLockCount );
+ }
+
+ @Override
+ public void close()
+ {
+ if ( locks.isEmpty() )
+ {
+ return;
+ }
+
+ // Release locks in reverse insertion order
+ int released = 0;
+ while ( !locks.isEmpty() )
+ {
+ try ( NamedLock namedLock = locks.pop() )
+ {
+ LOGGER.trace( "Releasing {} lock for '{}'",
+ shared ? "read" : "write", namedLock.name() );
+ namedLock.unlock();
+ released++;
+ }
+ }
+ LOGGER.trace( "Total locks released: {}", released );
+ }
+ }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SessionAwareNamedLockFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SessionAwareNamedLockFactory.java
new file mode 100644
index 0000000..28d7961
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SessionAwareNamedLockFactory.java
@@ -0,0 +1,40 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.named.NamedLock;
+import org.eclipse.aether.named.NamedLockFactory;
+
+/**
+ * A {@link NamedLockFactory} that wants to make use of {@link RepositorySystemSession}.
+ */
+public interface SessionAwareNamedLockFactory extends NamedLockFactory
+{
+ /**
+ * Creates or reuses existing {@link NamedLock}. Returns instance MUST BE treated as "resource", best in
+ * try-with-resource block.
+ *
+ * @param session the repository system session, must not be {@code null}
+ * @param name the lock name, must not be {@code null}
+ * @return named the lock instance, never {@code null}
+ */
+ NamedLock getLock( RepositorySystemSession session, String name );
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
new file mode 100644
index 0000000..14fc7e7
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.util.ConfigUtils;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * Static {@link NameMapper}, always assigns one same name, effectively becoming equivalent to "static" sync context.
+ */
+@Singleton
+@Named( StaticNameMapper.NAME )
+public class StaticNameMapper implements NameMapper
+{
+ public static final String NAME = "static";
+
+ /**
+ * Configuration property to pass in static name
+ */
+ private static final String CONFIG_PROP_NAME = "aether.syncContext.named.static.name";
+
+ private final String name;
+
+ /**
+ * Uses string {@code "static"} for the static name
+ */
+ @Inject
+ public StaticNameMapper()
+ {
+ this( NAME );
+ }
+
+ /**
+ * Uses passed in non-{@code null} string for the static name
+ */
+ public StaticNameMapper( final String name )
+ {
+ this.name = Objects.requireNonNull( name );
+ }
+
+ @Override
+ public Collection<String> nameLocks( final RepositorySystemSession session,
+ final Collection<? extends Artifact> artifacts,
+ final Collection<? extends Metadata> metadatas )
+ {
+ return Collections.singletonList( ConfigUtils.getString( session, name, CONFIG_PROP_NAME ) );
+ }
+}
diff --git a/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
new file mode 100644
index 0000000..54697a5
--- /dev/null
+++ b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
@@ -0,0 +1,95 @@
+${esc.hash} About SyncContextFactory
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+In this module there are several implementations of `SyncContextFactory`. The `SyncContextFactory` component is
+responsible to synchronize (coordinate) access to shared resources in Maven Resolver internals. Some examples:
+- single threaded (ST) build on a single host: given resolution by default happens in multi threaded (MT) way, files
+ in a local repository must be protected from simultaneous access. In this case, it is enough for coordination to
+ happen "locally", at single JVM process level.
+- multi threaded (MT) build on a single host: same issues as above, but multiplied by MT build threads. As above,
+ in this case, it is enough for coordination to happen "locally", at JVM process level.
+- multi process (MP) build on a single host: in case multiple processes "share" (access, are set to use) the same local
+ repository, coordination must exist between processes as well. Here the coordination must span across multiple
+ JVM processes sharing same resource.
+- multi process (MP) build on multiple hosts using same (for example NFS mounted) local repository. Here again, the
+ shared resource (the NFS mounted local repository) should be protected from simultaneous access. Here the coordination
+ must span all the processes across all the hosts involved.
+
+As can be seen, the ST and MT cases can be solved by "local" locking at JVM level only. In MP single host case
+a cross process, and in MP multi-host case a cross process and cross host coordination is required.
+
+The defaults in Maven Resolver cover ST and MT cases out of the box (by default).
+
+${esc.hash}${esc.hash} Provided Implementations
+
+This module provides a default implementation for the interface `org.eclipse.aether.spi.synccontext.SyncContextFactory`
+that is able to delegate to three implementations:
+
+- `named` (the default) is backed by named locks, and provide locks for MT and MP builds.
+- `global` (was experimental in pre 1.7.0 version) is using a single JVM
+ `java.util.concurrent.locks.ReentrantReadWriteLock` to protect read and write actions, hence suitable in MT
+ builds only. But, even in MT builds it is inferior from `named` due congestion on single lock instance.
+- `nolock` (was default in pre 1.7.0 version) is actually a no-op implementation. It is fundamentally flawed, as
+ even single threaded builds may produce concurrency issues (see MRESOLVER-153). Should not be used by end-users,
+ is left for "R&D" reasons only.
+
+To choose which `SyncContextFactory` you want to use, the `aether.syncContext.impl` JVM system property should be set
+to some value from those above. In case of wrong value, the factory constructor, hence, Maven itself will fail to
+initialize. The default value used if none is set by user is `named`.
+
+${esc.hash}${esc.hash} Configuration Options for "named" `SyncContextFactory`
+
+You can control and configure several aspects of `NamedSyncContextFactory`:
+
+- `aether.syncContext.named.factory` (optional, default is `rwlock-local`): the named lock factory to be used.
+- `aether.syncContext.named.nameMapper` (optional, default is `gav`): creates lock names out of artifact coordinates.
+- `aether.syncContext.named.time` (optional, default is 30): the time value for being blocked by trying to acquire a lock.
+- `aether.syncContext.named.time.unit` (optional, default is `java.util.concurrent.TimeUnit.SECONDS`): the time unit
+ of time value.
+
+For the `aether.syncContext.named.nameMapper` property following values are allowed:
+
+- `discriminating` (default), uses hostname + local repo + GAV to create unique lock names for artifacts.
+- `gav` uses GAV to create unique lock names for artifacts and metadata. Is not unique if multiple local repositories are involved.
+- `static` uses static (same) string as lock name for any input, effectively providing functionality same as "global"
+ locking SyncContextFactory.
+
+For the `aether.syncContext.named.factory` property following values are allowed:
+
+- `rwlock-local` (default), uses JVM `ReentrantReadWriteLock` per lock name, usable for MT builds.
+- `semaphore-local`, uses JVM `Semaphore` per lock name, usable for MT builds.
+
+Extra values for factory (these need extra setup and will work with Sisu DI only):
+
+- `semaphore-hazelcast`, distributed, usable with MT and MP builds, **extra setup needed**.
+- `semaphore-hazelcast-client`, distributed, usable with MT and MP builds, **extra setup needed**.
+- `rwlock-redisson`, distributed, usable with MT and MP builds, **extra setup needed**.
+- `semaphore-redisson`, distributed, usable with MT and MP builds, **extra setup needed**.
+
+Other configuration keys:
+
+- `aether.syncContext.named.static.name`, the value to use as static lock name, if `static` name mapper is used.
+ If not set, defaults to "static".
+- `aether.syncContext.named.discriminating.discriminator`, when `discriminating` name mapper is used, sets the a
+ discriminator uniquely identifying a host and local repository pair. If not set, discriminator is calculated by
+ applying `sha1(hostname + ":" + localRepoPath)` + GAV name mapper.
+- `aether.syncContext.named.discriminating.hostname`, the hostname to be used to calculate discriminator value,
+ if above value not set. If not set, the hostname is detected using Java API.
diff --git a/maven-resolver-impl/src/site/site.xml b/maven-resolver-impl/src/site/site.xml
index 946c950..5b09dcd 100644
--- a/maven-resolver-impl/src/site/site.xml
+++ b/maven-resolver-impl/src/site/site.xml
@@ -26,6 +26,7 @@ under the License.
<body>
<menu name="Overview">
<item name="Introduction" href="index.html"/>
+ <item name="About SyncContextFactory" href="synccontextfactory.html"/>
<item name="JavaDocs" href="apidocs/index.html"/>
<item name="Source Xref" href="xref/index.html"/>
<!--item name="FAQ" href="faq.html"/-->
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalReadWriteLockAdapterTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalReadWriteLockAdapterTest.java
new file mode 100644
index 0000000..82a2f01
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalReadWriteLockAdapterTest.java
@@ -0,0 +1,33 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class LocalReadWriteLockAdapterTest
+ extends NamedLockFactoryAdapterTestSupport
+{
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ namedLockFactory = new LocalReadWriteLockNamedLockFactory();
+ createAdapter();
+ }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalSemaphoreAdapterTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalSemaphoreAdapterTest.java
new file mode 100644
index 0000000..adf88fb
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/LocalSemaphoreAdapterTest.java
@@ -0,0 +1,33 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class LocalSemaphoreAdapterTest
+ extends NamedLockFactoryAdapterTestSupport
+{
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ namedLockFactory = new LocalSemaphoreNamedLockFactory();
+ createAdapter();
+ }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java
new file mode 100644
index 0000000..87cab6f
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java
@@ -0,0 +1,250 @@
+package org.eclipse.aether.internal.impl.synccontext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.impl.synccontext.named.*;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.spi.synccontext.SyncContextFactory;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * UT support for {@link SyncContextFactory}.
+ */
+public abstract class NamedLockFactoryAdapterTestSupport {
+ private static final long ADAPTER_TIME = 100L;
+
+ private static final TimeUnit ADAPTER_TIME_UNIT = TimeUnit.MILLISECONDS;
+
+ /**
+ * Subclass MAY populate this field but subclass must take care of proper cleanup as well, if needed!
+ */
+ protected static NameMapper nameMapper = new DiscriminatingNameMapper(new GAVNameMapper());
+
+ /**
+ * Subclass MUST populate this field but subclass must take care of proper cleanup as well, if needed! Once set,
+ * subclass must MUST call {@link #createAdapter()}.
+ */
+ protected static NamedLockFactory namedLockFactory;
+
+ private static NamedLockFactoryAdapter adapter;
+
+ private RepositorySystemSession session;
+
+ public static void createAdapter() {
+ Objects.requireNonNull(namedLockFactory, "NamedLockFactory not set");
+ adapter = new NamedLockFactoryAdapter(nameMapper, namedLockFactory, ADAPTER_TIME, ADAPTER_TIME_UNIT);
+ }
+
+ @AfterClass
+ public static void cleanupAdapter() {
+ if (adapter != null) {
+ adapter.shutdown();
+ }
+ }
+
+ @Before
+ public void before() throws IOException {
+ Files.createDirectories(Paths.get(System.getProperty("java.io.tmpdir"))); // hack for Surefire
+ LocalRepository localRepository = new LocalRepository(Files.createTempDirectory("test").toFile());
+ session = mock(RepositorySystemSession.class);
+ when(session.getLocalRepository()).thenReturn(localRepository);
+ }
+
+ @Test
+ public void justCreateAndClose() {
+ adapter.newInstance(session, false).close();
+ }
+
+ @Test
+ public void justAcquire() {
+ try (SyncContext syncContext = adapter.newInstance(session, false)) {
+ syncContext.acquire(
+ Arrays.asList(new DefaultArtifact("groupId:artifactId:1.0"), new DefaultArtifact("groupId:artifactId:1.1")),
+ null
+ );
+ }
+ }
+
+ @Test(timeout = 5000)
+ public void sharedAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void exclusiveAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void mixedAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedSharedShared() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(true, winners, losers, adapter, session,
+ new Access(true, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedExclusiveShared() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(false, winners, losers, adapter, session,
+ new Access(true, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedExclusiveExclusive() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(false, winners, losers, adapter, session,
+ new Access(false, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedSharedExclusive() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner (outer)
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser (inner)
+ Thread t1 = new Thread(
+ new Access(true, winners, losers, adapter, session,
+ new Access(false, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ private static class Access implements Runnable {
+ final boolean shared;
+ final CountDownLatch winner;
+ final CountDownLatch loser;
+ final NamedLockFactoryAdapter adapter;
+ final RepositorySystemSession session;
+ final Access chained;
+
+ public Access(boolean shared,
+ CountDownLatch winner,
+ CountDownLatch loser,
+ NamedLockFactoryAdapter adapter,
+ RepositorySystemSession session,
+ Access chained) {
+ this.shared = shared;
+ this.winner = winner;
+ this.loser = loser;
+ this.adapter = adapter;
+ this.session = session;
+ this.chained = chained;
+ }
+
+ @Override
+ public void run() {
+ try {
+ try (SyncContext syncContext = adapter.newInstance(session, shared)) {
+ syncContext.acquire(
+ Arrays.asList(new DefaultArtifact("groupId:artifactId:1.0"), new DefaultArtifact("groupId:artifactId:1.1")),
+ null
+ );
+ winner.countDown();
+ if (chained != null) {
+ chained.run();
+ }
+ loser.await();
+ } catch (IllegalStateException e) {
+ e.printStackTrace(); // for ref purposes
+ loser.countDown();
+ winner.await();
+ }
+ } catch (InterruptedException e) {
+ Assert.fail("interrupted");
+ }
+ }
+ }
+}
diff --git a/maven-resolver-impl/pom.xml b/maven-resolver-named-locks-hazelcast/pom.xml
similarity index 65%
copy from maven-resolver-impl/pom.xml
copy to maven-resolver-named-locks-hazelcast/pom.xml
index 7528884..2d78607 100644
--- a/maven-resolver-impl/pom.xml
+++ b/maven-resolver-named-locks-hazelcast/pom.xml
@@ -1,139 +1,129 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver</artifactId>
- <version>1.7.0-SNAPSHOT</version>
- </parent>
-
- <artifactId>maven-resolver-impl</artifactId>
-
- <name>Maven Artifact Resolver Implementation</name>
- <description>
- An implementation of the repository system.
- </description>
-
- <properties>
- <Automatic-Module-Name>org.apache.maven.resolver.impl</Automatic-Module-Name>
- <Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
- </properties>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-spi</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-util</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- <version>3.8.1</version>
- </dependency>
- <dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.eclipse.sisu</groupId>
- <artifactId>org.eclipse.sisu.inject</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
- <classifier>no_aop</classifier>
- <scope>provided</scope>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>failureaccess</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-simple</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.hamcrest</groupId>
- <artifactId>hamcrest-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-test-util</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.eclipse.sisu</groupId>
- <artifactId>sisu-maven-plugin</artifactId>
- </plugin>
- <plugin>
- <groupId>biz.aQute.bnd</groupId>
- <artifactId>bnd-maven-plugin</artifactId>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <configuration>
- <archive>
- <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
- </archive>
- </configuration>
- </plugin>
- </plugins>
- </build>
-</project>
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver</artifactId>
+ <version>1.7.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>maven-resolver-named-locks-hazelcast</artifactId>
+
+ <name>Maven Artifact Resolver Named Locks using Hazelcast</name>
+ <description>
+ A synchronization utility implementation using Hazelcast.
+ </description>
+
+ <properties>
+ <Automatic-Module-Name>org.apache.maven.resolver.named.hazelcast</Automatic-Module-Name>
+ <Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-named-locks</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast</artifactId>
+ <version>4.1.2</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.eclipse.sisu</groupId>
+ <artifactId>sisu-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>it</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactory.java b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactory.java
new file mode 100644
index 0000000..81e428e
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactory.java
@@ -0,0 +1,50 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import com.hazelcast.core.Hazelcast;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+/**
+ * {@link HazelcastSemaphoreNamedLockFactory} using Hazelcast and {@link
+ * com.hazelcast.core.HazelcastInstance#getCPSubsystem()} method to get CP semaphore. For this to work, the Hazelcast
+ * cluster the client is connecting to must be CP enabled cluster.
+ */
+@Singleton
+@Named( HazelcastCPSemaphoreNamedLockFactory.NAME )
+public class HazelcastCPSemaphoreNamedLockFactory
+ extends HazelcastSemaphoreNamedLockFactory
+{
+ public static final String NAME = "semaphore-hazelcast";
+
+ @Inject
+ public HazelcastCPSemaphoreNamedLockFactory()
+ {
+ super(
+ Hazelcast.newHazelcastInstance(),
+ ( hazelcastInstance, name ) -> hazelcastInstance.getCPSubsystem().getSemaphore( NAME_PREFIX + name ),
+ false,
+ true
+ );
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactory.java b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactory.java
new file mode 100644
index 0000000..097d120
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactory.java
@@ -0,0 +1,50 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import com.hazelcast.client.HazelcastClient;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+/**
+ * Provider of {@link HazelcastSemaphoreNamedLockFactory} using Hazelcast Client and {@link
+ * com.hazelcast.core.HazelcastInstance#getCPSubsystem()} method to get CP semaphore. For this to work, the Hazelcast
+ * cluster the client is connecting to must be CP enabled cluster.
+ */
+@Singleton
+@Named( HazelcastClientCPSemaphoreNamedLockFactory.NAME )
+public class HazelcastClientCPSemaphoreNamedLockFactory
+ extends HazelcastSemaphoreNamedLockFactory
+{
+ public static final String NAME = "semaphore-hazelcast-client";
+
+ @Inject
+ public HazelcastClientCPSemaphoreNamedLockFactory()
+ {
+ super(
+ HazelcastClient.newHazelcastClient(),
+ ( hazelcastInstance, name ) -> hazelcastInstance.getCPSubsystem().getSemaphore( NAME_PREFIX + name ),
+ false,
+ true
+ );
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastSemaphoreNamedLockFactory.java b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastSemaphoreNamedLockFactory.java
new file mode 100644
index 0000000..45d5f40
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/main/java/org/eclipse/aether/named/hazelcast/HazelcastSemaphoreNamedLockFactory.java
@@ -0,0 +1,117 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.cp.ISemaphore;
+import org.eclipse.aether.named.support.AdaptedSemaphoreNamedLock;
+import org.eclipse.aether.named.support.AdaptedSemaphoreNamedLock.AdaptedSemaphore;
+import org.eclipse.aether.named.support.NamedLockFactorySupport;
+import org.eclipse.aether.named.support.NamedLockSupport;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+
+/**
+ * Factory of {@link AdaptedSemaphoreNamedLock} instances using adapted Hazelcast {@link ISemaphore}. This class may
+ * use {@link HazelcastInstance} backed by Hazelcast Server or Hazelcast Client.
+ */
+public class HazelcastSemaphoreNamedLockFactory
+ extends NamedLockFactorySupport
+{
+ protected static final String NAME_PREFIX = "maven:resolver:";
+
+ private final HazelcastInstance hazelcastInstance;
+
+ private final BiFunction<HazelcastInstance, String, ISemaphore> semaphoreFunction;
+
+ private final boolean destroySemaphore;
+
+ private final boolean manageHazelcast;
+
+ private final ConcurrentHashMap<String, ISemaphore> semaphores;
+
+ public HazelcastSemaphoreNamedLockFactory(
+ final HazelcastInstance hazelcastInstance,
+ final BiFunction<HazelcastInstance, String, ISemaphore> semaphoreFunction,
+ final boolean destroySemaphore,
+ final boolean manageHazelcast
+ )
+ {
+ this.hazelcastInstance = hazelcastInstance;
+ this.semaphoreFunction = semaphoreFunction;
+ this.destroySemaphore = destroySemaphore;
+ this.manageHazelcast = manageHazelcast;
+ this.semaphores = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ protected NamedLockSupport createLock( final String name )
+ {
+ ISemaphore semaphore = semaphores.computeIfAbsent(
+ name, k -> semaphoreFunction.apply( hazelcastInstance, k )
+ );
+ return new AdaptedSemaphoreNamedLock( name, this, new HazelcastSemaphore( semaphore ) );
+ }
+
+ @Override
+ public void shutdown()
+ {
+ if ( manageHazelcast )
+ {
+ hazelcastInstance.shutdown();
+ }
+ }
+
+ @Override
+ protected void destroyLock( final NamedLockSupport lock )
+ {
+ ISemaphore semaphore = semaphores.remove( lock.name() );
+ if ( destroySemaphore )
+ {
+ semaphore.destroy();
+ }
+ }
+
+ private static final class HazelcastSemaphore implements AdaptedSemaphore
+ {
+ private final ISemaphore semaphore;
+
+ private HazelcastSemaphore( final ISemaphore semaphore )
+ {
+ semaphore.init( Integer.MAX_VALUE );
+ this.semaphore = semaphore;
+ }
+
+ @Override
+ public boolean tryAcquire( final int perms, final long time, final TimeUnit unit )
+ throws InterruptedException
+ {
+ return semaphore.tryAcquire( perms, time, unit );
+ }
+
+ @Override
+ public void release( final int perms )
+ {
+ semaphore.release( perms );
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/site/markdown/index.md.vm b/maven-resolver-named-locks-hazelcast/src/site/markdown/index.md.vm
new file mode 100644
index 0000000..886bcd4
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/site/markdown/index.md.vm
@@ -0,0 +1,51 @@
+${esc.hash} Named Locks using Hazelcast
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+This module implement named locks using Hazelcast. It provides two implementations, that are distributed and rely on
+Hazelcast 4.x ISemaphores.
+
+Out of the box "hazelcast" (distributed) named lock implementations are following:
+
+- `semaphore-hazelcast` implemented in `org.eclipse.aether.named.hazelcast.HazelcastCPSemaphoreNamedLockFactory` that uses
+ Hazelcast backed `com.hazelcast.cp.ISemaphore`. Full Hazelcast member is used here.
+- `semaphore-hazelcast-client` implemented in `org.eclipse.aether.named.hazelcast.HazelcastClientCPSemaphoreNamedLockFactory`
+ that uses Hazelcast Client backed `com.hazelcast.cp.ISemaphore`. This implementation uses Hazelcast Client, so
+ connection to some existing Hazelcast cluster, and its existence is a requirement.
+
+${esc.hash}${esc.hash} Open Issues/Notes
+
+- It only works when Sisu DI is used and not the bundled `AetherModule` or
+ `ServiceLocator` (Maven uses Sisu dependency injection).
+- Usage from plugins has not been tested yet.
+- The `furnace-maven-plugin` does not work this implementation because it uses `ServiceLocator` instead
+ of dependency injection.
+
+${esc.hash}${esc.hash} Installation/Testing
+
+- Create the directory `${maven.home}/lib/ext/hazelcast/`.
+- Modify `${maven.home}/bin/m2.conf` by adding `load ${maven.home}/lib/ext/hazelcast/*.jar`
+ right after the `${maven.conf}/logging` line.
+- Copy the following dependency from Maven Central to `${maven.home}/lib/ext/hazelcast/`:
+ <pre class="source">
+ ├── <a href="https://repo.maven.apache.org/maven2/com/hazelcast/hazelcast/4.1.1/hazelcast-4.1.1.jar">hazelcast-4.1.1.jar</a></pre>
+- Optionally configure Hazelcast instance with `${maven.conf}/hazelcast.xml` or `${maven.conf}/hazelcast-client.xml`
+ (see Hazelcast documentation for possibilities, or, see test resources of this project as starter).
+- Now start a multithreaded Maven build on your project and make sure `NamedSyncContextFactory` is being used.
diff --git a/maven-resolver-synccontext-global/src/site/site.xml b/maven-resolver-named-locks-hazelcast/src/site/site.xml
similarity index 97%
copy from maven-resolver-synccontext-global/src/site/site.xml
copy to maven-resolver-named-locks-hazelcast/src/site/site.xml
index 8183b5d..0a3c423 100644
--- a/maven-resolver-synccontext-global/src/site/site.xml
+++ b/maven-resolver-named-locks-hazelcast/src/site/site.xml
@@ -22,7 +22,7 @@ under the License.
<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
- name="GlobalSyncContext">
+ name="Named Locks using Hazelcast">
<body>
<menu name="Overview">
<item name="Introduction" href="index.html"/>
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreAdapterIT.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreAdapterIT.java
new file mode 100644
index 0000000..d2faad8
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreAdapterIT.java
@@ -0,0 +1,31 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.BeforeClass;
+
+public class HazelcastCPSemaphoreAdapterIT
+ extends NamedLockFactoryAdapterTestSupport {
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ setNamedLockFactory(new HazelcastCPSemaphoreNamedLockFactory());
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactoryIT.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactoryIT.java
new file mode 100644
index 0000000..c53f501
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastCPSemaphoreNamedLockFactoryIT.java
@@ -0,0 +1,39 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class HazelcastCPSemaphoreNamedLockFactoryIT
+ extends NamedLockFactoryTestSupport {
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ namedLockFactory = new HazelcastCPSemaphoreNamedLockFactory();
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ if (namedLockFactory != null) {
+ namedLockFactory.shutdown();
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreAdapterIT.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreAdapterIT.java
new file mode 100644
index 0000000..b5d0256
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreAdapterIT.java
@@ -0,0 +1,42 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class HazelcastClientCPSemaphoreAdapterIT
+ extends NamedLockFactoryAdapterTestSupport {
+
+ private static HazelcastClientUtils utils;
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ utils = new HazelcastClientUtils().createCpCluster();
+ setNamedLockFactory(new HazelcastClientCPSemaphoreNamedLockFactory());
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ if (utils != null) {
+ utils.cleanup();
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactoryIT.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactoryIT.java
new file mode 100644
index 0000000..2718bec
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientCPSemaphoreNamedLockFactoryIT.java
@@ -0,0 +1,45 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class HazelcastClientCPSemaphoreNamedLockFactoryIT
+ extends NamedLockFactoryTestSupport {
+
+ private static HazelcastClientUtils utils;
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ utils = new HazelcastClientUtils().createCpCluster();
+ namedLockFactory = new HazelcastClientCPSemaphoreNamedLockFactory();
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ if (namedLockFactory != null) {
+ namedLockFactory.shutdown();
+ }
+ if (utils != null) {
+ utils.cleanup();
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientUtils.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientUtils.java
new file mode 100644
index 0000000..1d3ecc2
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/HazelcastClientUtils.java
@@ -0,0 +1,88 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import com.hazelcast.config.Config;
+import com.hazelcast.config.cp.CPSubsystemConfig;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Hazelcast Client connects to remote server (or servers, in case of cluster). This class is a helper to
+ * help create and validate remote Hazelcast environment, that Hazelcast Client will connect to.
+ */
+public final class HazelcastClientUtils {
+
+ private static final int CP_CLUSTER_NODES = 3;
+
+ private List<HazelcastInstance> servers;
+
+ /**
+ * Creates a Hazelcast server instance, that client may connect to.
+ */
+ public HazelcastClientUtils createSingleServer() {
+ servers = Collections.singletonList(Hazelcast.newHazelcastInstance());
+ return this;
+ }
+
+ /**
+ * Creates a Hazelcast CP cluster, that client may connect to. When this method returns, cluster is not only
+ * created but it is properly formed as well.
+ */
+ public HazelcastClientUtils createCpCluster() {
+ ArrayList<HazelcastInstance> instances = new ArrayList<>(CP_CLUSTER_NODES);
+ for (int i = 0; i < CP_CLUSTER_NODES; i++) {
+ HazelcastInstance instance = Hazelcast.newHazelcastInstance(
+ loadCPNodeConfig().setInstanceName("node" + i)
+ );
+ instances.add(instance);
+ }
+ servers = instances;
+
+ // make sure HZ CP Cluster is ready
+ for (HazelcastInstance instance : servers) {
+ // this call will block until CP cluster if formed
+ // important thing here is that this blocking does not happen during timeout surrounded test
+ // hence, once this method returns, the CP cluster is "ready for use" without any delay.
+ instance.getCPSubsystem().getAtomicLong(instance.getName());
+ }
+ return this;
+ }
+
+ /**
+ * Shuts down the created server(s)
+ */
+ public void cleanup() {
+ if (servers != null) {
+ servers.forEach(HazelcastInstance::shutdown);
+ }
+ }
+
+ private Config loadCPNodeConfig() {
+ // "cluster" for CP needs some config tweak from the test/resources/hazelcast.xml
+ Config config = Config.load().setCPSubsystemConfig(new CPSubsystemConfig().setCPMemberCount(3));
+ config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(true);
+ return config;
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java
new file mode 100644
index 0000000..7fc8241
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java
@@ -0,0 +1,246 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactoryAdapter;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.spi.synccontext.SyncContextFactory;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * UT support for {@link SyncContextFactory}.
+ */
+public abstract class NamedLockFactoryAdapterTestSupport
+{
+ private static final long ADAPTER_TIME = 100L;
+
+ private static final TimeUnit ADAPTER_TIME_UNIT = TimeUnit.MILLISECONDS;
+
+ /**
+ * Subclass should populate this field, using {@link #setNamedLockFactory(NamedLockFactory)}, but subclass
+ * must take care of proper cleanup as well, if needed!
+ */
+ private static NamedLockFactoryAdapter adapter;
+
+ private RepositorySystemSession session;
+
+ protected static void setNamedLockFactory(final NamedLockFactory namedLockFactory) {
+ adapter = new NamedLockFactoryAdapter(
+ new DiscriminatingNameMapper(new GAVNameMapper()), namedLockFactory, ADAPTER_TIME, ADAPTER_TIME_UNIT
+ );
+ }
+
+ @AfterClass
+ public static void cleanupAdapter() {
+ if (adapter != null) {
+ adapter.shutdown();
+ }
+ }
+
+ @Before
+ public void before() throws IOException {
+ Files.createDirectories(Paths.get(System.getProperty("java.io.tmpdir"))); // hack for Surefire
+ LocalRepository localRepository = new LocalRepository(Files.createTempDirectory("test").toFile());
+ session = mock(RepositorySystemSession.class);
+ when(session.getLocalRepository()).thenReturn(localRepository);
+ }
+
+ @Test
+ public void justCreateAndClose() {
+ adapter.newInstance(session, false).close();
+ }
+
+ @Test
+ public void justAcquire() {
+ try (SyncContext syncContext = adapter.newInstance(session, false)) {
+ syncContext.acquire(
+ Arrays.asList(new DefaultArtifact("groupId:artifactId:1.0"), new DefaultArtifact("groupId:artifactId:1.1")),
+ null
+ );
+ }
+ }
+
+ @Test(timeout = 5000)
+ public void sharedAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void exclusiveAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void mixedAccess() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(true, winners, losers, adapter, session, null));
+ Thread t2 = new Thread(new Access(false, winners, losers, adapter, session, null));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedSharedShared() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(true, winners, losers, adapter, session,
+ new Access(true, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedExclusiveShared() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(false, winners, losers, adapter, session,
+ new Access(true, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedExclusiveExclusive() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(
+ new Access(false, winners, losers, adapter, session,
+ new Access(false, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void nestedSharedExclusive() throws InterruptedException {
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner (outer)
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser (inner)
+ Thread t1 = new Thread(
+ new Access(true, winners, losers, adapter, session,
+ new Access(false, winners, losers, adapter, session, null)
+ )
+ );
+ t1.start();
+ t1.join();
+ winners.await();
+ losers.await();
+ }
+
+ private static class Access implements Runnable {
+ final boolean shared;
+ final CountDownLatch winner;
+ final CountDownLatch loser;
+ final NamedLockFactoryAdapter adapter;
+ final RepositorySystemSession session;
+ final Access chained;
+
+ public Access(boolean shared,
+ CountDownLatch winner,
+ CountDownLatch loser,
+ NamedLockFactoryAdapter adapter,
+ RepositorySystemSession session,
+ Access chained) {
+ this.shared = shared;
+ this.winner = winner;
+ this.loser = loser;
+ this.adapter = adapter;
+ this.session = session;
+ this.chained = chained;
+ }
+
+ @Override
+ public void run() {
+ try {
+ try (SyncContext syncContext = adapter.newInstance(session, shared)) {
+ syncContext.acquire(
+ Arrays.asList(new DefaultArtifact("groupId:artifactId:1.0"), new DefaultArtifact("groupId:artifactId:1.1")),
+ null
+ );
+ winner.countDown();
+ if (chained != null) {
+ chained.run();
+ }
+ loser.await();
+ } catch (IllegalStateException e) {
+ loser.countDown();
+ winner.await();
+ }
+ }
+ catch (InterruptedException e) {
+ Assert.fail("interrupted");
+ }
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryTestSupport.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryTestSupport.java
new file mode 100644
index 0000000..c128e7f
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryTestSupport.java
@@ -0,0 +1,196 @@
+package org.eclipse.aether.named.hazelcast;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.NamedLock;
+import org.eclipse.aether.named.NamedLockFactory;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+
+/**
+ * UT support for {@link NamedLockFactory}.
+ */
+public abstract class NamedLockFactoryTestSupport {
+
+ protected static NamedLockFactory namedLockFactory;
+
+ @Rule
+ public TestName testName = new TestName();
+
+ @Test
+ public void refCounting() {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name);
+ NamedLock two = namedLockFactory.getLock(name)) {
+ assertThat(one, sameInstance(two));
+ one.close();
+ two.close();
+
+ try (NamedLock three = namedLockFactory.getLock(name)) {
+ assertThat(three, not(sameInstance(two)));
+ }
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void unlockWoLock() {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void wwBoxing() throws InterruptedException {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void rrBoxing() throws InterruptedException {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void wrBoxing() throws InterruptedException {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void rwBoxing() throws InterruptedException {
+ final String name = testName.getMethodName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(false));
+ one.unlock();
+ }
+ }
+
+ @Test(timeout = 5000)
+ public void sharedAccess() throws InterruptedException {
+ final String name = testName.getMethodName();
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winner
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 loser
+ Thread t1 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void exclusiveAccess() throws InterruptedException {
+ final String name = testName.getMethodName();
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void mixedAccess() throws InterruptedException {
+ final String name = testName.getMethodName();
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ private static class Access implements Runnable {
+ final NamedLockFactory namedLockFactory;
+ final String name;
+ final boolean shared;
+ final CountDownLatch winner;
+ final CountDownLatch loser;
+
+ public Access(NamedLockFactory namedLockFactory,
+ String name,
+ boolean shared,
+ CountDownLatch winner,
+ CountDownLatch loser) {
+ this.namedLockFactory = namedLockFactory;
+ this.name = name;
+ this.shared = shared;
+ this.winner = winner;
+ this.loser = loser;
+ }
+
+ @Override
+ public void run() {
+ try (NamedLock lock = namedLockFactory.getLock(name)) {
+ if (shared ? lock.lockShared(100L, TimeUnit.MILLISECONDS) : lock.lockExclusively(100L, TimeUnit.MILLISECONDS)) {
+ try {
+ winner.countDown();
+ loser.await();
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ loser.countDown();
+ winner.await();
+ }
+ } catch (InterruptedException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast-client.xml b/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast-client.xml
new file mode 100644
index 0000000..19f1cc0
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast-client.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<hazelcast-client xmlns="http://www.hazelcast.com/schema/client-config"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.hazelcast.com/schema/client-config
+ http://www.hazelcast.com/schema/client-config/hazelcast-client-config-4.1.xsd">
+
+ <cluster-name>maven-resolver-named</cluster-name>
+ <instance-name>client</instance-name>
+ <properties>
+ <property name="hazelcast.logging.type">slf4j</property>
+ </properties>
+ <network>
+ <cluster-members>
+ <address>localhost</address>
+ </cluster-members>
+ </network>
+</hazelcast-client>
diff --git a/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast.xml b/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast.xml
new file mode 100644
index 0000000..0c204f7
--- /dev/null
+++ b/maven-resolver-named-locks-hazelcast/src/test/resources/hazelcast.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<hazelcast xmlns="http://www.hazelcast.com/schema/config"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.hazelcast.com/schema/config
+ http://www.hazelcast.com/schema/config/hazelcast-config-4.1.xsd">
+
+ <cluster-name>maven-resolver-named</cluster-name>
+ <instance-name>server</instance-name>
+ <properties>
+ <property name="hazelcast.logging.type">slf4j</property>
+ </properties>
+ <network>
+ <join>
+ <multicast enabled="false"/>
+ <tcp-ip>
+ <interface>localhost</interface>
+ </tcp-ip>
+ </join>
+ </network>
+</hazelcast>
diff --git a/maven-resolver-synccontext-redisson/pom.xml b/maven-resolver-named-locks-redisson/pom.xml
similarity index 69%
rename from maven-resolver-synccontext-redisson/pom.xml
rename to maven-resolver-named-locks-redisson/pom.xml
index a12d1c4..0e303a3 100644
--- a/maven-resolver-synccontext-redisson/pom.xml
+++ b/maven-resolver-named-locks-redisson/pom.xml
@@ -28,51 +28,56 @@
<version>1.7.0-SNAPSHOT</version>
</parent>
- <artifactId>maven-resolver-synccontext-redisson</artifactId>
+ <artifactId>maven-resolver-named-locks-redisson</artifactId>
- <name>Maven Artifact Resolver Sync Context Redisson</name>
+ <name>Maven Artifact Resolver Named Locks using Redisson</name>
<description>
- A synchronization context implementation using Redisson distributed locks.
+ A synchronization utility implementation using Redisson.
</description>
<properties>
- <javaVersion>8</javaVersion>
- <Automatic-Module-Name>org.apache.maven.resolver.synccontext.redisson</Automatic-Module-Name>
+ <Automatic-Module-Name>org.apache.maven.resolver.named.redisson</Automatic-Module-Name>
<Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-api</artifactId>
+ <artifactId>maven-resolver-named-locks</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-util</artifactId>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-spi</artifactId>
+ <groupId>org.redisson</groupId>
+ <artifactId>redisson</artifactId>
+ <version>3.15.3</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
- <scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
- <groupId>javax.annotation</groupId>
- <artifactId>javax.annotation-api</artifactId>
- <version>1.3.2</version>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson</artifactId>
- <version>3.13.3</version>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
</dependency>
</dependencies>
@@ -97,4 +102,18 @@
</plugin>
</plugins>
</build>
+
+ <profiles>
+ <profile>
+ <id>it</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonNamedLockFactorySupport.java b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonNamedLockFactorySupport.java
new file mode 100644
index 0000000..d49c19e
--- /dev/null
+++ b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonNamedLockFactorySupport.java
@@ -0,0 +1,118 @@
+package org.eclipse.aether.named.redisson;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.support.NamedLockFactorySupport;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Support class for factories using {@link RedissonClient}.
+ */
+public abstract class RedissonNamedLockFactorySupport
+ extends NamedLockFactorySupport
+{
+ protected static final String NAME_PREFIX = "maven:resolver:";
+
+ private static final String DEFAULT_CONFIG_FILE_NAME = "maven-resolver-redisson.yaml";
+
+ private static final String DEFAULT_REDIS_ADDRESS = "redis://localhost:6379";
+
+ private static final String DEFAULT_CLIENT_NAME = "maven-resolver";
+
+ private static final String CONFIG_PROP_CONFIG_FILE = "aether.syncContext.named.redisson.configFile";
+
+ protected final RedissonClient redissonClient;
+
+ public RedissonNamedLockFactorySupport()
+ {
+ this.redissonClient = createRedissonClient();
+ }
+
+ @Override
+ public void shutdown()
+ {
+ logger.trace( "Shutting down Redisson client with id '{}'", redissonClient.getId() );
+ redissonClient.shutdown();
+ }
+
+ private RedissonClient createRedissonClient()
+ {
+ Path configFilePath = null;
+
+ String configFile = System.getProperty( CONFIG_PROP_CONFIG_FILE );
+ if ( configFile != null && !configFile.isEmpty() )
+ {
+ configFilePath = Paths.get( configFile );
+ if ( Files.notExists( configFilePath ) )
+ {
+ throw new IllegalArgumentException( "The specified Redisson config file does not exist: "
+ + configFilePath );
+ }
+ }
+
+ if ( configFilePath == null )
+ {
+ String mavenConf = System.getProperty( "maven.conf" );
+ if ( mavenConf != null && !mavenConf.isEmpty() )
+ {
+ configFilePath = Paths.get( mavenConf, DEFAULT_CONFIG_FILE_NAME );
+ if ( Files.notExists( configFilePath ) )
+ {
+ configFilePath = null;
+ }
+ }
+ }
+
+ Config config;
+
+ if ( configFilePath != null )
+ {
+ logger.trace( "Reading Redisson config file from '{}'", configFilePath );
+ try ( InputStream is = Files.newInputStream( configFilePath ) )
+ {
+ config = Config.fromYAML( is );
+ }
+ catch ( IOException e )
+ {
+ throw new IllegalStateException( "Failed to read Redisson config file: " + configFilePath, e );
+ }
+ }
+ else
+ {
+ config = new Config();
+ config.useSingleServer()
+ .setAddress( DEFAULT_REDIS_ADDRESS )
+ .setClientName( DEFAULT_CLIENT_NAME );
+ }
+
+ RedissonClient redissonClient = Redisson.create( config );
+ logger.trace( "Created Redisson client with id '{}'", redissonClient.getId() );
+
+ return redissonClient;
+ }
+}
diff --git a/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonReadWriteLockNamedLockFactory.java b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonReadWriteLockNamedLockFactory.java
new file mode 100644
index 0000000..089ecc9
--- /dev/null
+++ b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonReadWriteLockNamedLockFactory.java
@@ -0,0 +1,47 @@
+package org.eclipse.aether.named.redisson;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.support.ReadWriteLockNamedLock;
+import org.eclipse.aether.named.support.NamedLockSupport;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+/**
+ * Provider of {@link RedissonReadWriteLockNamedLockFactory} using Redisson and {@link org.redisson.api.RReadWriteLock}.
+ */
+@Singleton
+@Named( RedissonReadWriteLockNamedLockFactory.NAME )
+public class RedissonReadWriteLockNamedLockFactory
+ extends RedissonNamedLockFactorySupport
+{
+ public static final String NAME = "rwlock-redisson";
+
+ @Override
+ protected NamedLockSupport createLock( final String name )
+ {
+ return new ReadWriteLockNamedLock(
+ name,
+ this,
+ redissonClient.getReadWriteLock( NAME_PREFIX + name )
+ );
+ }
+}
diff --git a/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonSemaphoreNamedLockFactory.java b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonSemaphoreNamedLockFactory.java
new file mode 100644
index 0000000..bef84cc
--- /dev/null
+++ b/maven-resolver-named-locks-redisson/src/main/java/org/eclipse/aether/named/redisson/RedissonSemaphoreNamedLockFactory.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.named.redisson;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.support.AdaptedSemaphoreNamedLock;
+import org.eclipse.aether.named.support.NamedLockSupport;
+import org.redisson.api.RSemaphore;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provider of {@link RedissonSemaphoreNamedLockFactory} using Redisson and {@link org.redisson.api.RSemaphore}.
+ */
+@Singleton
+@Named( RedissonSemaphoreNamedLockFactory.NAME )
+public class RedissonSemaphoreNamedLockFactory
+ extends RedissonNamedLockFactorySupport
+{
+ public static final String NAME = "semaphore-redisson";
+
+ @Override
+ protected NamedLockSupport createLock( final String name )
+ {
+ return new AdaptedSemaphoreNamedLock(
+ name, this, new RedissonSemaphore( redissonClient.getSemaphore( NAME_PREFIX + name ) )
+ );
+ }
+
+ private static final class RedissonSemaphore implements AdaptedSemaphoreNamedLock.AdaptedSemaphore
+ {
+ private final RSemaphore semaphore;
+
+ private RedissonSemaphore( final RSemaphore semaphore )
+ {
+ semaphore.trySetPermits( Integer.MAX_VALUE );
+ this.semaphore = semaphore;
+ }
+
+ @Override
+ public boolean tryAcquire( final int perms, final long time, final TimeUnit unit )
+ throws InterruptedException
+ {
+ return semaphore.tryAcquire( perms, time, unit );
+ }
+
+ @Override
+ public void release( final int perms )
+ {
+ semaphore.release( perms );
+ }
+ }
+}
diff --git a/maven-resolver-named-locks-redisson/src/site/markdown/index.md.vm b/maven-resolver-named-locks-redisson/src/site/markdown/index.md.vm
new file mode 100644
index 0000000..3f649ec
--- /dev/null
+++ b/maven-resolver-named-locks-redisson/src/site/markdown/index.md.vm
@@ -0,0 +1,72 @@
+${esc.hash} Named Locks using Redisson
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+This module implement named locks using Redisson. It provides two implementations, that are distributed and rely on
+Redisson distributed objects feature.
+
+Out of the box "redisson" (distributed) named lock implementations are following:
+
+- `rwlock-redisson` implemented in `org.eclipse.aether.named.redisson.RedissonReadWriteLockNamedLockFactory`
+ that uses Redisson backed `org.redisson.api.RReadWriteLock`.
+- `semaphore-redisson` implemented in `org.eclipse.aether.named.redisson.RedissonSemaphoreNamedLockFactory`
+ that uses Redisson backed `org.redisson.api.RSemaphore`.
+
+${esc.hash}${esc.hash} Open Issues/Notes
+
+- It only works when Sisu DI is used and not the bundled `AetherModule` or
+ `ServiceLocator` (Maven uses Sisu dependency injection).
+- Usage from plugins has not been tested yet.
+- The `furnace-maven-plugin` does not work this implementation because it uses `ServiceLocator` instead
+ of dependency injection.
+
+${esc.hash}${esc.hash} Installation/Testing
+
+- Create the directory `${maven.home}/lib/ext/redisson/`.
+- Modify `${maven.home}/bin/m2.conf` by adding `load ${maven.home}/lib/ext/redisson/*.jar`
+ right after the `${maven.conf}/logging` line.
+- Copy the following dependencies from Maven Central to `${maven.home}/lib/ext/redisson/`:
+ <pre class="source">
+ ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.12.1/jackson-annotations-2.12.1.jar">jackson-annotations-2.12.1.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.12.1/jackson-core-2.12.1.jar">jackson-core-2.12.1.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.12.1/jackson-databind-2.12.1.jar">jackson-databind-2.12.1.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.12.1/jackson-dataformat-yaml-2.12.1.jar">jackson-dataformat-yaml-2.12.1.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-buffer/4.1.60.Final/netty-buffer-4.1.60.Final.jar">netty-buffer-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-codec-dns/4.1.60.Final/netty-codec-dns-4.1.60.Final.jar">netty-codec-dns-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-codec/4.1.60.Final/netty-codec-4.1.60.Final.jar">netty-codec-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-common/4.1.60.Final/netty-common-4.1.60.Final.jar">netty-common-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-handler/4.1.60.Final/netty-handler-4.1.60.Final.jar">netty-handler-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-resolver-dns/4.1.60.Final/netty-resolver-dns-4.1.60.Final.jar">netty-resolver-dns-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-resolver/4.1.60.Final/netty-resolver-4.1.60.Final.jar">netty-resolver-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-transport/4.1.60.Final/netty-transport-4.1.60.Final.jar">netty-transport-4.1.60.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/projectreactor/reactor-core/3.4.2/reactor-core-3.4.2.jar">reactor-core-3.4.2.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/io/reactivex/rxjava3/rxjava/3.0.10/rxjava-3.0.10.jar">rxjava-3.0.10.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/javax/cache/cache-api/1.0.0/cache-api-1.0.0.jar">cache-api-1.0.0.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy/1.10.14/byte-buddy-1.10.14.jar">byte-buddy-1.10.14.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/apache/maven/resolver/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar">${project.artifactId}-${project.version}.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/jboss/marshalling/jboss-marshalling-river/2.0.11.Final/jboss-marshalling-river-2.0.11.Final.jar">jboss-marshalling-river-2.0.11.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/jboss/marshalling/jboss-marshalling/2.0.11.Final/jboss-marshalling-2.0.11.Final.jar">jboss-marshalling-2.0.11.Final.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/jodd/jodd-bean/5.1.6/jodd-bean-5.1.6.jar">jodd-bean-5.1.6.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/jodd/jodd-core/5.1.6/jodd-core-5.1.6.jar">jodd-core-5.1.6.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/reactivestreams/reactive-streams/1.0.3/reactive-streams-1.0.3.jar">reactive-streams-1.0.3.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/redisson/redisson/3.15.3/redisson-3.15.3.jar">redisson-3.15.3.jar</a>
+ ├── <a href="https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.27/snakeyaml-1.27.jar">snakeyaml-1.27.jar</a></pre>
+- Start your Redis instance on `localhost` or configure a remote instance with `${maven.conf}/maven-resolver-redisson.yaml`.
+- Now start a multithreaded Maven build on your project and make sure `NamedSyncContextFactory` is being used.
diff --git a/maven-resolver-synccontext-redisson/src/site/site.xml b/maven-resolver-named-locks-redisson/src/site/site.xml
similarity index 97%
rename from maven-resolver-synccontext-redisson/src/site/site.xml
rename to maven-resolver-named-locks-redisson/src/site/site.xml
index 0dd2b23..99b3bbd 100644
--- a/maven-resolver-synccontext-redisson/src/site/site.xml
+++ b/maven-resolver-named-locks-redisson/src/site/site.xml
@@ -22,7 +22,7 @@ under the License.
<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
- name="RedissonSyncContext">
+ name="Named Locks using Redisson">
<body>
<menu name="Overview">
<item name="Introduction" href="index.html"/>
diff --git a/maven-resolver-synccontext-global/pom.xml b/maven-resolver-named-locks/pom.xml
similarity index 75%
rename from maven-resolver-synccontext-global/pom.xml
rename to maven-resolver-named-locks/pom.xml
index 6362b9b..da7f4d2 100644
--- a/maven-resolver-synccontext-global/pom.xml
+++ b/maven-resolver-named-locks/pom.xml
@@ -28,46 +28,52 @@
<version>1.7.0-SNAPSHOT</version>
</parent>
- <artifactId>maven-resolver-synccontext-global</artifactId>
+ <artifactId>maven-resolver-named-locks</artifactId>
- <name>Maven Artifact Resolver Sync Context Global</name>
+ <name>Maven Artifact Resolver Named Locks</name>
<description>
- A synchronization context implementation using a global lock.
+ A synchronization utility implementation using Named locks.
</description>
<properties>
- <javaVersion>8</javaVersion>
- <Automatic-Module-Name>org.apache.maven.resolver.synccontext.global</Automatic-Module-Name>
+ <Automatic-Module-Name>org.apache.maven.resolver.named</Automatic-Module-Name>
<Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
</properties>
<dependencies>
<dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-api</artifactId>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-util</artifactId>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <optional>true</optional>
</dependency>
<dependency>
- <groupId>org.apache.maven.resolver</groupId>
- <artifactId>maven-resolver-spi</artifactId>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>javax.annotation</groupId>
- <artifactId>javax.annotation-api</artifactId>
- <version>1.3.2</version>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
</dependency>
</dependencies>
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLock.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLock.java
new file mode 100644
index 0000000..81ebe5e
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLock.java
@@ -0,0 +1,75 @@
+package org.eclipse.aether.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A named lock, functionally similar to existing JVM and other implementations. Must support boxing (reentrancy), but
+ * no lock upgrade is supported. The lock instance obtained from lock factory must be treated as a resource, best in
+ * try-with-resource block. Usual pattern to use this lock:
+ * <pre>
+ * try (NamedLock lock = factory.getLock("resourceName")) {
+ * if (lock.lockExclusively(10L, Timeunit.SECONDS)) {
+ * try {
+ * ... exclusive access to "resourceName" resource gained here
+ * }
+ * finally {
+ * lock.unlock();
+ * }
+ * }
+ * else {
+ * ... failed to gain access within specified time, handle it
+ * }
+ * }
+ * </pre>
+ */
+public interface NamedLock extends AutoCloseable
+{
+ /**
+ * Returns this instance name, never null
+ */
+ String name();
+
+ /**
+ * Tries to lock shared, may block for given time. If successful, returns {@code true}.
+ */
+ boolean lockShared( long time, TimeUnit unit ) throws InterruptedException;
+
+ /**
+ * Tries to lock exclusively, may block for given time. If successful, returns {@code true}.
+ */
+ boolean lockExclusively( long time, TimeUnit unit ) throws InterruptedException;
+
+ /**
+ * Unlocks the lock, must be invoked by caller after one of the {@link #lockShared(long, TimeUnit)} or {@link
+ * #lockExclusively(long, TimeUnit)}.
+ */
+ void unlock();
+
+ /**
+ * Closes the lock resource. Lock MUST be unlocked using {@link #unlock()} in case any locking happened on it. After
+ * invoking this method, the lock instance MUST NOT be used anymore. If lock for same name needed, a new instance
+ * should be obtained from factory using {@link NamedLockFactory#getLock(String)}. Ideally, instances are to be used
+ * within try-with-resource blocks, so calling this method directly is not really needed, nor advised.
+ */
+ @Override
+ void close();
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLockFactory.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLockFactory.java
new file mode 100644
index 0000000..5b74909
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/NamedLockFactory.java
@@ -0,0 +1,40 @@
+package org.eclipse.aether.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A factory of {@link NamedLock}s.
+ */
+public interface NamedLockFactory
+{
+ /**
+ * Creates or reuses existing {@link NamedLock}. Returns instance MUST BE treated as "resource", best in
+ * try-with-resource block.
+ *
+ * @param name the lock name, must not be {@code null}.
+ * @return named lock instance, never {@code null}.
+ */
+ NamedLock getLock( String name );
+
+ /**
+ * Performs a clean shut down of the factory.
+ */
+ void shutdown();
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalReadWriteLockNamedLockFactory.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalReadWriteLockNamedLockFactory.java
new file mode 100644
index 0000000..316a8d2
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalReadWriteLockNamedLockFactory.java
@@ -0,0 +1,49 @@
+package org.eclipse.aether.named.providers;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.named.support.ReadWriteLockNamedLock;
+import org.eclipse.aether.named.support.NamedLockFactorySupport;
+
+/**
+ * A JVM-local named lock factory that uses named {@link ReentrantReadWriteLock}s.
+ */
+@Singleton
+@Named( LocalReadWriteLockNamedLockFactory.NAME )
+public class LocalReadWriteLockNamedLockFactory
+ extends NamedLockFactorySupport
+{
+ public static final String NAME = "rwlock-local";
+
+ @Override
+ protected ReadWriteLockNamedLock createLock( final String name )
+ {
+ return new ReadWriteLockNamedLock(
+ name,
+ this,
+ new ReentrantReadWriteLock()
+ );
+ }
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalSemaphoreNamedLockFactory.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalSemaphoreNamedLockFactory.java
new file mode 100644
index 0000000..464a432
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/LocalSemaphoreNamedLockFactory.java
@@ -0,0 +1,72 @@
+package org.eclipse.aether.named.providers;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.named.support.AdaptedSemaphoreNamedLock;
+import org.eclipse.aether.named.support.NamedLockFactorySupport;
+
+/**
+ * A JVM-local named lock factory that uses named {@link Semaphore}s.
+ */
+@Singleton
+@Named( LocalSemaphoreNamedLockFactory.NAME )
+public class LocalSemaphoreNamedLockFactory
+ extends NamedLockFactorySupport
+{
+ public static final String NAME = "semaphore-local";
+
+ @Override
+ protected AdaptedSemaphoreNamedLock createLock( final String name )
+ {
+ return new AdaptedSemaphoreNamedLock( name, this, new JVMSemaphore() );
+ }
+
+ /**
+ * Adapted JVM {@link java.util.concurrent.Semaphore}.
+ */
+ private static final class JVMSemaphore
+ implements AdaptedSemaphoreNamedLock.AdaptedSemaphore
+ {
+ private final Semaphore semaphore;
+
+ private JVMSemaphore()
+ {
+ this.semaphore = new Semaphore( Integer.MAX_VALUE );
+ }
+
+ @Override
+ public boolean tryAcquire( final int perms, final long time, final TimeUnit unit ) throws InterruptedException
+ {
+ return semaphore.tryAcquire( perms, time, unit );
+ }
+
+ @Override
+ public void release( final int perms )
+ {
+ semaphore.release( perms );
+ }
+ }
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/AdaptedSemaphoreNamedLock.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/AdaptedSemaphoreNamedLock.java
new file mode 100644
index 0000000..70a4b1b
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/AdaptedSemaphoreNamedLock.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether.named.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Named lock support implementation that is using "adapted" semaphore (to be able to use semaphores not sharing common
+ * API).
+ */
+public class AdaptedSemaphoreNamedLock extends NamedLockSupport
+{
+ /**
+ * Wrapper for semaphore-like stuff, that do not share common ancestor. Semaphore must be created to support {@link
+ * Integer#MAX_VALUE} permissions.
+ */
+ public interface AdaptedSemaphore
+ {
+ boolean tryAcquire( int perms, long time, TimeUnit unit ) throws InterruptedException;
+
+ void release( int perms );
+ }
+
+ /**
+ * Count of permissions involved with "nop" locking. When required lock step is preceded with a step that already
+ * fulfils currently requested locking, no locking is needed. In other words, caller already possesses the access to
+ * lock protected resource. The "nop" locking is used to track proper "boxing" of lock/unlock calls.
+ */
+ private static final int NONE = 0;
+
+ /**
+ * Count of permissions involved with shared locking
+ */
+ private static final int SHARED = 1;
+
+ /**
+ * Count of permissions involved with exclusive locking
+ */
+ private static final int EXCLUSIVE = Integer.MAX_VALUE;
+
+ private final ThreadLocal<Deque<Integer>> threadPerms;
+
+ private final AdaptedSemaphore semaphore;
+
+ public AdaptedSemaphoreNamedLock( final String name, final NamedLockFactorySupport factory,
+ final AdaptedSemaphore semaphore )
+ {
+ super( name, factory );
+ this.threadPerms = ThreadLocal.withInitial( ArrayDeque::new );
+ this.semaphore = semaphore;
+ }
+
+ @Override
+ public boolean lockShared( final long time, final TimeUnit unit ) throws InterruptedException
+ {
+ Deque<Integer> perms = threadPerms.get();
+ if ( !perms.isEmpty() )
+ { // we already own shared or exclusive lock
+ perms.push( NONE );
+ return true;
+ }
+ if ( semaphore.tryAcquire( SHARED, time, unit ) )
+ {
+ perms.push( SHARED );
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean lockExclusively( final long time, final TimeUnit unit ) throws InterruptedException
+ {
+ Deque<Integer> perms = threadPerms.get();
+ if ( !perms.isEmpty() )
+ { // we already own shared or exclusive lock
+ if ( perms.contains( EXCLUSIVE ) )
+ {
+ perms.push( NONE );
+ return true;
+ }
+ else
+ {
+ return false; // Lock upgrade not supported
+ }
+ }
+ if ( semaphore.tryAcquire( EXCLUSIVE, time, unit ) )
+ {
+ perms.push( EXCLUSIVE );
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void unlock()
+ {
+ Deque<Integer> steps = threadPerms.get();
+ if ( steps.isEmpty() )
+ {
+ throw new IllegalStateException( "Wrong API usage: unlock w/o lock" );
+ }
+ int step = steps.pop();
+ if ( step > 0 )
+ {
+ semaphore.release( step );
+ }
+ }
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
new file mode 100644
index 0000000..ef5a402
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
@@ -0,0 +1,134 @@
+package org.eclipse.aether.named.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.NamedLockFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Support class for {@link NamedLockFactory} implementations providing reference counting.
+ */
+public abstract class NamedLockFactorySupport implements NamedLockFactory
+{
+ protected final Logger logger = LoggerFactory.getLogger( getClass() );
+
+ private final ConcurrentMap<String, NamedLockHolder> locks;
+
+ public NamedLockFactorySupport()
+ {
+ this.locks = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public NamedLockSupport getLock( final String name )
+ {
+ return locks.compute( name, ( k, v ) ->
+ {
+ if ( v == null )
+ {
+ v = new NamedLockHolder( createLock( k ) );
+ }
+ v.incRef();
+ return v;
+ } ).namedLock;
+ }
+
+ @Override
+ public void shutdown()
+ {
+ // override if needed
+ }
+
+ public boolean closeLock( final NamedLockSupport lock )
+ {
+ AtomicBoolean destroyed = new AtomicBoolean( false );
+ locks.compute( lock.name(), ( k, v ) ->
+ {
+ if ( v != null && v.decRef() == 0 )
+ {
+ destroyLock( v.namedLock );
+ destroyed.set( true );
+ return null;
+ }
+ return v;
+ } );
+ return destroyed.get();
+ }
+
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ try
+ {
+ if ( !locks.isEmpty() )
+ {
+ // report leak
+ logger.warn( "Lock leak, referenced locks still exist {}", locks );
+ }
+ }
+ finally
+ {
+ super.finalize();
+ }
+ }
+
+ protected abstract NamedLockSupport createLock( final String name );
+
+ protected void destroyLock( final NamedLockSupport lock )
+ {
+ // override if needed
+ }
+
+ private static final class NamedLockHolder
+ {
+ private final NamedLockSupport namedLock;
+
+ private final AtomicInteger referenceCount;
+
+ private NamedLockHolder( NamedLockSupport namedLock )
+ {
+ this.namedLock = namedLock;
+ this.referenceCount = new AtomicInteger( 0 );
+ }
+
+ private int incRef()
+ {
+ return referenceCount.incrementAndGet();
+ }
+
+ private int decRef()
+ {
+ return referenceCount.decrementAndGet();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[refCount=" + referenceCount.get() + ", lock=" + namedLock + "]";
+ }
+ }
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockSupport.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockSupport.java
new file mode 100644
index 0000000..02f9960
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockSupport.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.named.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.NamedLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Support class for {@link NamedLock} implementations providing reference counting.
+ */
+public abstract class NamedLockSupport implements NamedLock
+{
+ protected final Logger logger = LoggerFactory.getLogger( getClass() );
+
+ private final String name;
+
+ private final NamedLockFactorySupport factory;
+
+ public NamedLockSupport( final String name, final NamedLockFactorySupport factory )
+ {
+ this.name = name;
+ this.factory = factory;
+ }
+
+ @Override
+ public String name()
+ {
+ return name;
+ }
+
+ @Override
+ public void close()
+ {
+ factory.closeLock( this );
+ }
+}
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/ReadWriteLockNamedLock.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/ReadWriteLockNamedLock.java
new file mode 100644
index 0000000..6344294
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/ReadWriteLockNamedLock.java
@@ -0,0 +1,108 @@
+package org.eclipse.aether.named.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReadWriteLock;
+
+/**
+ * Named lock support implementation that is using {@link ReadWriteLock} instances. The adapted lock MUST SUPPORT
+ * reentrancy, non re-entrant locks will NOT work. It is the responsibility of an adapting lock, to ensure that
+ * above lock requirement stands.
+ */
+public class ReadWriteLockNamedLock extends NamedLockSupport
+{
+ private enum Step
+ {
+ /**
+ * Step when {@link ReadWriteLock#readLock()} was locked
+ */
+ SHARED,
+
+ /**
+ * Step when {@link ReadWriteLock#writeLock()} was locked
+ */
+ EXCLUSIVE
+ }
+
+ private final ThreadLocal<Deque<Step>> threadSteps;
+
+ private final ReadWriteLock readWriteLock;
+
+ public ReadWriteLockNamedLock( final String name, final NamedLockFactorySupport factory,
+ final ReadWriteLock readWriteLock )
+ {
+ super( name, factory );
+ this.threadSteps = ThreadLocal.withInitial( ArrayDeque::new );
+ this.readWriteLock = readWriteLock;
+ }
+
+ @Override
+ public boolean lockShared( final long time, final TimeUnit unit ) throws InterruptedException
+ {
+ Deque<Step> steps = threadSteps.get();
+ if ( readWriteLock.readLock().tryLock( time, unit ) )
+ {
+ steps.push( Step.SHARED );
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean lockExclusively( final long time, final TimeUnit unit ) throws InterruptedException
+ {
+ Deque<Step> steps = threadSteps.get();
+ if ( !steps.isEmpty() )
+ { // we already own shared or exclusive lock
+ if ( !steps.contains( Step.EXCLUSIVE ) )
+ {
+ return false; // Lock upgrade not supported
+ }
+ }
+ if ( readWriteLock.writeLock().tryLock( time, unit ) )
+ {
+ steps.push( Step.EXCLUSIVE );
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void unlock()
+ {
+ Deque<Step> steps = threadSteps.get();
+ if ( steps.isEmpty() )
+ {
+ throw new IllegalStateException( "Wrong API usage: unlock w/o lock" );
+ }
+ Step step = steps.pop();
+ if ( Step.SHARED == step )
+ {
+ readWriteLock.readLock().unlock();
+ }
+ else if ( Step.EXCLUSIVE == step )
+ {
+ readWriteLock.writeLock().unlock();
+ }
+ }
+}
diff --git a/maven-resolver-named-locks/src/site/markdown/index.md.vm b/maven-resolver-named-locks/src/site/markdown/index.md.vm
new file mode 100644
index 0000000..b8d47d8
--- /dev/null
+++ b/maven-resolver-named-locks/src/site/markdown/index.md.vm
@@ -0,0 +1,33 @@
+${esc.hash} Named Locks
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+Named locks are essentially locks that are assigned to some given (opaque) ID. If you work with multiple
+resources that each can have unique ID assigned (file w/ file an absolute path, some entities with unique ID),
+then you can use named locks to make sure they are being protected from concurrent read and write actions.
+
+Named locks provide support classes for implementations, and provide out of the box two named lock implementation.
+
+Out of the box "local" (local to JVM) named lock implementations are following:
+
+- `rwlock-local` implemented in `org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory` that uses
+ JVM `java.util.concurrent.locks.ReentrantReadWriteLock`.
+- `semaphore-local` implemented in `org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory` that uses
+ JVM `java.util.concurrent.Semaphore`.
diff --git a/maven-resolver-synccontext-global/src/site/site.xml b/maven-resolver-named-locks/src/site/site.xml
similarity index 97%
rename from maven-resolver-synccontext-global/src/site/site.xml
rename to maven-resolver-named-locks/src/site/site.xml
index 8183b5d..efed113 100644
--- a/maven-resolver-synccontext-global/src/site/site.xml
+++ b/maven-resolver-named-locks/src/site/site.xml
@@ -22,7 +22,7 @@ under the License.
<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
- name="GlobalSyncContext">
+ name="Named Locks">
<body>
<menu name="Overview">
<item name="Introduction" href="index.html"/>
diff --git a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalReadWriteLockNamedLockFactoryTest.java b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalReadWriteLockNamedLockFactoryTest.java
new file mode 100644
index 0000000..3fb6ca2
--- /dev/null
+++ b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalReadWriteLockNamedLockFactoryTest.java
@@ -0,0 +1,32 @@
+package org.eclipse.aether.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class LocalReadWriteLockNamedLockFactoryTest
+ extends NamedLockFactoryTestSupport {
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ namedLockFactory = new LocalReadWriteLockNamedLockFactory();
+ }
+}
diff --git a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalSemaphoreNamedLockFactoryTest.java b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalSemaphoreNamedLockFactoryTest.java
new file mode 100644
index 0000000..df8c247
--- /dev/null
+++ b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/LocalSemaphoreNamedLockFactoryTest.java
@@ -0,0 +1,32 @@
+package org.eclipse.aether.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class LocalSemaphoreNamedLockFactoryTest
+ extends NamedLockFactoryTestSupport {
+
+ @BeforeClass
+ public static void createNamedLockFactory() {
+ namedLockFactory = new LocalSemaphoreNamedLockFactory();
+ }
+}
diff --git a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/NamedLockFactoryTestSupport.java b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/NamedLockFactoryTestSupport.java
new file mode 100644
index 0000000..9141039
--- /dev/null
+++ b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/NamedLockFactoryTestSupport.java
@@ -0,0 +1,199 @@
+package org.eclipse.aether.named;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+
+/**
+ * UT support for {@link NamedLockFactory}.
+ */
+public abstract class NamedLockFactoryTestSupport {
+
+ protected static NamedLockFactory namedLockFactory;
+
+ @Rule
+ public TestName testName = new TestName();
+
+ protected String lockName()
+ {
+ return testName.getMethodName();
+ }
+
+ @Test
+ public void refCounting() {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name);
+ NamedLock two = namedLockFactory.getLock(name)) {
+ assertThat(one, sameInstance(two));
+ one.close();
+ two.close();
+
+ try (NamedLock three = namedLockFactory.getLock(name)) {
+ assertThat(three, not(sameInstance(two)));
+ }
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void unlockWoLock() {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void wwBoxing() throws InterruptedException {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void rrBoxing() throws InterruptedException {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void wrBoxing() throws InterruptedException {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ one.unlock();
+ one.unlock();
+ }
+ }
+
+ @Test
+ public void rwBoxing() throws InterruptedException {
+ final String name = lockName();
+ try (NamedLock one = namedLockFactory.getLock(name)) {
+ assertThat(one.lockShared(1L, TimeUnit.MILLISECONDS), is(true));
+ assertThat(one.lockExclusively(1L, TimeUnit.MILLISECONDS), is(false));
+ one.unlock();
+ }
+ }
+
+ @Test(timeout = 5000)
+ public void sharedAccess() throws InterruptedException {
+ final String name = lockName();
+ CountDownLatch winners = new CountDownLatch(2); // we expect 2 winners
+ CountDownLatch losers = new CountDownLatch(0); // we expect 0 losers
+ Thread t1 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void exclusiveAccess() throws InterruptedException {
+ final String name = lockName();
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ @Test(timeout = 5000)
+ public void mixedAccess() throws InterruptedException {
+ final String name = lockName();
+ CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+ CountDownLatch losers = new CountDownLatch(1); // we expect 1 loser
+ Thread t1 = new Thread(new Access(namedLockFactory, name, true, winners, losers));
+ Thread t2 = new Thread(new Access(namedLockFactory, name, false, winners, losers));
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+ winners.await();
+ losers.await();
+ }
+
+ private static class Access implements Runnable {
+ final NamedLockFactory namedLockFactory;
+ final String name;
+ final boolean shared;
+ final CountDownLatch winner;
+ final CountDownLatch loser;
+
+ public Access(NamedLockFactory namedLockFactory,
+ String name,
+ boolean shared,
+ CountDownLatch winner,
+ CountDownLatch loser) {
+ this.namedLockFactory = namedLockFactory;
+ this.name = name;
+ this.shared = shared;
+ this.winner = winner;
+ this.loser = loser;
+ }
+
+ @Override
+ public void run() {
+ try (NamedLock lock = namedLockFactory.getLock(name)) {
+ if (shared ? lock.lockShared(100L, TimeUnit.MILLISECONDS) : lock.lockExclusively(100L, TimeUnit.MILLISECONDS)) {
+ try {
+ winner.countDown();
+ loser.await();
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ loser.countDown();
+ winner.await();
+ }
+ } catch (InterruptedException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+ }
+}
diff --git a/maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
deleted file mode 100644
index ffc200f..0000000
--- a/maven-resolver-synccontext-global/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package org.eclipse.aether.internal.impl;
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import org.eclipse.aether.SyncContext;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.Map;
-import java.util.Properties;
-
-/**
- * Manages access to a properties file. This override drops internal synchronization becauses it
- * relies on external synchronization provided by outer {@link SyncContext} instances.
- */
-class TrackingFileManager
-{
-
- private static final Logger LOGGER = LoggerFactory.getLogger( TrackingFileManager.class );
-
- public Properties read( File file )
- {
- FileInputStream stream = null;
- try
- {
- if ( !file.exists() )
- {
- return null;
- }
-
- stream = new FileInputStream( file );
-
- Properties props = new Properties();
- props.load( stream );
-
- return props;
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Failed to read tracking file {}", file, e );
- }
- finally
- {
- close( stream, file );
- }
-
- return null;
- }
-
- public Properties update( File file, Map<String, String> updates )
- {
- Properties props = new Properties();
-
- File directory = file.getParentFile();
- if ( !directory.mkdirs() && !directory.exists() )
- {
- LOGGER.warn( "Failed to create parent directories for tracking file {}", file );
- return props;
- }
-
- RandomAccessFile raf = null;
- try
- {
- raf = new RandomAccessFile( file, "rw" );
-
- if ( file.canRead() )
- {
- byte[] buffer = new byte[(int) raf.length()];
-
- raf.readFully( buffer );
-
- ByteArrayInputStream stream = new ByteArrayInputStream( buffer );
-
- props.load( stream );
- }
-
- for ( Map.Entry<String, String> update : updates.entrySet() )
- {
- if ( update.getValue() == null )
- {
- props.remove( update.getKey() );
- }
- else
- {
- props.setProperty( update.getKey(), update.getValue() );
- }
- }
-
- ByteArrayOutputStream stream = new ByteArrayOutputStream( 1024 * 2 );
-
- LOGGER.debug( "Writing tracking file {}", file );
- props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
- + ", its format can be changed without prior notice." );
-
- raf.seek( 0 );
- raf.write( stream.toByteArray() );
- raf.setLength( raf.getFilePointer() );
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Failed to write tracking file {}", file, e );
- }
- finally
- {
- close( raf, file );
- }
-
- return props;
- }
-
- private void close( Closeable closeable, File file )
- {
- if ( closeable != null )
- {
- try
- {
- closeable.close();
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Error closing tracking file {}", file, e );
- }
- }
- }
-
-}
diff --git a/maven-resolver-synccontext-global/src/site/markdown/index.md.vm b/maven-resolver-synccontext-global/src/site/markdown/index.md.vm
deleted file mode 100644
index 5eba55a..0000000
--- a/maven-resolver-synccontext-global/src/site/markdown/index.md.vm
+++ /dev/null
@@ -1,57 +0,0 @@
-${esc.hash} Global Sync Context for Maven Resolver
-
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<span style="color: red; font-size: 16pt">***Note***: *This component is still considered to be experimental, use with caution!*</span>
-
-The Global Sync Context Factory is Java global lock factory for Maven Resolver to provide a
-concurrent-safe access from a single Maven instance to the same local Maven repository.
-
-For further details about the factory read the [Javadoc](./apidocs/org/eclipse/aether/synccontext/GlobalSyncContextFactory.html).
-
-${esc.hash}${esc.hash} Open Issues/Notes
-
-- It only works when dependency injection is used and not the bundled `AetherModule` or
- `ServiceLocator` (Maven uses dependency injection).
-- Usage from plugins has not been tested yet.
-- The `furnace-maven-plugin` does not work this implementation because it uses `ServiceLocator` instead
- of dependency injection.
-
-${esc.hash}${esc.hash} Installation/Testing
-- Copy the following dependencies from Maven Central to `${maven.home}/lib/ext/`:
- <pre class="source">
- ├── <a href="https://repo1.maven.org/maven2/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar">javax.annotation-api-1.3.2.jar</a>
- └── <a href="https://repo1.maven.org/maven2/org/apache/maven/resolver/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar">${project.artifactId}-${project.version}.jar</a></pre>
-- Now start a multithreaded Maven build on your project.
-
-${esc.hash}${esc.hash}${esc.hash} Debugging
-
-- Add/modify the following entries in `${maven.conf}/logging/simplelogger.properties`:
- ```
- org.slf4j.simpleLogger.showDateTime=true
- org.slf4j.simpleLogger.showThreadName=true
- org.slf4j.simpleLogger.showShortLogName=true
- org.slf4j.simpleLogger.log.org.eclipse.aether.synccontext=trace
- ```
-- Now start a multithreaded Maven build on your project and you should see at least these lines:
- ```
- 4626 [main] [TRACE] GlobalSyncContextFactory$GlobalSyncContext - Acquiring global...
- 35318 [main] [TRACE] GlobalSyncContextFactory$GlobalSyncContext - Releasing global...
- ```
diff --git a/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
deleted file mode 100644
index ffc200f..0000000
--- a/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package org.eclipse.aether.internal.impl;
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import org.eclipse.aether.SyncContext;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.Map;
-import java.util.Properties;
-
-/**
- * Manages access to a properties file. This override drops internal synchronization becauses it
- * relies on external synchronization provided by outer {@link SyncContext} instances.
- */
-class TrackingFileManager
-{
-
- private static final Logger LOGGER = LoggerFactory.getLogger( TrackingFileManager.class );
-
- public Properties read( File file )
- {
- FileInputStream stream = null;
- try
- {
- if ( !file.exists() )
- {
- return null;
- }
-
- stream = new FileInputStream( file );
-
- Properties props = new Properties();
- props.load( stream );
-
- return props;
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Failed to read tracking file {}", file, e );
- }
- finally
- {
- close( stream, file );
- }
-
- return null;
- }
-
- public Properties update( File file, Map<String, String> updates )
- {
- Properties props = new Properties();
-
- File directory = file.getParentFile();
- if ( !directory.mkdirs() && !directory.exists() )
- {
- LOGGER.warn( "Failed to create parent directories for tracking file {}", file );
- return props;
- }
-
- RandomAccessFile raf = null;
- try
- {
- raf = new RandomAccessFile( file, "rw" );
-
- if ( file.canRead() )
- {
- byte[] buffer = new byte[(int) raf.length()];
-
- raf.readFully( buffer );
-
- ByteArrayInputStream stream = new ByteArrayInputStream( buffer );
-
- props.load( stream );
- }
-
- for ( Map.Entry<String, String> update : updates.entrySet() )
- {
- if ( update.getValue() == null )
- {
- props.remove( update.getKey() );
- }
- else
- {
- props.setProperty( update.getKey(), update.getValue() );
- }
- }
-
- ByteArrayOutputStream stream = new ByteArrayOutputStream( 1024 * 2 );
-
- LOGGER.debug( "Writing tracking file {}", file );
- props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
- + ", its format can be changed without prior notice." );
-
- raf.seek( 0 );
- raf.write( stream.toByteArray() );
- raf.setLength( raf.getFilePointer() );
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Failed to write tracking file {}", file, e );
- }
- finally
- {
- close( raf, file );
- }
-
- return props;
- }
-
- private void close( Closeable closeable, File file )
- {
- if ( closeable != null )
- {
- try
- {
- closeable.close();
- }
- catch ( IOException e )
- {
- LOGGER.warn( "Error closing tracking file {}", file, e );
- }
- }
- }
-
-}
diff --git a/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/synccontext/RedissonSyncContextFactory.java b/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/synccontext/RedissonSyncContextFactory.java
deleted file mode 100644
index aae7ace..0000000
--- a/maven-resolver-synccontext-redisson/src/main/java/org/eclipse/aether/synccontext/RedissonSyncContextFactory.java
+++ /dev/null
@@ -1,390 +0,0 @@
-package org.eclipse.aether.synccontext;
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.TreeSet;
-
-import javax.annotation.PreDestroy;
-import javax.annotation.Priority;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.eclipse.aether.RepositorySystemSession;
-import org.eclipse.aether.SyncContext;
-import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.spi.synccontext.SyncContextFactory;
-import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.util.ChecksumUtils;
-import org.eclipse.aether.util.ConfigUtils;
-import org.redisson.Redisson;
-import org.redisson.api.RLock;
-import org.redisson.api.RReadWriteLock;
-import org.redisson.api.RedissonClient;
-import org.redisson.config.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A singleton factory to create synchronization contexts using Redisson's {@link RReadWriteLock}.
- * It locks fine-grained with groupId, artifactId and version if required.
- * <p>
- * <strong>Note: This component is still considered to be experimental, use with caution!</strong>
- * <h2>Configuration</h2>
- * You can configure various aspects of this factory.
- *
- * <h3>Redisson Client</h3>
- * To fully configure the Redisson client, this factory uses the following staggered approach:
- * <ol>
- * <li>If the property {@code aether.syncContext.redisson.configFile} is set and the file at that
- * specific path does exist, load it otherwise an exception is thrown.</li>
- * <li>If no configuration file path is provided, load default from
- * <code>${maven.conf}/maven-resolver-redisson.yaml</code>, but ignore if it does not exist.</li>
- * <li>If no configuration file is available at all, Redisson is configured with a single server pointing
- * to {@code redis://localhost:6379} with client name {@code maven-resolver}.</li>
- * </ol>
- * Please note that an invalid confguration file results in an exception too.
- *
- * <h3>Discrimination</h3>
- * You may freely use a single Redis instance to serve multiple Maven instances, on multiple hosts
- * with shared or exclusive local repositories. Every sync context instance will generate a unique
- * discriminator which identifies each host paired with the local repository currently accessed.
- * The following staggered approach is used:
- * <ol>
- * <li>Determine hostname, if not possible use {@code localhost}.</li>
- * <li>If the property {@code aether.syncContext.redisson.discriminator} is set, use it and skip
- * the remaining steps.</li>
- * <li>Concat hostname with the path of the local repository: <code>${hostname}:${maven.repo.local}</code>.</li>
- * <li>Calculate the SHA-1 digest of this value. If that fails use the static digest of an empty string.</li>
- * </ol>
- *
- * <h2>Key Composition</h2>
- * Each lock is assigned a unique key in the configured Redis instance which has the following pattern:
- * <code>maven:resolver:${discriminator}:${artifact|metadata}</code>.
- * <ul>
- * <li><code>${artifact}</code> will
- * always resolve to <code>artifact:${groupId}:${artifactId}:${baseVersion}</code>.</li>
- * <li><code>${metadata}</code> will resolve to one of <code>metadata:${groupId}:${artifactId}:${version}</code>,
- * <code>metadata:${groupId}:${artifactId}</code>, <code>metadata:${groupId}</code>,
- * <code>metadata:</code>.</li>
- * </ul>
- */
-@Named
-@Priority( Integer.MAX_VALUE )
-@Singleton
-public class RedissonSyncContextFactory
- implements SyncContextFactory
-{
-
- private static final String DEFAULT_CONFIG_FILE_NAME = "maven-resolver-redisson.yaml";
- private static final String DEFAULT_REDIS_ADDRESS = "redis://localhost:6379";
- private static final String DEFAULT_CLIENT_NAME = "maven-resolver";
- private static final String DEFAULT_HOSTNAME = "localhost";
- private static final String DEFAULT_DISCRIMINATOR_DIGEST = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
-
- private static final String CONFIG_PROP_CONFIG_FILE = "aether.syncContext.redisson.configFile";
-
- private static final Logger LOGGER = LoggerFactory.getLogger( RedissonSyncContextFactory.class );
-
- // We are in a singleton so these should exist only once!
- private RedissonClient redissonClient;
- private String hostname;
-
- public RedissonSyncContextFactory()
- {
- // TODO These two log statements will go away
- LOGGER.trace( "TCCL: {}", Thread.currentThread().getContextClassLoader() );
- LOGGER.trace( "CCL: {}", getClass().getClassLoader() );
- this.redissonClient = createRedissonClient();
- this.hostname = getHostname();
- }
-
- private RedissonClient createRedissonClient()
- {
- Path configFilePath = null;
-
- String configFile = ConfigUtils.getString( System.getProperties(), null, CONFIG_PROP_CONFIG_FILE );
- if ( configFile != null && !configFile.isEmpty() )
- {
- configFilePath = Paths.get( configFile );
- if ( Files.notExists( configFilePath ) )
- {
- throw new IllegalArgumentException( "The specified Redisson config file does not exist: "
- + configFilePath );
- }
- }
-
- if ( configFilePath == null )
- {
- String mavenConf = ConfigUtils.getString( System.getProperties(), null, "maven.conf" );
- if ( mavenConf != null && !mavenConf.isEmpty() )
- {
- configFilePath = Paths.get( mavenConf, DEFAULT_CONFIG_FILE_NAME );
- if ( Files.notExists( configFilePath ) )
- {
- configFilePath = null;
- }
- }
- }
-
- Config config = null;
-
- if ( configFilePath != null )
- {
- LOGGER.trace( "Reading Redisson config file from '{}'", configFilePath );
- try ( InputStream is = Files.newInputStream( configFilePath ) )
- {
- config = Config.fromYAML( is );
- }
- catch ( IOException e )
- {
- throw new IllegalStateException( "Failed to read Redisson config file: " + configFilePath, e );
- }
- }
- else
- {
- config = new Config();
- config.useSingleServer()
- .setAddress( DEFAULT_REDIS_ADDRESS )
- .setClientName( DEFAULT_CLIENT_NAME );
- }
-
- RedissonClient redissonClient = Redisson.create( config );
- LOGGER.trace( "Created Redisson client with id '{}'", redissonClient.getId() );
-
- return redissonClient;
- }
-
- private String getHostname()
- {
- try
- {
- return InetAddress.getLocalHost().getHostName();
- }
- catch ( UnknownHostException e )
- {
- LOGGER.warn( "Failed to get hostname, using '{}'",
- DEFAULT_HOSTNAME, e );
- return DEFAULT_HOSTNAME;
- }
- }
-
- public SyncContext newInstance( RepositorySystemSession session, boolean shared )
- {
- // This log statement will go away
- LOGGER.trace( "Instance: {}", this );
- return new RedissonSyncContext( session, hostname, redissonClient, shared );
- }
-
- @PreDestroy
- public void shutdown()
- {
- LOGGER.trace( "Shutting down Redisson client with id '{}'", redissonClient.getId() );
- redissonClient.shutdown();
- }
-
- static class RedissonSyncContext
- implements SyncContext
- {
-
- private static final String CONFIG_PROP_DISCRIMINATOR = "aether.syncContext.redisson.discriminator";
-
- private static final String KEY_PREFIX = "maven:resolver:";
-
- private static final Logger LOGGER = LoggerFactory.getLogger( RedissonSyncContext.class );
-
- private final RepositorySystemSession session;
- private final String hostname;
- private final RedissonClient redissonClient;
- private final boolean shared;
- private final Map<String, RReadWriteLock> locks = new LinkedHashMap<>();
-
- private RedissonSyncContext( RepositorySystemSession session, String hostname,
- RedissonClient redissonClient, boolean shared )
- {
- this.session = session;
- this.hostname = hostname;
- this.redissonClient = redissonClient;
- this.shared = shared;
- }
-
- public void acquire( Collection<? extends Artifact> artifacts,
- Collection<? extends Metadata> metadatas )
- {
- // Deadlock prevention: https://stackoverflow.com/a/16780988/696632
- // We must acquire multiple locks always in the same order!
- Collection<String> keys = new TreeSet<>();
- if ( artifacts != null )
- {
- for ( Artifact artifact : artifacts )
- {
- // TODO Should we include extension and classifier too?
- String key = "artifact:" + artifact.getGroupId() + ":"
- + artifact.getArtifactId() + ":" + artifact.getBaseVersion();
- keys.add( key );
- }
- }
-
- if ( metadatas != null )
- {
- for ( Metadata metadata : metadatas )
- {
- StringBuilder key = new StringBuilder( "metadata:" );
- if ( !metadata.getGroupId().isEmpty() )
- {
- key.append( metadata.getGroupId() );
- if ( !metadata.getArtifactId().isEmpty() )
- {
- key.append( ':' ).append( metadata.getArtifactId() );
- if ( !metadata.getVersion().isEmpty() )
- {
- key.append( ':' ).append( metadata.getVersion() );
- }
- }
- }
- keys.add( key.toString() );
- }
- }
-
- if ( keys.isEmpty() )
- {
- return;
- }
-
- String discriminator = createDiscriminator();
- LOGGER.trace( "Using Redis key discriminator '{}' during this session", discriminator );
-
- LOGGER.trace( "Need {} {} lock(s) for {}", keys.size(), shared ? "read" : "write", keys );
- int acquiredLockCount = 0;
- int reacquiredLockCount = 0;
- for ( String key : keys )
- {
- RReadWriteLock rwLock = locks.get( key );
- if ( rwLock == null )
- {
- rwLock = redissonClient
- .getReadWriteLock( KEY_PREFIX + discriminator + ":" + key );
- locks.put( key, rwLock );
- acquiredLockCount++;
- }
- else
- {
- reacquiredLockCount++;
- }
-
- RLock actualLock = shared ? rwLock.readLock() : rwLock.writeLock();
- // Avoid #getHoldCount() and #isLocked() roundtrips when we are not logging
- if ( LOGGER.isTraceEnabled() )
- {
- LOGGER.trace( "Acquiring {} lock for '{}' (currently held: {}, already locked: {})",
- shared ? "read" : "write", key, actualLock.getHoldCount(),
- actualLock.isLocked() );
- }
- // If this still produces a deadlock we might need to switch to #tryLock() with n attempts
- actualLock.lock();
- }
- LOGGER.trace( "Total new locks acquired: {}, total existing locks reacquired: {}",
- acquiredLockCount, reacquiredLockCount );
- }
-
- private String createDiscriminator()
- {
- String discriminator = ConfigUtils.getString( session, null, CONFIG_PROP_DISCRIMINATOR );
-
- if ( discriminator == null || discriminator.isEmpty() )
- {
-
- File basedir = session.getLocalRepository().getBasedir();
- discriminator = hostname + ":" + basedir;
- try
- {
- Map<String, Object> checksums = ChecksumUtils.calc(
- discriminator.toString().getBytes( StandardCharsets.UTF_8 ),
- Collections.singletonList( "SHA-1" ) );
- Object checksum = checksums.get( "SHA-1" );
-
- if ( checksum instanceof Exception )
- {
- throw (Exception) checksum;
- }
-
- return String.valueOf( checksum );
- }
- catch ( Exception e )
- {
- // TODO Should this be warn?
- LOGGER.trace( "Failed to calculate discriminator digest, using '{}'",
- DEFAULT_DISCRIMINATOR_DIGEST, e );
- return DEFAULT_DISCRIMINATOR_DIGEST;
- }
- }
-
- return discriminator;
- }
-
- public void close()
- {
- if ( locks.isEmpty() )
- {
- return;
- }
-
- // Release locks in reverse insertion order
- Deque<String> keys = new LinkedList<>( locks.keySet() );
- Iterator<String> keysIter = keys.descendingIterator();
- while ( keysIter.hasNext() )
- {
- String key = keysIter.next();
- RReadWriteLock rwLock = locks.get( key );
- RLock actualLock = shared ? rwLock.readLock() : rwLock.writeLock();
- while ( actualLock.getHoldCount() > 0 )
- {
- // Avoid #getHoldCount() roundtrips when we are not logging
- if ( LOGGER.isTraceEnabled() )
- {
- LOGGER.trace( "Releasing {} lock for '{}' (currently held: {})",
- shared ? "read" : "write", key, actualLock.getHoldCount() );
- }
- actualLock.unlock();
- }
- }
- // TODO Should we count reentrant ones too?
- LOGGER.trace( "Total locks released: {}", locks.size() );
- locks.clear();
- }
-
- }
-
-}
diff --git a/maven-resolver-synccontext-redisson/src/site/markdown/index.md.vm b/maven-resolver-synccontext-redisson/src/site/markdown/index.md.vm
deleted file mode 100644
index 7e2a0a7..0000000
--- a/maven-resolver-synccontext-redisson/src/site/markdown/index.md.vm
+++ /dev/null
@@ -1,103 +0,0 @@
-${esc.hash} Redisson Sync Context for Maven Resolver
-
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<span style="color: red; font-size: 16pt">***Note***: *This component is still considered to be experimental, use with caution!*</span>
-
-The Redisson Sync Context Factory is a Redisson-based distributed locks factory for Maven Resolver
-on top of Redis to provide a fast, concurrent-safe access from one or multiple Maven instances to the
-same local Maven repository.
-
-For further details about the factory read the [Javadoc](./apidocs/org/eclipse/aether/synccontext/RedissonSyncContextFactory.html).
-
-${esc.hash}${esc.hash} Open Issues/Notes
-
-- It only works when dependency injection is used and not the bundled `AetherModule` or
- `ServiceLocator` (Maven uses dependency injection).
-- It includes a lot of trace logging which partially will go way as soon as it has been stabilized.
-- Usage from plugins has not been tested yet.
-- The `furnace-maven-plugin` does not work this implementation because it uses `ServiceLocator` instead
- of dependency injection.
-
-${esc.hash}${esc.hash} Installation/Testing
-
-- Create the directory `${maven.home}/lib/ext/redisson/`.
-- Modify `${maven.home}/bin/m2.conf` by adding `load ${maven.home}/lib/ext/redisson/*.jar`
- right after the `${maven.conf}/logging` line.
-- Copy the following dependencies from Maven Central to `${maven.home}/lib/ext/redisson/`:
- <pre class="source">
- ├── <a href="https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy/1.10.7/byte-buddy-1.10.7.jar">byte-buddy-1.10.7.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/javax/cache/cache-api/1.0.0/cache-api-1.0.0.jar">cache-api-1.0.0.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.11.1/jackson-annotations-2.11.1.jar">jackson-annotations-2.11.1.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.11.1/jackson-core-2.11.1.jar">jackson-core-2.11.1.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.11.1/jackson-databind-2.11.1.jar">jackson-databind-2.11.1.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.11.1/jackson-dataformat-yaml-2.11.1.jar">jackson-dataformat-yaml-2.11.1.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar">javax.annotation-api-1.3.2.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/jboss/marshalling/jboss-marshalling/2.0.9.Final/jboss-marshalling-2.0.9.Final.jar">jboss-marshalling-2.0.9.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/jboss/marshalling/jboss-marshalling-river/2.0.9.Final/jboss-marshalling-river-2.0.9.Final.jar">jboss-marshalling-river-2.0.9.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/jodd/jodd-bean/5.0.13/jodd-bean-5.0.13.jar">jodd-bean-5.0.13.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/jodd/jodd-core/5.0.13/jodd-core-5.0.13.jar">jodd-core-5.0.13.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/apache/maven/resolver/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar">${project.artifactId}-${project.version}.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-buffer/4.1.51.Final/netty-buffer-4.1.51.Final.jar">netty-buffer-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-codec/4.1.51.Final/netty-codec-4.1.51.Final.jar">netty-codec-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-codec-dns/4.1.51.Final/netty-codec-dns-4.1.51.Final.jar">netty-codec-dns-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-common/4.1.51.Final/netty-common-4.1.51.Final.jar">netty-common-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-handler/4.1.51.Final/netty-handler-4.1.51.Final.jar">netty-handler-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-resolver/4.1.51.Final/netty-resolver-4.1.51.Final.jar">netty-resolver-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-resolver-dns/4.1.51.Final/netty-resolver-dns-4.1.51.Final.jar">netty-resolver-dns-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/netty/netty-transport/4.1.51.Final/netty-transport-4.1.51.Final.jar">netty-transport-4.1.51.Final.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/reactivestreams/reactive-streams/1.0.3/reactive-streams-1.0.3.jar">reactive-streams-1.0.3.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/projectreactor/reactor-core/3.3.4.RELEASE/reactor-core-3.3.4.RELEASE.jar">reactor-core-3.3.4.RELEASE.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/org/redisson/redisson/3.13.3/redisson-3.13.3.jar">redisson-3.13.3.jar</a>
- ├── <a href="https://repo1.maven.org/maven2/io/reactivex/rxjava2/rxjava/2.2.19/rxjava-2.2.19.jar">rxjava-2.2.19.jar</a>
- └── <a href="https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.26/snakeyaml-1.26.jar">snakeyaml-1.26.jar</a></pre>
-- Start your Redis instance on `localhost` or configure a remote instance with `${maven.conf}/maven-resolver-redisson.yaml`.
-- Now start a multithreaded Maven build on your project.
-
-${esc.hash}${esc.hash}${esc.hash} Debugging
-
-- Add/modify the following entries in `${maven.home}/conf/logging/simplelogger.properties`:
- ```
- org.slf4j.simpleLogger.showDateTime=true
- org.slf4j.simpleLogger.showThreadName=true
- org.slf4j.simpleLogger.showShortLogName=true
- org.slf4j.simpleLogger.log.org.eclipse.aether.synccontext=trace
- #org.slf4j.simpleLogger.log.org.redisson=debug
- #org.slf4j.simpleLogger.log.io.netty=debug
- ```
-- Now start a multithreaded Maven build on your project and you should see at least these lines:
- ```
- # This line does not appear for the default configuration
- 2316 [main] [TRACE] RedissonSyncContextFactory - Reading Redisson config file from '${maven.conf}/maven-resolver-redisson.yaml'
- 4626 [main] [TRACE] RedissonSyncContextFactory - Created Redisson client with id '1c8db59b-7939-4014-8506-ae841c74608c'
- ...
- 35318 [main] [TRACE] RedissonSyncContextFactory - Shutting down Redisson client with id '1c8db59b-7939-4014-8506-ae841c74608c'
- ```
-
-${esc.hash}${esc.hash} Configuration Options
-
-Option | Type | Description | Default Value
---- | --- | --- | --- | ---
-`aether.syncContext.redisson.configFile` | String | Path to a Redisson configuration file in YAML format. Read [official documentation](https://github.com/redisson/redisson/wiki/2.-Configuration) for details. | `${maven.conf}/maven-resolver-redisson.yaml`
-`aether.syncContext.redisson.discriminator` | String | A discriminator uniquely identifying a host and repository pair. If the generation of the default value fails, it will use `sha1('')`. | `sha1('${esc.dollar}{hostname:-localhost}:${maven.repo.local}')`
-
-${esc.hash}${esc.hash} Set Configuration from Apache Maven
-
-To set one of the configuration options from above just use system variables.
diff --git a/pom.xml b/pom.xml
index c53cc82..4c12924 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,7 @@
<properties>
<javaVersion>8</javaVersion>
<surefire.redirectTestOutputToFile>true</surefire.redirectTestOutputToFile>
+ <failsafe.redirectTestOutputToFile>${surefire.redirectTestOutputToFile}</failsafe.redirectTestOutputToFile>
<maven.site.path>resolver-archives/resolver-LATEST</maven.site.path>
<checkstyle.violation.ignore>None</checkstyle.violation.ignore>
<sisuVersion>0.3.4</sisuVersion>
@@ -85,6 +86,9 @@
<module>maven-resolver-api</module>
<module>maven-resolver-spi</module>
<module>maven-resolver-util</module>
+ <module>maven-resolver-named-locks</module>
+ <module>maven-resolver-named-locks-hazelcast</module>
+ <module>maven-resolver-named-locks-redisson</module>
<module>maven-resolver-impl</module>
<module>maven-resolver-test-util</module>
<module>maven-resolver-connector-basic</module>
@@ -92,8 +96,6 @@
<module>maven-resolver-transport-file</module>
<module>maven-resolver-transport-http</module>
<module>maven-resolver-transport-wagon</module>
- <module>maven-resolver-synccontext-global</module>
- <module>maven-resolver-synccontext-redisson</module>
<module>maven-resolver-demos</module>
</modules>
@@ -116,6 +118,11 @@
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-named-locks</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-impl</artifactId>
<version>${project.version}</version>
</dependency>
@@ -159,6 +166,12 @@
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <version>2.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>2.2</version>
<scope>test</scope>
@@ -372,6 +385,26 @@
</configuration>
</plugin>
<plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration>
+ <failIfNoTests>false</failIfNoTests>
+ <argLine>-Xmx128m</argLine>
+ <redirectTestOutputToFile>${failsafe.redirectTestOutputToFile}</redirectTestOutputToFile>
+ <systemPropertyVariables>
+ <java.io.tmpdir>${project.build.directory}/failsafe-tmp</java.io.tmpdir>
+ </systemPropertyVariables>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
<groupId>org.eclipse.sisu</groupId>
<artifactId>sisu-maven-plugin</artifactId>
<version>${sisuVersion}</version>