How to reliably detect that an atomic move of a directory failed because the target already exists

1.1k views Asked by At

In a server, I am initializing the working directory for a resource on first access to the resource. There may be parallel requests for the resource handled by multiple processes of the server, which means that I need to take care that none of the processes sees a partially initialized working directory. The solution for this is initialize the working directories in a temporary, sibling directory and then move it to its final location with Files.move and the StandardCopyOption.ATOMIC_MOVE.

In case two processes initialize a working directory at the same time, the second atomic move fails. This is not really a problem because then the working directory is initialized correctly, so the process that came in second only needs to discard the temporary directory it created and continue.

I tried to do this with the following code:

private void initalizeWorkDirectory(final Resource resource) throws IOException {
    File workDir = resource.getWorkDirectory();
    if (!workDir.exists()) {
        File tempDir = createTemporarySibligDirectory(workDir);
        try {
            fillWorkDirectory(tempDir, resource);
            Files.move(tempDir.toPath(), workDir.toPath(), StandardCopyOption.ATOMIC_MOVE);
        } catch (FileAlreadyExistsException e) {
            // do some logging
        } finally {
            FileUtils.deleteQuietly(tempDir);
        }
    }
}

However I noticed, that only catching the FileAlreadyExistsException doesn't seem enough. In case of a move collision, there are also other exceptions thrown. I don't just want to catch all exceptions because this could be hiding real problems.

So isn't there a way to reliably detect from the exception thrown by Files.move that an atomic move of a directory failed because the target directory already existed?

1

There are 1 answers

1
oberlies On BEST ANSWER

From observing the exceptions and by looking at the FileSystemProvider implementations for Windows and Linux, we found out the following:

  • On Windows, an AccessDeniedException is thrown when trying to move a directory to an existing directory.
  • On Unix (and its relatives), a generic FileSystemException is thrown with a message that corresponds to the errno.h constant ENOTEMPTY.

When programming against these implementation details, one can reasonably well detect directory move collisions without too much danger of hiding other problems.

The following code implements an atomic move which always throws a FileAlreadyExistsException in case of a move collision:

import java.io.File;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

public class AtomicMove {
    private static final String ENOTEMPTY = "Directory not empty";

    public static void move(final File source, final File target) throws FileAlreadyExistsException, IOException {
        try {
            Files.move(source.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE);

        } catch (AccessDeniedException e) {
            // directory move collision on Windows
            throw new FileAlreadyExistsException(source.toString(), target.toString(), e.getMessage());

        } catch (FileSystemException e) {
            if (ENOTEMPTY.equals(e.getReason())) {
                // directory move collision on Unix
                throw new FileAlreadyExistsException(source.toString(), target.toString(), e.getMessage());
            } else {
                // other problem
                throw e;
            }
        }
    }
}