如何可靠地检测到目录的原子移动失败,因为目标已经存在

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

在服务器中,我在首次访问资源时初始化资源的工作目录。可能存在对服务器多个进程处理的资源的并行请求,这意味着我需要注意 none 个进程看到部分初始化的工作目录。解决方案是在一个临时的同级目录中初始化工作目录,然后使用 Files.moveStandardCopyOption.ATOMIC_MOVE.

将其移动到最终位置

如果两个进程同时初始化工作目录,则第二次原子移动失败。这不是真正的问题,因为工作目录被正确初始化,所以排在第二位的进程只需要丢弃它创建的临时目录并继续。

我尝试使用以下代码执行此操作:

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);
        }
    }
}

但是我注意到,仅抓住 FileAlreadyExistsException 似乎还不够。如果发生移动碰撞,还会抛出其他异常。我不只是想捕获所有异常,因为这可能隐藏了真正的问题。

所以没有办法从 Files.move 抛出的异常中可靠地检测到由于目标目录已经存在而导致目录的原子移动失败吗?

通过观察异常并查看 Windows 和 Linux 的 FileSystemProvider 实现,我们发现了以下内容:

  • 在 Windows 上,尝试将目录移动到现有目录时抛出 AccessDeniedException
  • 在 Unix(及其亲戚)上,会抛出一个通用的 FileSystemException,并附带一条对应于 errno.h constant ENOTEMPTY.
  • 的消息

针对这些实现细节进行编程时,可以合理地很好地检测目录移动冲突,而不会隐藏其他问题的太多危险。

以下代码实现了一个原子移动,在移动冲突的情况下总是抛出 FileAlreadyExistsException

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;
            }
        }
    }
}