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 ) );
+ }
+}