Skip to content

Commit 1b8ce7f

Browse files
authored
[MRESOLVER-372] Rework the FileUtils collocated temp file (#365)
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. --- https://issues.apache.org/jira/browse/MRESOLVER-372 Backport to 1.9.x branch of the #364
1 parent 1de8710 commit 1b8ce7f

File tree

1 file changed

+72
-3
lines changed
  • maven-resolver-util/src/main/java/org/eclipse/aether/util

1 file changed

+72
-3
lines changed

maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@
2020

2121
import java.io.Closeable;
2222
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.OutputStream;
25+
import java.nio.ByteBuffer;
26+
import java.nio.channels.FileChannel;
2327
import java.nio.file.Files;
2428
import java.nio.file.Path;
2529
import java.nio.file.StandardCopyOption;
30+
import java.nio.file.StandardOpenOption;
2631
import java.util.concurrent.ThreadLocalRandom;
32+
import java.util.concurrent.atomic.AtomicBoolean;
2733

2834
import static java.util.Objects.requireNonNull;
2935

@@ -33,6 +39,10 @@
3339
* @since 1.9.0
3440
*/
3541
public final class FileUtils {
42+
// Logic borrowed from Commons-Lang3: we really need only this, to decide do we fsync on directories or not
43+
private static final boolean IS_WINDOWS =
44+
System.getProperty("os.name", "unknown").startsWith("Windows");
45+
3646
private FileUtils() {
3747
// hide constructor
3848
}
@@ -52,7 +62,10 @@ public interface TempFile extends Closeable {
5262
*/
5363
public interface CollocatedTempFile extends TempFile {
5464
/**
55-
* Atomically moves temp file to target file it is collocated with.
65+
* Upon close, atomically moves temp file to target file it is collocated with overwriting target (if exists).
66+
* Invocation of this method merely signals that caller ultimately wants temp file to replace the target
67+
* file, but when this method returns, the move operation did not yet happen, it will happen when this
68+
* instance is closed.
5669
*/
5770
void move() throws IOException;
5871
}
@@ -98,23 +111,79 @@ public static CollocatedTempFile newTempFile(Path file) throws IOException {
98111
Path tempFile = parent.resolve(file.getFileName() + "."
99112
+ Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
100113
return new CollocatedTempFile() {
114+
private final AtomicBoolean wantsMove = new AtomicBoolean(false);
115+
101116
@Override
102117
public Path getPath() {
103118
return tempFile;
104119
}
105120

106121
@Override
107-
public void move() throws IOException {
108-
Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE);
122+
public void move() {
123+
wantsMove.set(true);
109124
}
110125

111126
@Override
112127
public void close() throws IOException {
128+
if (wantsMove.get() && Files.isReadable(tempFile)) {
129+
if (IS_WINDOWS) {
130+
copy(tempFile, file);
131+
} else {
132+
fsyncFile(tempFile);
133+
Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE);
134+
fsyncParent(tempFile);
135+
}
136+
}
113137
Files.deleteIfExists(tempFile);
114138
}
115139
};
116140
}
117141

142+
/**
143+
* On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
144+
*/
145+
private static void copy(Path source, Path target) throws IOException {
146+
ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
147+
byte[] array = buffer.array();
148+
try (InputStream is = Files.newInputStream(source);
149+
OutputStream os = Files.newOutputStream(target)) {
150+
while (true) {
151+
int bytes = is.read(array);
152+
if (bytes < 0) {
153+
break;
154+
}
155+
os.write(array, 0, bytes);
156+
}
157+
}
158+
}
159+
160+
/**
161+
* Performs fsync: makes sure no OS "dirty buffers" exist for given file.
162+
*
163+
* @param target Path that must not be {@code null}, must exist as plain file.
164+
*/
165+
private static void fsyncFile(Path target) throws IOException {
166+
try (FileChannel file = FileChannel.open(target, StandardOpenOption.WRITE)) {
167+
file.force(true);
168+
}
169+
}
170+
171+
/**
172+
* Performs directory fsync: not usable on Windows, but some other OSes may also throw, hence thrown IO exception
173+
* is just ignored.
174+
*
175+
* @param target Path that must not be {@code null}, must exist as plain file, and must have parent.
176+
*/
177+
private static void fsyncParent(Path target) throws IOException {
178+
try (FileChannel parent = FileChannel.open(target.getParent(), StandardOpenOption.READ)) {
179+
try {
180+
parent.force(true);
181+
} catch (IOException e) {
182+
// ignore
183+
}
184+
}
185+
}
186+
118187
/**
119188
* A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
120189
* hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent

0 commit comments

Comments
 (0)