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 50df10d  [MRESOLVER-219] Implement NamedLock with advisory file locking
50df10d is described below

commit 50df10dca354445f2f93356f08b21a6ee5143d0c
Author: Tamas Cservenak <[email protected]>
AuthorDate: Tue Nov 23 18:14:45 2021 +0100

    [MRESOLVER-219] Implement NamedLock with advisory file locking
    
    This closes #131
---
 .../eclipse/aether/impl/guice/AetherModule.java    |  14 +-
 .../impl/synccontext/named/FileGAVNameMapper.java  | 131 ++++++++++
 .../synccontext/named/NamedLockFactoryAdapter.java |  10 +
 .../named/SimpleNamedLockFactorySelector.java      |   3 +
 .../src/site/markdown/synccontextfactory.md.vm     |   2 +
 .../impl/synccontext/FileLockAdapterTest.java      |  36 +++
 .../named/providers/FileLockNamedLockFactory.java  | 104 ++++++++
 .../aether/named/support/FileLockNamedLock.java    | 287 +++++++++++++++++++++
 .../aether/named/support/FileSystemFriendly.java   |  33 +++
 .../org/eclipse/aether/named/support/Retry.java    |  93 +++++++
 .../src/site/markdown/index.md.vm                  |   3 +
 .../named/FileLockNamedLockFactorySupportTest.java |  54 ++++
 .../aether/named/NamedLockFactoryTestSupport.java  |  51 +++-
 .../java/org/eclipse/aether/named/RetryTest.java   |  70 +++++
 14 files changed, 885 insertions(+), 6 deletions(-)

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 4681982..9edf5f9 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
@@ -49,7 +49,9 @@ 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.internal.impl.synccontext.named.FileGAVNameMapper;
 import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
 import org.eclipse.aether.impl.UpdateCheckManager;
@@ -171,6 +173,8 @@ public class AetherModule
             .to( GAVNameMapper.class ).in( Singleton.class );
         bind( NameMapper.class ).annotatedWith( Names.named( 
DiscriminatingNameMapper.NAME ) )
             .to( DiscriminatingNameMapper.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( 
FileGAVNameMapper.NAME ) )
+            .to( FileGAVNameMapper.class ).in( Singleton.class );
 
         bind( NamedLockFactory.class ).annotatedWith( Names.named( 
NoopNamedLockFactory.NAME ) )
             .to( NoopNamedLockFactory.class ).in( Singleton.class );
@@ -178,6 +182,8 @@ public class AetherModule
             .to( LocalReadWriteLockNamedLockFactory.class ).in( 
Singleton.class );
         bind( NamedLockFactory.class ).annotatedWith( Names.named( 
LocalSemaphoreNamedLockFactory.NAME ) )
             .to( LocalSemaphoreNamedLockFactory.class ).in( Singleton.class );
+        bind( NamedLockFactory.class ).annotatedWith( Names.named( 
FileLockNamedLockFactory.NAME ) )
+            .to( FileLockNamedLockFactory.class ).in( Singleton.class );
 
         install( new Slf4jModule() );
 
@@ -188,12 +194,14 @@ public class AetherModule
     Map<String, NameMapper> provideNameMappers(
         @Named( StaticNameMapper.NAME ) NameMapper staticNameMapper,
         @Named( GAVNameMapper.NAME ) NameMapper gavNameMapper,
-        @Named( DiscriminatingNameMapper.NAME ) NameMapper 
discriminatingNameMapper )
+        @Named( DiscriminatingNameMapper.NAME ) NameMapper 
discriminatingNameMapper,
+        @Named( FileGAVNameMapper.NAME ) NameMapper fileGavNameMapper )
     {
         Map<String, NameMapper> nameMappers = new HashMap<>();
         nameMappers.put( StaticNameMapper.NAME, staticNameMapper );
         nameMappers.put( GAVNameMapper.NAME, gavNameMapper );
         nameMappers.put( DiscriminatingNameMapper.NAME, 
discriminatingNameMapper );
+        nameMappers.put( FileGAVNameMapper.NAME, fileGavNameMapper );
         return Collections.unmodifiableMap( nameMappers );
     }
 
