You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2021/10/07 13:41:22 UTC

[maven-resolver] branch file-lock created (now 16eb043)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a change to branch file-lock
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git.


      at 16eb043  File advisory locking based NamedLocks

This branch includes the following new commits:

     new 16eb043  File advisory locking based NamedLocks

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[maven-resolver] 01/01: File advisory locking based NamedLocks

Posted by cs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch file-lock
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git

commit 16eb04374d34989f39602a37a7b02ae0e48a282e
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Thu Oct 7 15:40:32 2021 +0200

    File advisory locking based NamedLocks
    
    This PR returns the removed FileNamedLocks from original PR
    and will try to make them work as expected.
---
 .../named/providers/FileLockNamedLockFactory.java  | 104 ++++++++
 .../aether/named/support/FileLockNamedLock.java    | 266 +++++++++++++++++++++
 .../aether/named/FileLockNamedLockFactoryTest.java |  46 ++++
 3 files changed, 416 insertions(+)

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..15ccee2
--- /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.StandardOpenOption;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.aether.named.support.FileLockNamedLock;
+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 is abstract. This
+ * class does not "know" how to resolve lock names to FS paths.
+ */
+public abstract class FileLockNamedLockFactory
+    extends NamedLockFactorySupport
+{
+    private final ConcurrentHashMap<String, FileChannel> channels;
+
+    public FileLockNamedLockFactory()
+    {
+        this.channels = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Implementations should be able to "resolve" lock name to {@link Path}, this method must <strong>never return
+     * {@code null}!</strong>
+     */
+    protected abstract Path resolveName( final String name );
+
+    @Override
+    protected NamedLockSupport createLock( final String name )
+    {
+        Path path = resolveName( name );
+        FileChannel fileChannel = channels.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( path.toString(), e );
+            }
+        } );
+        return new FileLockNamedLock( name, fileChannel, this );
+    }
+
+    @Override
+    protected void destroyLock( final String name )
+    {
+        try
+        {
+            FileChannel fileChannel = channels.remove( name );
+            if ( fileChannel != null )
+            {
+                try
+                {
+                    fileChannel.close();
+                }
+                catch ( IOException e )
+                {
+                    throw new UncheckedIOException( e );
+                }
+            }
+            else
+            {
+                logger.warn( "No FileChannel for lock '{}'", name );
+            }
+        }
+        finally
+        {
+            super.destroyLock( name );
+        }
+    }
+}
\ No newline at end of file
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..4dc1113
--- /dev/null
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileLockNamedLock.java
@@ -0,0 +1,266 @@
+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.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Named lock that uses {@link FileLock}.
+ */
+public final class FileLockNamedLock
+    extends NamedLockSupport
+{
+    private static final long LOCK_POSITION = 0L;
+
+    private static final long LOCK_SIZE = 1L;
+
+    private final HashMap<Thread, Deque<FileLock>> threadSteps;
+
+    private final FileChannel fileChannel;
+
+    private final ReentrantLock fairLock;
+
+    public FileLockNamedLock( final String name,
+                              final FileChannel fileChannel,
+                              final NamedLockFactorySupport factory )
+    {
+        super( name, factory );
+        this.threadSteps = new HashMap<>();
+        this.fileChannel = fileChannel;
+        this.fairLock = new ReentrantLock( true );
+    }
+
+    @Override
+    public boolean lockShared( final long time, final TimeUnit unit )
+    {
+        fairLock.lock();
+        try
+        {
+            Deque<FileLock> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
+            if ( !steps.isEmpty() )
+            { // we already own shared or exclusive lock
+                logger.trace( "{} steps not empty: lock assumed", name() );
+                steps.push( dummyLock( true ) );
+                return true;
+            }
+            if ( threadSteps.size() > 1 )
+            { // we may succeed (w/o locking file as JVM already hold lock) if any other thread does not have exclusive
+                boolean noOtherThreadExclusive = threadSteps.values().stream()
+                                                            .flatMap( Collection::stream )
+                                                            .allMatch( FileLock::isShared );
+                logger.trace( "{} other threads hold it: can lock = {}", name(), noOtherThreadExclusive );
+                if ( noOtherThreadExclusive )
+                {
+                    steps.push( dummyLock( true ) );
+                }
+                return noOtherThreadExclusive;
+            }
+
+            logger.trace( "{} steps empty: getting real shared lock", name() );
+            FileLock fileLock = realLock( true, unit.toNanos( time ) );
+            if ( fileLock != null )
+            {
+                steps.push( fileLock );
+                return true;
+            }
+            return false;
+        }
+        finally
+        {
+            fairLock.unlock();
+        }
+    }
+
+    @Override
+    public boolean lockExclusively( final long time, final TimeUnit unit )
+    {
+        fairLock.lock();
+        try
+        {
+            Deque<FileLock> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
+            if ( !steps.isEmpty() )
+            { // we already own shared or exclusive lock
+                if ( steps.stream().anyMatch( l -> !l.isShared() ) )
+                {
+                    logger.trace( "{} steps not empty, has exclusive lock: lock assumed", name() );
+                    steps.push( dummyLock( false ) );
+                    return true;
+                }
+                else
+                {
+                    logger.trace( "{} steps not empty, has not exclusive lock: lock-upgrade not supported", name() );
+                    return false; // Lock upgrade not supported
+                }
+            }
+            if ( threadSteps.size() > 1 )
+            { // some other thread already posses lock, we want exclusive -> fail
+                logger.trace( "{} other threads hold it: cannot lock exclusively", name() );
+                return false;
+            }
+
+            logger.trace( "{} steps empty: getting real exclusive lock", name() );
+            FileLock fileLock = realLock( false, unit.toNanos( time ) );
+            if ( fileLock != null )
+            {
+                steps.push( fileLock );
+                return true;
+            }
+            return false;
+        }
+        finally
+        {
+            fairLock.unlock();
+        }
+    }
+
+    @Override
+    public void unlock()
+    {
+        fairLock.lock();
+        try
+        {
+            Deque<FileLock> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
+            if ( steps.isEmpty() )
+            {
+                throw new IllegalStateException( "Wrong API usage: unlock without lock" );
+            }
+            try
+            {
+                steps.pop().release();
+            }
+            catch ( IOException e )
+            {
+                throw new UncheckedIOException( e );
+            }
+        }
+        finally
+        {
+            fairLock.unlock();
+        }
+    }
+
+    private FileLock realLock( final boolean shared, final long maxWaitNanos )
+    {
+        boolean interrupted = false;
+        long now = System.nanoTime();
+        final long barrier = now + maxWaitNanos;
+        FileLock result = null;
+        int attempt = 1;
+        try
+        {
+            while ( now < barrier && result == null )
+            {
+                try
+                {
+                    result = fileChannel.tryLock( LOCK_POSITION, LOCK_SIZE, shared );
+                    if ( result == null )
+                    {
+                        logger.trace( "Attempt {}: Interrupted {} while on {}",
+                                attempt, Thread.currentThread().getName(), name() );
+                        interrupted = Thread.interrupted();
+                        break;
+                    }
+                }
+                catch ( OverlappingFileLockException e )
+                {
+                    logger.trace( "Attempt {}: Overlap on {}, sleeping", attempt, name() );
+                    try
+                    {
+                        Thread.sleep( 100 );
+                    }
+                    catch ( InterruptedException intEx )
+                    {
+                        interrupted = true;
+                        break;
+                    }
+                }
+                catch ( IOException e )
+                {
+                    if ( e.getMessage().toLowerCase( Locale.ENGLISH ).contains( "deadlock" ) )
+                    {
+                        logger.trace( "Attempt {}: Deadlock on {}, sleeping", attempt, name() );
+                        try
+                        {
+                            Thread.sleep( 100 );
+                        }
+                        catch ( InterruptedException intEx )
+                        {
+                            interrupted = true;
+                            break;
+                        }
+                    }
+                    else
+                    {
+                        logger.trace( "Attempt {}: Failure on {}", attempt, name(), e );
+                        throw new UncheckedIOException( e );
+                    }
+                }
+                now = System.nanoTime();
+                attempt++;
+            }
+        }
+        finally
+        {
+            if ( interrupted )
+            {
+                logger.trace( "Interrupted thread {} while in lock {}", Thread.currentThread().getName(), name() );
+                Thread.currentThread().interrupt();
+            }
+
+        }
+        return result;
+    }
+
+    private FileLock dummyLock( final boolean shared )
+    {
+        return new DummyLock( fileChannel, shared );
+    }
+
+    private static final class DummyLock extends FileLock
+    {
+        private DummyLock( final FileChannel channel, final boolean shared )
+        {
+            super( channel, LOCK_POSITION, LOCK_SIZE, shared );
+        }
+
+        @Override
+        public boolean isValid()
+        {
+            return channel().isOpen();
+        }
+
+        @Override
+        public void release() throws IOException
+        {
+            // noop
+        }
+    }
+}
\ No newline at end of file
diff --git a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactoryTest.java b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactoryTest.java
new file mode 100644
index 0000000..e834821
--- /dev/null
+++ b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactoryTest.java
@@ -0,0 +1,46 @@
+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 FileLockNamedLockFactoryTest
+    extends NamedLockFactoryTestSupport {
+
+    @BeforeClass
+    public static void createNamedLockFactory() 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
+
+        Path baseDir = Files.createTempDirectory( null );
+        namedLockFactory = new FileLockNamedLockFactory() {
+            @Override
+            protected Path resolveName(final String name) {
+                return baseDir.resolve( name );
+            }
+        };
+    }
+}