7

I am trying to implement "write temporary file and rename" using Java on Windows correctly.

How to atomically rename a file in Java, even if the dest file already exists? suggests renaming files is "atomic operation" (whatever "atomic" actually means). https://stackoverflow.com/a/20570968/65458 suggests writing tmp file and renaming is cross-platform and ensures final file either does not exist or can be processed by the other process.

So I tried to actually implement this approach. Below is the summary of my attempts. For the actual question -- jump to the bottom.

write methods

I tried various ways of writing and renaming file (content and charset are String and Charset respectively):

Using java.nio.file.Files:

Files.copy(new ByteArrayInputStream(content.getBytes(charset)), tmpFile);
Files.move(tmpFile, finalFile, StandardCopyOption.ATOMIC_MOVE);

Using Guava (14) and java.io.File:

com.google.common.io.Files.write(content, tmpFile, charset);
tmpFile.renameTo(finalFile);

Or even more obscure approaches:

try (OutputStream os = new FileOutputStream(tmpFile);
        Writer writer = new OutputStreamWriter(os, charset)) {
    writer.write(content);
}
Runtime.getRuntime().exec(
        new String[] { "cmd.exe", "/C", "move " + tmpFile + " " + finalFile }).waitFor();

read methods

Now assume another thread (thread because I'm in tests, in real-life it could be another process) is executing one of the following versions of code:

With common function:

void waitUntilExists() throws InterruptedException {
    while (!java.nio.file.Files.exists(finalFile)) {
        NANOSECONDS.sleep(1);
    }
}

Using java.nio.file.Files:

waitUntilExists();
return new String(Files.readAllBytes(finalFile), charset);

Using Guava (14):

waitUntilExists();
return new String(com.google.common.io.Files.toByteArray(finalFile.toFile()), charset);

Or even more obscure approaches:

waitUntilExists();
StringBuilder sb = new StringBuilder();
try (InputStream is = new FileInputStream(finalFile.toFile())) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = is.read(buf)) > 0) {
        sb.append(new String(buf, 0, n, charset));
    }
}
return sb.toString();

Results

If I read using using "java.nio.file.Files approach", everything is working fine.

If I run this code on Linux (out of scope of this question, I know), everything is working fine.

However, if i implement read with Guava or FileInputStream, then with likelihood above 0.5% (0.005) the test fails with

java.io.FileNotFoundException: Process cannot access the file, because it is being used by another process

(Message translated by myself cause my windows is not English; Referring to "another process" is misleading, since it is normal for Windows to tell this even if this is the same process, a I verified with explicit blocking.)

Question

How to implement create-then-rename using Java on Windows so that final file appears atomically, i.e. either does not exist or can be read?

As I do have control over processes than will pick up the files, I cannot assume any particular reading method in use, or even that they are in Java. Therefore the solution should work with all read methods listed above.

Community
  • 1
  • 1
Piotr Findeisen
  • 19,480
  • 2
  • 52
  • 82
  • For which way of `write` the `read` (Guava and FileInputStream) fails and succeed (NIO)? – SubOptimal Apr 29 '15 at 07:31
  • When `read` is implemented with Guava or `FileInputStream`, it randomly fails regardless of `write` method used. When `read` is implemented with NIO, it always succeeds, regardless of `write` method used. (Actually I think the different `write` methods may not be really different under the hood, as it all boils down to file rename. Or -- there can be different rename methods in Windows API?) – Piotr Findeisen Apr 29 '15 at 20:29
  • Do you mean independent from which way of renaming you use, NIO read is always sucessful and Guave/FileInputStream are failing from time to time? – SubOptimal Apr 29 '15 at 21:01
  • Exactly. But I cannot just say "let's read with NIO", since I control writer only. – Piotr Findeisen Apr 30 '15 at 04:30
  • Your atomic write implementations look correct. That's probably as good as you can get from Java. Your readers may just have to catch the errors and retry. – Andrew Janke Apr 30 '15 at 05:22
  • The Guava file stuff is implemented on top of `java.io.FileInputStream` and the other `java.io` classes, so it's likely to have the same behavior. – Andrew Janke Apr 30 '15 at 05:22
  • @AndrewJanke, yes Guava is just `FileInput/OutputStream`, but I could miss some detail... BTW any clue why *reading* with NIO behaves different? Is it using different windows API functions, different parameters or what? – Piotr Findeisen Apr 30 '15 at 08:21
  • I'd guess it's because NIO is newer and designed for higher performance, so it may be using newer Windows APIs/options, using them better, or willing to do more system-specific work at the expense of more platform-divergent behavior. The `java.io` stuff is 20 years old, and at the time the Java devs seemed to prioritize consistency across platforms over power and performance on any single platform. (e.g. `strictfp` used to be the default-and only-mode.) Now they're leaning a bit the other way, and `java.io` is stuck with back-compatibility commitments. – Andrew Janke Apr 30 '15 at 09:01
  • 1
    Might be interesting to stick both methods under `procmon` to see what system calls they're each doing and how long they're taking. That'd give you an idea as to whether they're functionally different, one's just faster, or what. – Andrew Janke Apr 30 '15 at 09:04
  • Probably unrelated, but: I have had to call System.gc() (Instantly perform garbage collection.) to free a file, but I was using several libraries. The lib opened and read and closed the file. But I could then not move the file (Same thread.) because it was supposedly still accessed elsewhere. Calling System.gc() "reliably" (Don't know of any failures.) solved this. This is super wicked and shouldn't happen - I assume the library is doing something wrong somehow. Windows 10, Oracle Java 8 (something around u70 back then). – Dreamspace President Nov 13 '17 at 10:25
  • @DreamspacePresident if you need to `System.gc` to close file, then you have file handle leak (missing `.close()` call). This is, however, hardly related to this question :) – Piotr Findeisen Nov 13 '17 at 11:08