@@ -201,11 +209,13 @@ public class AetherModule
     @Singleton
     Map<String, NamedLockFactory> provideNamedLockFactories(
             @Named( LocalReadWriteLockNamedLockFactory.NAME ) NamedLockFactory 
localRwLock,
-            @Named( LocalSemaphoreNamedLockFactory.NAME ) NamedLockFactory 
localSemaphore )
+            @Named( LocalSemaphoreNamedLockFactory.NAME ) NamedLockFactory 
localSemaphore,
+            @Named( FileLockNamedLockFactory.NAME ) NamedLockFactory 
fileLockFactory )
     {
         Map<String, NamedLockFactory> factories = new HashMap<>();
         factories.put( LocalReadWriteLockNamedLockFactory.NAME, localRwLock );
         factories.put( LocalSemaphoreNamedLockFactory.NAME, localSemaphore );
+        factories.put( FileLockNamedLockFactory.NAME, fileLockFactory );
         return Collections.unmodifiableMap( factories );
     }
 
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java
new file mode 100644
index 0000000..500b330
--- /dev/null
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java
@@ -0,0 +1,131 @@
+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.named.support.FileSystemFriendly;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A {@link NameMapper} that creates same name mapping as Takari Local 
Repository does, with
+ * {@code baseDir} (local repo). Part of code blatantly copies parts of the 
Takari
+ * {@code LockingSyncContext}.
+ *
+ * @see <a 
href="https://github.com/takari/takari-local-repository/blob/24133e50a0478dccb5620ac2f2255187608f165b/src/main/java/io/takari/aether/concurrency/LockingSyncContext.java";>Takari
+ * LockingSyncContext.java</a>
+ */
+@Singleton
+@Named( FileGAVNameMapper.NAME )
+public class FileGAVNameMapper
+    implements NameMapper, FileSystemFriendly
+{
+    public static final String NAME = "file-gav";
+
+    private static final String LOCK_SUFFIX = ".resolverlock";
+
+    private static final char SEPARATOR = '~';
+
+    private final ConcurrentMap<String, Path> baseDirs;
+
+    public FileGAVNameMapper()
+    {
+        this.baseDirs = new ConcurrentHashMap<>();
+    }
+
+    @Override
+    public TreeSet<String> nameLocks( final RepositorySystemSession session,
+                                      final Collection<? extends Artifact> 
artifacts,
+                                      final Collection<? extends Metadata> 
metadatas )
+    {
+        File localRepositoryBasedir = 
session.getLocalRepository().getBasedir();
+        // here we abuse concurrent hash map to make sure costly 
getCanonicalFile is invoked only once
+        Path baseDir = baseDirs.computeIfAbsent(
+            localRepositoryBasedir.getPath(), k ->
+            {
+                try
+                {
+                    return new File( localRepositoryBasedir, ".locks" 
).getCanonicalFile().toPath();
+                }
+                catch ( IOException e )
+                {
+                    throw new UncheckedIOException( e );
+                }
+            }
+        );
+
+        TreeSet<String> paths = new TreeSet<>();
+        if ( artifacts != null )
+        {
+            for ( Artifact artifact : artifacts )
+            {
+                paths.add( getPath( baseDir, artifact ) + LOCK_SUFFIX );
+            }
+        }
+        if ( metadatas != null )
+        {
+            for ( Metadata metadata : metadatas )
+            {
+                paths.add( getPath( baseDir, metadata ) + LOCK_SUFFIX );
+            }
+        }
+        return paths;
+    }
+
+    private String getPath( final Path baseDir, final Artifact artifact )
+    {
+        // NOTE: Don't use LRM.getPath*() as those paths could be different 
across processes, e.g. due to staging LRMs.
+        String path = artifact.getGroupId()
+            + SEPARATOR + artifact.getArtifactId()
+            + SEPARATOR + artifact.getBaseVersion();
+        return baseDir.resolve( path ).toAbsolutePath().toString();
+    }
+
+    private String getPath( final Path baseDir, final Metadata metadata )
+    {
+        // NOTE: Don't use LRM.getPath*() as those paths could be different 
across processes, e.g. due to staging.
+        String path = "";
+        if ( metadata.getGroupId().length() > 0 )
+        {
+            path += metadata.getGroupId();
+            if ( metadata.getArtifactId().length() > 0 )
+            {
+                path += SEPARATOR + metadata.getArtifactId();
+                if ( metadata.getVersion().length() > 0 )
+                {
+                    path += SEPARATOR + metadata.getVersion();
+                }
+            }
+        }
+        return baseDir.resolve( path ).toAbsolutePath().toString();
+    }
+}
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
index be9ea1e..b59b26c 100644
--- 
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
@@ -25,7 +25,9 @@ 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.eclipse.aether.named.support.FileSystemFriendly;
 import org.eclipse.aether.util.ConfigUtils;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -56,6 +58,14 @@ public final class NamedLockFactoryAdapter
     {
         this.nameMapper = Objects.requireNonNull( nameMapper );
         this.namedLockFactory = Objects.requireNonNull( namedLockFactory );
+        // TODO: this is ad-hoc "validation", experimental and likely to change
+        if ( this.namedLockFactory instanceof FileSystemFriendly
+                && !( this.nameMapper instanceof FileSystemFriendly ) )
+        {
+            throw new IllegalArgumentException(
+                    "Misconfiguration: FS friendly lock factory requires FS 
friendly name mapper"
+            );
+        }
     }
 
     public SyncContext newInstance( final RepositorySystemSession session, 
final boolean shared )
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SimpleNamedLockFactorySelector.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SimpleNamedLockFactorySelector.java
index db91308..8c96173 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SimpleNamedLockFactorySelector.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/SimpleNamedLockFactorySelector.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.internal.impl.synccontext.named;
  */
 
 import org.eclipse.aether.named.NamedLockFactory;
