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 <ta...@cservenak.net> 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); + } }