This is an automated email from the ASF dual-hosted git repository.
cstamas 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 7e8ffdba [MRESOLVER-364] Put back file locking in tracking file
manager (#295)
7e8ffdba is described below
commit 7e8ffdbafadf07bf88fc7fb15dddd52725d965dd
Author: Tamas Cservenak <[email protected]>
AuthorDate: Thu Jun 1 15:02:58 2023 +0200
[MRESOLVER-364] Put back file locking in tracking file manager (#295)
This (logically) reverts the fcb6be59c5a5855573886b09c70dab80074a1d27
---
https://issues.apache.org/jira/browse/MRESOLVER-364
---
.../internal/impl/DefaultTrackingFileManager.java | 104 ++++++++++++++++-----
.../impl/DefaultTrackingFileManagerTest.java | 45 +++++++++
2 files changed, 125 insertions(+), 24 deletions(-)
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
index 04ce96e1..f3a89062 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
@@ -21,11 +21,16 @@ package org.eclipse.aether.internal.impl;
import javax.inject.Named;
import javax.inject.Singleton;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.RandomAccessFile;
import java.io.UncheckedIOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -36,6 +41,11 @@ import org.slf4j.LoggerFactory;
/**
* Manages access to a properties file.
+ * <p>
+ * Note: the file locking in this component (that predates {@link
org.eclipse.aether.SyncContext}) is present only
+ * to back off two parallel implementations that coexist in Maven (this class
and {@code maven-compat} one), as in
+ * certain cases the two implementations may collide on properties files. This
locking must remain in place for as long
+ * as {@code maven-compat} code exists.
*/
@Singleton
@Named
@@ -46,13 +56,16 @@ public final class DefaultTrackingFileManager implements
TrackingFileManager {
public Properties read(File file) {
Path filePath = file.toPath();
if (Files.isReadable(filePath)) {
- try (InputStream stream = Files.newInputStream(filePath)) {
- Properties props = new Properties();
- props.load(stream);
- return props;
- } catch (IOException e) {
- LOGGER.warn("Failed to read tracking file '{}'", file, e);
- throw new UncheckedIOException(e);
+ synchronized (getMutex(filePath)) {
+ try (FileInputStream stream = new
FileInputStream(filePath.toFile());
+ FileLock unused = fileLock(stream.getChannel(),
Math.max(1, file.length()), true)) {
+ Properties props = new Properties();
+ props.load(stream);
+ return props;
+ } catch (IOException e) {
+ LOGGER.warn("Failed to read tracking file '{}'", file, e);
+ throw new UncheckedIOException(e);
+ }
}
}
return null;
@@ -70,33 +83,76 @@ public final class DefaultTrackingFileManager implements
TrackingFileManager {
throw new UncheckedIOException(e);
}
- try {
- if (Files.isReadable(filePath)) {
- try (InputStream stream = Files.newInputStream(filePath)) {
- props.load(stream);
+ synchronized (getMutex(filePath)) {
+ try (RandomAccessFile raf = new
RandomAccessFile(filePath.toFile(), "rw");
+ FileLock unused = fileLock(raf.getChannel(), Math.max(1,
raf.length()), false)) {
+ if (raf.length() > 0) {
+ byte[] buffer = new byte[(int) raf.length()];
+ raf.readFully(buffer);
+ props.load(new ByteArrayInputStream(buffer));
}
- }
- for (Map.Entry<String, String> update : updates.entrySet()) {
- if (update.getValue() == null) {
- props.remove(update.getKey());
- } else {
- props.setProperty(update.getKey(), update.getValue());
+ for (Map.Entry<String, String> update : updates.entrySet()) {
+ if (update.getValue() == null) {
+ props.remove(update.getKey());
+ } else {
+ props.setProperty(update.getKey(), update.getValue());
+ }
}
- }
- try (OutputStream stream = Files.newOutputStream(filePath)) {
LOGGER.debug("Writing tracking file '{}'", file);
+ ByteArrayOutputStream stream = new ByteArrayOutputStream(1024
* 2);
props.store(
stream,
"NOTE: This is a Maven Resolver internal
implementation file"
+ ", its format can be changed without prior
notice.");
+ raf.seek(0L);
+ raf.write(stream.toByteArray());
+ raf.setLength(raf.getFilePointer());
+ } catch (IOException e) {
+ LOGGER.warn("Failed to write tracking file '{}'", file, e);
+ throw new UncheckedIOException(e);
}
- } catch (IOException e) {
- LOGGER.warn("Failed to write tracking file '{}'", file, e);
- throw new UncheckedIOException(e);
}
return props;
}
+
+ private Object getMutex(Path file) {
+ /*
+ * NOTE: Locks held by one JVM must not overlap and using the
canonical path is our best bet, still another
+ * piece of code might have locked the same file (unlikely though) or
the canonical path fails to capture file
+ * identity sufficiently as is the case with Java 1.6 and symlinks on
Windows.
+ */
+ try {
+ return file.toRealPath().toString().intern();
+ } catch (IOException e) {
+ LOGGER.warn("Failed to get real path {}", file, e);
+ return file.toAbsolutePath().toString().intern();
+ }
+ }
+
+ @SuppressWarnings({"checkstyle:magicnumber"})
+ private FileLock fileLock(FileChannel channel, long size, boolean shared)
throws IOException {
+ FileLock lock = null;
+ for (int attempts = 8; attempts >= 0; attempts--) {
+ try {
+ lock = channel.lock(0, size, shared);
+ break;
+ } catch (OverlappingFileLockException e) {
+ if (attempts <= 0) {
+ throw new IOException(e);
+ }
+ try {
+ Thread.sleep(50L);
+ } catch (InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ if (lock == null) {
+ throw new IOException("Could not lock file");
+ }
+ return lock;
+ }
}
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManagerTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManagerTest.java
index 22fbea79..f9d9fbe2 100644
---
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManagerTest.java
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManagerTest.java
@@ -20,7 +20,10 @@ package org.eclipse.aether.internal.impl;
import java.io.File;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -97,4 +100,46 @@ public class DefaultTrackingFileManagerTest {
assertTrue("Leaked file: " + propFile, propFile.delete());
}
}
+
+ @Test
+ public void testLockingOnCanonicalPath() throws Exception {
+ final TrackingFileManager tfm = new DefaultTrackingFileManager();
+
+ final File propFile =
TestFileUtils.createTempFile("#COMMENT\nkey1=value1\nkey2 : value2");
+
+ final List<Throwable> errors = Collections.synchronizedList(new
ArrayList<>());
+
+ Thread[] threads = new Thread[4];
+ for (int i = 0; i < threads.length; i++) {
+ String path = propFile.getParent();
+ for (int j = 0; j < i; j++) {
+ path += "/.";
+ }
+ path += "/" + propFile.getName();
+ final File file = new File(path);
+
+ threads[i] = new Thread(() -> {
+ try {
+ for (int i1 = 0; i1 < 1000; i1++) {
+ assertNotNull(tfm.read(file));
+ HashMap<String, String> update = new HashMap<>();
+ update.put("wasHere", Thread.currentThread().getName()
+ "-" + i1);
+ tfm.update(file, update);
+ }
+ } catch (Throwable e) {
+ errors.add(e);
+ }
+ });
+ }
+
+ for (Thread thread1 : threads) {
+ thread1.start();
+ }
+
+ for (Thread thread : threads) {
+ thread.join();
+ }
+
+ assertEquals(Collections.emptyList(), errors);
+ }
}