+import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory;
 import org.eclipse.aether.named.providers.NoopNamedLockFactory;
@@ -84,12 +85,14 @@ public final class SimpleNamedLockFactorySelector
         factories.put( NoopNamedLockFactory.NAME, new NoopNamedLockFactory() );
         factories.put( LocalReadWriteLockNamedLockFactory.NAME, new 
LocalReadWriteLockNamedLockFactory() );
         factories.put( LocalSemaphoreNamedLockFactory.NAME, new 
LocalSemaphoreNamedLockFactory() );
+        factories.put( FileLockNamedLockFactory.NAME, new 
FileLockNamedLockFactory() );
         this.namedLockFactory = selectNamedLockFactory( factories, 
getFactoryName() );
 
         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() ) );
+        nameMappers.put( FileGAVNameMapper.NAME, new FileGAVNameMapper() );
         this.nameMapper = selectNameMapper( nameMappers, getNameMapperName() );
     }
 
diff --git a/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm 
b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
index d198291..9c1f492 100644
--- a/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
+++ b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
@@ -55,6 +55,7 @@ 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.
+- `file-lock`, uses advisory file locking, usable for MP builds (must be used 
with `file-gav` name mapping).
 - `noop`, implement no-op locking (no locking). For experimenting only. Has 
same functionality as old "nolock"
   SyncContextFactory implementation.
 
