Search code examples
javawindowsfile-ioatomic

How to create-then-atomically-rename file in Java on Windows?


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.


Solution

  • 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.)