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 dfdb9476 [MRESOLVER-372] Rework the FileUtils collocated temp file 
(#364)
dfdb9476 is described below

commit dfdb94765637ddfb4d403b371dbc4b80b0e4bc1a
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Fri Nov 17 13:26:54 2023 +0100

    [MRESOLVER-372] Rework the FileUtils collocated temp file (#364)
    
    Fixes:
    * move() call should NOT perform the move, as writer stream to tmp file may 
still be open
    * move the file move logic to close, make it happen only when closing 
collocated temp file
    * perform fsync before atomic move to ensure there is no OS dirty buffers 
related to newly written file
    * on windows go with old code that for some reason works (avoid NIO2)
    * on non-Win OS fsync the parent directory as well.
    
    So, to recap:
    * original issue is not related to locking at all, is about change done in 
1.9 where file processor was altered to use nio2 atomic move (to prevent 
possibility of partial download being read by other process and prevent 
incomplete files in case of crash).  Maven LRM uses layout, so randomized temp 
files does not matter (if they remain after crash), as they would merely just 
clutter the local repository but not corrupt it.
    * nuking local repo is part of healthy hygiene anyway
    * seems windows (or java on windows) have issues with atomic fs ops
    * resolver have no deps on p-u or any commons, api and util modules are 
zero dependency jars and should remain as such
    * one thing bugs me still: why does the old code work? Or in reverse, the 
new why does not work? As opposed to "high frequency writes", this is not that 
case (fixed by @olamy ... Um, sorry @michael-o )
    
    To explain this last bullet: there was 
https://issues.apache.org/jira/browse/MRESOLVER-325 with solution 
https://github.com/apache/maven-resolver/pull/259 that "seems similar", as 
originally this code was used by both, and in that case "high frequency atomic 
moves" was applicable. But this now happens seemingly on "install" and "cache" 
(download, place it in local repo) that has way less frequency that tracking 
file write....
    
    ---
    
    https://issues.apache.org/jira/browse/MRESOLVER-372
---
 .../java/org/eclipse/aether/util/FileUtils.java    | 75 +++++++++++++++++++++-
 1 file changed, 72 insertions(+), 3 deletions(-)

diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
index dc260d76..0ec3880b 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
@@ -20,10 +20,16 @@ package org.eclipse.aether.util;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static java.util.Objects.requireNonNull;
 
@@ -33,6 +39,10 @@ import static java.util.Objects.requireNonNull;
  * @since 1.9.0
  */
 public final class FileUtils {
+    // Logic borrowed from Commons-Lang3: we really need only this, to decide 
do we fsync on directories or not
+    private static final boolean IS_WINDOWS =
+            System.getProperty("os.name", "unknown").startsWith("Windows");
+
     private FileUtils() {
         // hide constructor
     }
@@ -52,7 +62,10 @@ public final class FileUtils {
      */
     public interface CollocatedTempFile extends TempFile {
         /**
-         * Atomically moves temp file to target file it is collocated with.
+         * Upon close, atomically moves temp file to target file it is 
collocated with overwriting target (if exists).
+         * Invocation of this method merely signals that caller ultimately 
wants temp file to replace the target
+         * file, but when this method returns, the move operation did not yet 
happen, it will happen when this
+         * instance is closed.
          */
         void move() throws IOException;
     }
@@ -98,23 +111,79 @@ public final class FileUtils {
         Path tempFile = parent.resolve(file.getFileName() + "."
                 + 
Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
         return new CollocatedTempFile() {
+            private final AtomicBoolean wantsMove = new AtomicBoolean(false);
+
             @Override
             public Path getPath() {
                 return tempFile;
             }
 
             @Override
-            public void move() throws IOException {
-                Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE);
+            public void move() {
+                wantsMove.set(true);
             }
 
             @Override
             public void close() throws IOException {
+                if (wantsMove.get() && Files.isReadable(tempFile)) {
+                    if (IS_WINDOWS) {
+                        copy(tempFile, file);
+                    } else {
+                        fsyncFile(tempFile);
+                        Files.move(tempFile, file, 
StandardCopyOption.ATOMIC_MOVE);
+                        fsyncParent(tempFile);
+                    }
+                }
                 Files.deleteIfExists(tempFile);
             }
         };
     }
 
+    /**
+     * On Windows we use pre-NIO2 way to copy files, as for some reason it 
works. Beat me why.
+     */
+    private static void copy(Path source, Path target) throws IOException {
+        ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
+        byte[] array = buffer.array();
+        try (InputStream is = Files.newInputStream(source);
+                OutputStream os = Files.newOutputStream(target)) {
+            while (true) {
+                int bytes = is.read(array);
+                if (bytes < 0) {
+                    break;
+                }
+                os.write(array, 0, bytes);
+            }
+        }
+    }
+
+    /**
+     * Performs fsync: makes sure no OS "dirty buffers" exist for given file.
+     *
+     * @param target Path that must not be {@code null}, must exist as plain 
file.
+     */
+    private static void fsyncFile(Path target) throws IOException {
+        try (FileChannel file = FileChannel.open(target, 
StandardOpenOption.WRITE)) {
+            file.force(true);
+        }
+    }
+
+    /**
+     * Performs directory fsync: not usable on Windows, but some other OSes 
may also throw, hence thrown IO exception
+     * is just ignored.
+     *
+     * @param target Path that must not be {@code null}, must exist as plain 
file, and must have parent.
+     */
+    private static void fsyncParent(Path target) throws IOException {
+        try (FileChannel parent = FileChannel.open(target.getParent(), 
StandardOpenOption.READ)) {
+            try {
+                parent.force(true);
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
     /**
      * A file writer, that accepts a {@link Path} to write some content to. 
Note: the file denoted by path may exist,
      * hence implementation have to ensure it is able to achieve its goal 
("replace existing" option or equivalent

Reply via email to