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>