@@ -62,6 +63,7 @@ For the `aether.syncContext.named.nameMapper` property 
following values are allo
 
 - `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.
+- `file-gav` uses GAV and session to create absolute file paths (to be used 
with `file-lock` factory)
 - `static` uses static (same) string as lock name for any input. Effectively 
providing functionality same as old
   "global" locking SyncContextFactory.
 
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
new file mode 100644
index 0000000..bebd367
--- /dev/null
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
@@ -0,0 +1,36 @@
+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.internal.impl.synccontext.named.FileGAVNameMapper;
+import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class FileLockAdapterTest
+    extends NamedLockFactoryAdapterTestSupport
+{
+    @BeforeClass
+    public static void createNamedLockFactory()
+    {
+        nameMapper = new FileGAVNameMapper();
+        namedLockFactory = new FileLockNamedLockFactory();
+        createAdapter();
+    }
+}
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
new file mode 100644
index 0000000..b40772e
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
@@ -0,0 +1,104 @@
+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.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.named.support.FileLockNamedLock;
+import org.eclipse.aether.named.support.FileSystemFriendly;
+import org.eclipse.aether.named.support.NamedLockFactorySupport;
+import org.eclipse.aether.named.support.NamedLockSupport;
+
+/**
+ * Named locks factory of {@link FileLockNamedLock}s. This is a bit special 
implementation, as it
+ * expects locks names to be fully qualified absolute file system paths.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( FileLockNamedLockFactory.NAME )
+public class FileLockNamedLockFactory
+    extends NamedLockFactorySupport
+    implements FileSystemFriendly
+{
+    public static final String NAME = "file-lock";
+
+    private final ConcurrentMap<String, FileChannel> fileChannels;
+
+    public FileLockNamedLockFactory()
+    {
+        this.fileChannels = new ConcurrentHashMap<>();
+    }
+
+    @Override
+    protected NamedLockSupport createLock( final String name )
+    {
+        Path path = Paths.get( name );
+        FileChannel fileChannel = fileChannels.computeIfAbsent( name, k ->
+        {
+            try
+            {
+                Files.createDirectories( path.getParent() );
+                return FileChannel.open(
+                        path,
+                        StandardOpenOption.READ, StandardOpenOption.WRITE,
+                        StandardOpenOption.CREATE, 
StandardOpenOption.DELETE_ON_CLOSE
+                );
+            }
+            catch ( IOException e )
+            {
+                throw new UncheckedIOException( "Failed to open file channel 
for '"
+                    + name + "'", e );
+            }
+        } );
+        return new FileLockNamedLock( name, fileChannel, this );
+    }
+
+    @Override
+    protected void destroyLock( final String name )
+    {
+        FileChannel fileChannel = fileChannels.remove( name );
+        if ( fileChannel == null )
+        {
+            throw new IllegalStateException( "File channel expected, but does 
not exist: " + name );
+        }
+
+        try
+        {
+            fileChannel.close();
+        }
+        catch ( IOException e )
+        {
+            throw new UncheckedIOException( "Failed to close file channel for 
'"
+                    + name + "'", e );
+        }
+    }
+}
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileLockNamedLock.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileLockNamedLock.java
new file mode 100644
index 0000000..b4c2877
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileLockNamedLock.java
@@ -0,0 +1,287 @@
+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.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static org.eclipse.aether.named.support.Retry.retry;
+
+/**
+ * Named lock that uses {@link FileLock}. An instance of this class is about 
ONE LOCK (one file)
+ * and is possibly used by multiple threads. Each thread (if properly coded re 
boxing) will try to
+ * obtain either shared or exclusive lock. As file locks are JVM-scoped (so 
one JVM can obtain
+ * same file lock only once), the threads share file lock and synchronize 
according to it. Still,
+ * as file lock obtain operation does not block (or in other words, the method 
that does block
+ * cannot be controlled for how long it blocks), we are "simulating" thread 
blocking using
+ * {@link Retry} utility.
+ * This implementation performs coordination not only on thread (JVM-local) 
level, but also on
+ * process level, as long as other parties are using this same "advisory" 
locking mechanism.
+ *
+ * @since TBD
+ */
+public final class FileLockNamedLock
+    extends NamedLockSupport
+{
+    private static final long RETRY_SLEEP_MILLIS = 100L;
+
+    private static final long LOCK_POSITION = 0L;
+
+    private static final long LOCK_SIZE = 1L;
+
+    /**
+     * Thread -> steps stack (true = shared, false = exclusive)
+     */
+    private final Map<Thread, Deque<Boolean>> threadSteps;
+
+    /**
+     * The {@link FileChannel} this instance is about.
+     */
+    private final FileChannel fileChannel;
+
+    /**
+     * The reference of {@link FileLock}, if obtained.
+     */
+    private final AtomicReference<FileLock> fileLockRef;
+
+    /**
+     * Lock protecting "critical region": this is where threads are allowed to 
perform locking but should leave this
+     * region as quick as possible.
+     */
+    private final ReentrantLock criticalRegion;
+
+    public FileLockNamedLock( final String name,
+                              final FileChannel fileChannel,
+                              final NamedLockFactorySupport factory )
+    {
+        super( name, factory );
+        this.threadSteps = new HashMap<>();
+        this.fileChannel = fileChannel;
+        this.fileLockRef = new AtomicReference<>( null );
+        this.criticalRegion = new ReentrantLock();
+    }
+
+    @Override
+    public boolean lockShared( final long time, final TimeUnit unit ) throws 
InterruptedException
+    {
+        return retry( time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, 
null, false );
+    }
+
+    @Override
+    public boolean lockExclusively( final long time, final TimeUnit unit ) 
throws InterruptedException
+    {
+        return retry( time, unit, RETRY_SLEEP_MILLIS, this::doLockExclusively, 
null, false );
+    }
+
+    private Boolean doLockShared()
+    {
+        if ( criticalRegion.tryLock() )
+        {
+            try
+            {
+                Deque<Boolean> steps = threadSteps.computeIfAbsent( 
Thread.currentThread(), k -> new ArrayDeque<>() );
+                FileLock obtainedLock = fileLockRef.get();
+                if ( obtainedLock != null )
+                {
+                    if ( obtainedLock.isShared() )
+                    {
+                        // TODO No counterpart in other lock impls, drop or 
make consistent?
+                        logger.trace( "{} lock (shared={})", name(), true );
+                        steps.push( Boolean.TRUE );
+                        return true;
+                    }
+                    else
+                    {
+                        // if we own exclusive, that's still fine
+                        boolean weOwnExclusive = steps.contains( Boolean.FALSE 
);
+                        if ( weOwnExclusive )
+                        {
+                            // TODO No counterpart in other lock impls, drop 
or make consistent?
+                            logger.trace( "{} lock (shared={})", name(), true 
);
+                            steps.push( Boolean.TRUE );
+                            return true;
+                        }
+                        else
+                        {
+                            // someone else owns exclusive, let's wait
+                            return null;
+                        }
+                    }
+                }
+
+                // TODO No counterpart in other lock impls, drop or make 
consistent?
+                logger.trace( "{} no obtained lock: obtain shared file lock", 
name() );
+                FileLock fileLock = obtainFileLock( true );
+                if ( fileLock != null )
+                {
+                    fileLockRef.set( fileLock );
+                    steps.push( Boolean.TRUE );
+                    return true;
+                }
+            }
+            finally
+            {
+                criticalRegion.unlock();
+            }
+        }
+        return null;
+    }
+
+    private Boolean doLockExclusively()
+    {
+        if ( criticalRegion.tryLock() )
+        {
+            try
+            {
+                Deque<Boolean> steps = threadSteps.computeIfAbsent( 
Thread.currentThread(), k -> new ArrayDeque<>() );
+                FileLock obtainedLock = fileLockRef.get();
+                if ( obtainedLock != null )
+                {
+                    if ( obtainedLock.isShared() )
+                    {
+                        // if we own shared, that's attempted upgrade
+                        boolean weOwnShared = steps.contains( Boolean.TRUE );
+                        if ( weOwnShared )
+                        {
+                            // TODO No counterpart in other lock impls, drop 
or make consistent?
+                            logger.trace(
+                                    "{} steps not empty, has not exclusive 
lock: lock-upgrade not supported", name()
+                            );
+                            return false; // Lock upgrade not supported
+                        }
+                        else
+                        {
+                            // someone else owns shared, let's wait
+                            return null;
+                        }
+                    }
+                    else
+                    {
+                        // if we own exclusive, that's fine
+                        boolean weOwnExclusive = steps.contains( Boolean.FALSE 
);
+                        if ( weOwnExclusive )
+                        {
+                            // TODO No counterpart in other lock impls, drop 
or make consistent?
+                            logger.trace( "{} lock (shared={})", name(), false 
);
+                            steps.push( Boolean.FALSE );
+                            return true;
+                        }
+                        else
+                        {
+                            // someone else owns exclusive, let's wait
+                            return null;
+                        }
+                    }
+                }
+
+                // TODO No counterpart in other lock impls, drop or make 
consistent?
+                logger.trace( "{} no obtained lock: obtain exclusive file 
lock", name() );
+                FileLock fileLock = obtainFileLock( false );
+                if ( fileLock != null )
+                {
+                    fileLockRef.set( fileLock );
+                    steps.push( Boolean.FALSE );
+                    return true;
+                }
+            }
+            finally
+            {
+                criticalRegion.unlock();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void unlock()
+    {
+        criticalRegion.lock();
+        try
+        {
+            Deque<Boolean> steps = threadSteps.computeIfAbsent( 
Thread.currentThread(), k -> new ArrayDeque<>() );
+            if ( steps.isEmpty() )
+            {
+                throw new IllegalStateException( "Wrong API usage: unlock 
without lock" );
+            }
+            Boolean shared = steps.pop();
+            // TODO No counterpart in other lock impls, drop or make 
consistent?
+            logger.trace( "{} unlock (shared = {})", name(), shared );
+            if ( steps.isEmpty() && !anyOtherThreadHasSteps() )
+            {
+                try
+                {
+                    fileLockRef.getAndSet( null ).release();
+                }
+                catch ( IOException e )
+                {
+                    throw new UncheckedIOException( e );
+                }
+            }
+        }
+        finally
+        {
+            criticalRegion.unlock();
+        }
+    }
+
+    /**
+     * Returns {@code true} if any other than this thread using this instance 
has any step recorded.
+     */
+    private boolean anyOtherThreadHasSteps()
+    {
+        return threadSteps.entrySet().stream()
+                .filter( e -> !Thread.currentThread().equals( e.getKey() ) )
+                .map( Map.Entry::getValue )
+                .anyMatch( d -> !d.isEmpty() );
+    }
+
+    /**
+     * Attempts to obtain real {@link FileLock}, returns non-null value is 
succeeds, or {@code null} if cannot.
+     */
+    private FileLock obtainFileLock( final boolean shared )
+    {
+        FileLock result;
+        try
+        {
+            result = fileChannel.tryLock( LOCK_POSITION, LOCK_SIZE, shared );
+        }
+        catch ( OverlappingFileLockException e )
+        {
+            logger.trace( "File lock overlap on '{}'", name(), e );
+            return null;
+        }
+        catch ( IOException e )
+        {
+            logger.trace( "Failure on acquire of file lock for '{}'", name(), 
e );
+            throw new UncheckedIOException( "Failed to acquire lock file 
channel for '" + name() + "'", e );
+        }
+        return result;
+    }
+}
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java
new file mode 100644
index 0000000..d029e4e
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java
@@ -0,0 +1,33 @@
+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.
+ */
+
+/**
+ * A marker interface that mark component "file system friendly". In case of 
lock factory, it
+ * would mean that passed in lock names MUST ADHERE to file path naming 
convention (and not use
+ * some special, non FS friendly characters in it). Essentially, component 
marked with this
+ * interface expects (or uses) that "name" is an absolute and valid file path.
+ *
+ * <strong>Important note:</strong> Experimental interface, is not meant to be 
used outside of
+ * Maven Resolver codebase. May change or be removed completely without any 
further notice.
+ */
+public interface FileSystemFriendly
+{
+}
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/Retry.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/Retry.java
new file mode 100644
index 0000000..1f5a61b
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/Retry.java
@@ -0,0 +1,93 @@
+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.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Retry helper: retries given {@code Callable} as long as it returns {@code 
null} (interpreted
+ * as "no answer yet") or given time passes. This helper implements similar 
semantics regarding
+ * caller threads as {@link java.util.concurrent.locks.Lock#tryLock(long, 
TimeUnit)} method does:
+ * blocks the caller thread until operation return non-{@code null} value 
within the given waiting
+ * time and the current thread has not been {@linkplain Thread#interrupt 
interrupted}.
+ *
+ * @since TBD
+ */
+public final class Retry
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger( Retry.class 
);
+
+    private Retry()
+    {
+      // no instances
+    }
+
+    /**
+     * Retries for given amount of time (time, unit) the passed in operation, 
sleeping given
+     * {@code sleepMills} between retries. In case operation returns {@code 
null}, it is assumed
+     * "is not done yet" state, so retry will happen (if time barrier allows). 
If time barrier
+     * passes, and still {@code null} ("is not done yet") is returned from 
operation, the
+     * {@code defaultResult} is returned.
+     */
+    public static  <R> R retry( final long time,
+                                final TimeUnit unit,
+                                final long sleepMillis,
+                                final Callable<R> operation,
+                                final Predicate<Exception> retryPredicate,
+                                final R defaultResult ) throws 
InterruptedException
+    {
+        long now = System.nanoTime();
+        final long barrier = now + unit.toNanos( time );
+        int attempt = 1;
+        R result = null;
+        while ( now < barrier && result == null )
+        {
+          try
+          {
+            result = operation.call();
+            if ( result == null )
+            {
+              LOGGER.trace( "Retry attempt {}: no result", attempt );
+              Thread.sleep( sleepMillis );
+            }
+          }
+          catch ( InterruptedException e )
+          {
+            throw e;
+          }
+          catch ( Exception e )
+          {
+            LOGGER.trace( "Retry attempt {}: operation failure", attempt, e );
+            if ( retryPredicate != null && !retryPredicate.test( e ) )
+            {
+                throw new IllegalStateException( e );
+            }
+          }
+          now = System.nanoTime();
+          attempt++;
+        }
+        return result == null ? defaultResult : result;
+    }
+}
diff --git a/maven-resolver-named-locks/src/site/markdown/index.md.vm 
b/maven-resolver-named-locks/src/site/markdown/index.md.vm
index 80b6378..0d8d401 100644
--- a/maven-resolver-named-locks/src/site/markdown/index.md.vm
+++ b/maven-resolver-named-locks/src/site/markdown/index.md.vm
@@ -31,6 +31,8 @@ Out of the box, "local" (local to JVM) named lock 
implementations are the follow
   JVM `java.util.concurrent.locks.ReentrantReadWriteLock`.
 - `semaphore-local` implemented in 
`org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory` that uses
   JVM `java.util.concurrent.Semaphore`.
+- `file-lock` implemented in 
`org.eclipse.aether.named.providers.FileLockNamedLockFactory` that uses
+  JVM `java.nio.channels.FileLock`.
 - `noop` implemented in 
`org.eclipse.aether.named.providers.NoopNamedLockFactory` that uses no locking.
 
 Out of the box, "distributed" named lock implementations are the following 
(separate modules which require additional dependencies):
@@ -51,3 +53,4 @@ Out of the box, name mapper implementations are the following:
 - `static` implemented in 
`org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper`.
 - `gav` implemented in 
`org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper`.
 - `discriminating` implemented in 
`org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper`.
+- `file-gav` implemented in 
`org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper`.
diff --git 
a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
 
b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
new file mode 100644
index 0000000..03f0856
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
@@ -0,0 +1,54 @@
+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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
+import org.junit.BeforeClass;
+
+public class FileLockNamedLockFactorySupportTest
+    extends NamedLockFactoryTestSupport
+    {
+
+    private final Path baseDir;
+
+    public FileLockNamedLockFactorySupportTest() throws IOException
+    {
+        String path = System.getProperty( "java.io.tmpdir" );
+        Files.createDirectories( Paths.get (path) ); // hack for surefire: 
sets the property but directory does not exist
+        this.baseDir = Files.createTempDirectory( null );
+    }
+
+    @Override
+    protected String lockName()
+    {
+        return baseDir.resolve( testName.getMethodName() 
).toAbsolutePath().toString();
+    }
+
+    @BeforeClass
+    public static void createNamedLockFactory() throws IOException
+    {
+        namedLockFactory = new FileLockNamedLockFactory();
+    }
+}
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
index 9141039..de4a8fe 100644
--- 
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
@@ -28,9 +28,7 @@ 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;
+import static org.hamcrest.Matchers.*;
 
 /**
  * UT support for {@link NamedLockFactory}.
@@ -158,6 +156,51 @@ public abstract class NamedLockFactoryTestSupport {
         losers.await();
     }
 
+    @Test(timeout = 5000)
+    public void fullyConsumeLockTime() throws InterruptedException {
+        long start = System.nanoTime();
+        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();
+        long end = System.nanoTime();
+        long duration = end - start;
+        long expectedDuration = TimeUnit.MILLISECONDS.toNanos( 
ACCESS_WAIT_MILLIS );
+        assertThat(duration, greaterThanOrEqualTo(expectedDuration)); // equal 
in ideal case
+    }
+
+    @Test(timeout = 5000)
+    public void releasedExclusiveAllowAccess() throws InterruptedException {
+        final String name = lockName();
+        CountDownLatch winners = new CountDownLatch(1); // we expect 1 winner
+        CountDownLatch losers = new CountDownLatch(0); // we expect 0 loser
+        Thread t1 = new Thread(new Access(namedLockFactory, name, true, 
winners, losers));
+        try (NamedLock namedLock = namedLockFactory.getLock(name))
+        {
+            assertThat( namedLock.lockExclusively( 50L, TimeUnit.MILLISECONDS 
), is( true ));
+            try {
+                t1.start();
+                Thread.sleep(50L );
+            }
+            finally
+            {
+                namedLock.unlock();
+            }
+        }
+        t1.join();
+        winners.await();
+        losers.await();
+    }
+
+    private static final long ACCESS_WAIT_MILLIS = 1000L;
+
     private static class Access implements Runnable {
         final NamedLockFactory namedLockFactory;
         final String name;
@@ -180,7 +223,7 @@ public abstract class NamedLockFactoryTestSupport {
         @Override
         public void run() {
             try (NamedLock lock = namedLockFactory.getLock(name)) {
-                if (shared ? lock.lockShared(100L, TimeUnit.MILLISECONDS) : 
lock.lockExclusively(100L, TimeUnit.MILLISECONDS)) {
+                if (shared ? lock.lockShared(ACCESS_WAIT_MILLIS, 
TimeUnit.MILLISECONDS) : lock.lockExclusively(ACCESS_WAIT_MILLIS, 
TimeUnit.MILLISECONDS)) {
                     try {
                         winner.countDown();
                         loser.await();
diff --git 
a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/RetryTest.java
 
b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/RetryTest.java
new file mode 100644
index 0000000..c26aeea
--- /dev/null
+++ 
b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/RetryTest.java
@@ -0,0 +1,70 @@
+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;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import static org.eclipse.aether.named.support.Retry.retry;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+
+/**
+ * UT for {@link org.eclipse.aether.named.support.Retry}.
+ */
+public class RetryTest
+{
+    private static final long RETRY_SLEEP_MILLIS = 250L;
+
+    @Rule
+    public TestName testName = new TestName();
+
+    @Test
+    public void happy() throws InterruptedException
+    {
+        LongAdder retries = new LongAdder();
+        String result = retry( 1L, TimeUnit.SECONDS, RETRY_SLEEP_MILLIS, () -> 
{ retries.increment(); return "happy"; }, null, "notHappy" );
+        assertThat( result, equalTo( "happy" ) );
+        assertThat( retries.sum(), equalTo( 1L ) );
+    }
+
+    @Test
+    public void notHappy() throws InterruptedException
+    {
+        LongAdder retries = new LongAdder();
+        String result = retry( 1L, TimeUnit.SECONDS, RETRY_SLEEP_MILLIS, () -> 
{ retries.increment(); return null; }, null, "notHappy" );
+        assertThat( result, equalTo( "notHappy" ) );
+        assertThat( retries.sum(), greaterThan( 1L ) );
+    }
+
+    @Test
+    public void happyOnSomeAttempt() throws InterruptedException
+    {
+        LongAdder retries = new LongAdder();
+        String result = retry( 1L, TimeUnit.SECONDS, RETRY_SLEEP_MILLIS, () -> 
{ retries.increment(); return retries.sum() == 2 ? "got it" : null; }, null, 
"notHappy" );
+        assertThat( result, equalTo( "got it" ) );
+        assertThat( retries.sum(), equalTo( 2L ) );
+    }
+}

Reply via email to