2 Answers2

0

This seems to be just how Windows/NTFS behaves.

Moreover, the behavioral difference between reads using old IO and NIO may be because they use different Windows APIs.

Wikipedia on File locking says

For applications that use the file read/write APIs in Windows, byte-range locks are enforced (also referred to as mandatory locks) by the file systems that execute within Windows. For applications that use the file mapping APIs in Windows, byte-range locks are not enforced (also referred to as advisory locks.)

While Wikipedia isn't Windows's docs, this still sheds some light.

(I put this answer only so that others thinking the same don't have to write this. True answers, with references to docs or reported bugs, very much appreciated.)

Piotr Findeisen
  • 19,480
  • 2
  • 52
  • 82
  • 1
    Windows-specific discussion here: http://stackoverflow.com/questions/167414/is-an-atomic-file-rename-with-overwrite-possible-on-windows – Andrew Janke Apr 30 '15 at 05:20
  • 1
    Maybe following link about [TxF - Transactional NTFS](https://msdn.microsoft.com/en-us/library/hh802690%28v=vs.85%29.aspx) could prove your assumption that the behavior is NTFS specific. – SubOptimal Apr 30 '15 at 06:12
  • 1
    Maybe this one too [discussion in news group golang-nuts](https://groups.google.com/d/msg/golang-nuts/ZjRWB8bMhv4/BbTJCgfluegJ) – SubOptimal Apr 30 '15 at 06:18
  • @AndrewJanke, I saw the link before, but I don't understand. Isn't that more about rename-with-overwrite? – Piotr Findeisen Apr 30 '15 at 08:14
  • @SubOptimal, the link is quite interesting. Is Java using `ReplaceFile` too? Maybe there is a problem in what "atomicity" is meant to mean -- is it atomic as in ACID (all or nothing is written) or atomic as in concurrency (no intermediate state is to be observed by others). – Piotr Findeisen Apr 30 '15 at 08:19
  • @PiotrFindeisen Sorry this I don't know. Before your question I was not even aware of this strange behavior on NTFS. :-) But it's interesting stuff. So if I find some further informations for sure I will post them here. – SubOptimal Apr 30 '15 at 08:37
  • I think the "with overwrite" is more of a side issue. It's discussing whether the file renaming – that is, the "file metadata update" – is atomic, and the consensus seems to be that it is not, because (base) NTFS does not have support for it. – Andrew Janke Apr 30 '15 at 08:57
  • 1
    If you want to get a look at what the JVM is doing, you could run your test programs under [Procmon](https://technet.microsoft.com/en-us/library/bb896645.aspx) and it'll tell you what system calls it's making. But that only tells you what this particular JVM implementation is doing, not necessarily what the API spec guarantees. `ReplaceFile` might be too high level to show up itself, but you still might get some interesting info. – Andrew Janke Apr 30 '15 at 08:57
0

There is bug report for java.io.File.renameTo() function in JDK which is not atomic on Windows which has been closed with Won't fix: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4017593. So there is probably no clean method to fix your problem.

x_rex
  • 166
  • 1
  • 3
  • 1
    That bug covers "not atomic rename" problem when target file already exists. My question is a about "atomic rename" in the simpler situation, when target file is known not to exist yet. – Piotr Findeisen Feb 12 '16 at 19:46