42

I have a cluster of machines, each running a Java app.

These Java apps need to access a unique resource.txt file concurrently.

I need to atomically rename a temp.txt file to resource.txt in Java, even if resource.txt already exist.

Deleting resource.txt and renaming temp.txt doesn't work, as it's not atomic (it creates a small timeframe where resource.txt doesn't exist).

And it should be cross-platform...

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Sébastien RoccaSerra
  • 16,731
  • 8
  • 50
  • 54
  • 1
    Note to self, 8 years later: I don't remember the context, but if the *problem* requires cross-platform atomicity, maybe the *solution* should not involve a file? – Sébastien RoccaSerra Feb 12 '18 at 09:32

8 Answers8

45

For Java 1.7+, use java.nio.file.Files.move(Path source, Path target, CopyOption... options) with CopyOptions "REPLACE_EXISTING" and "ATOMIC_MOVE".

See API documentation for more information.

For example:

Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
Eirik W
  • 3,086
  • 26
  • 29
  • 21
    If you specify ATOMIC_MOVE, then "all other options are ignored" and "if the target file exists then it is implementation specific if the existing file is replaced or this method fails by throwing an IOException". However, I've tested passing ATOMIC_MOVE on Windows 7, Solaris 10 and RHEL Server 6.3 and they all perform the rename atomically, replacing the destination file. – Martin McNulty Nov 28 '12 at 15:00
  • 3
    I should mention that I only tested renaming files (not directories) within the same directory (not cross-filesystem) on local (not networked) filesystems. That's enough for my purposes, but YMMV. – Martin McNulty Nov 28 '12 at 15:04
14

On Linux (and I believe Solaris and other UNIX operating systems), Java's File.renameTo() method will overwrite the destination file if it exists, but this is not the case under Windows.

To be cross platform, I think you'd have to use file locking on resource.txt and then overwrite the data.

The behavior of the file lock is platform-dependent. On some platforms, the file lock is advisory, which means that unless an application checks for a file lock, it will not be prevented from accessing the file. On other platforms, the file lock is mandatory, which means that a file lock prevents any application from accessing the file.

try {
    // Get a file channel for the file
    File file = new File("filename");
    FileChannel channel = new RandomAccessFile(file, "rw").getChannel();

    // Use the file channel to create a lock on the file.
    // This method blocks until it can retrieve the lock.
    FileLock lock = channel.lock();

    // Try acquiring the lock without blocking. This method returns
    // null or throws an exception if the file is already locked.
    try {
        lock = channel.tryLock();
    } catch (OverlappingFileLockException e) {
        // File is already locked in this thread or virtual machine
    }

    // Release the lock
    lock.release();

    // Close the file
    channel.close();
} catch (Exception e) {
}

Linux, by default, uses voluntary locking, while Windows enforces it. Maybe you could detect the OS, and use renameTo() under UNIX with some locking code for Windows?

There's also a way to turn on mandatory locking under Linux for specific files, but it's kind of obscure. You have to set the mode bits just right.

Linux, following System V (see System V Interface Definition (SVID) Version 3), lets the sgid bit for files without group execute permission mark the file for mandatory locking

Grodriguez
  • 21,501
  • 10
  • 63
  • 107
Stephen
  • 4,176
  • 2
  • 24
  • 29
  • this should be marked as deprecated, as the new way should be java.nio! – user1052080 Sep 24 '13 at 13:33
  • This only works if the file is so small that it can be overwritten in one write operation. – Thomas Mueller Jul 21 '14 at 09:17
  • @user1052080 Unfortunately NIO only goes partway to solving this problem. NIO doesn't support sync of file operations other than writing from a stream. This is sometimes a critical issue. – Leliel Jun 02 '16 at 03:24
7

Here is a discussion that relates: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4017593

TofuBeer
  • 60,850
  • 18
  • 118
  • 163
3

As stated here, it looks like the Windows OS doesn't even support atomic file rename for older versions. It's very likely you have to use some manual locking mechanisms or some kind of transactions. For that, you might want to take a look into the apache commons transaction package.

vlp
  • 7,811
  • 2
  • 23
  • 51
MicSim
  • 26,265
  • 16
  • 90
  • 133
  • 1
    Commons Transaction buyer beware (see project web page): We have decided to move the project to dormant as we are convinced that the main advertised feature transactional file access can not be implemented reliably. We are convinced that no such implementation can be possible on top of an ordinary file system. Although there are other useful parts (as multi level locking including deadlock detection) the transactional file system is the main reason people use this library for. As it simply can not be made fully transactional, it does not work as advertised. – vlp Apr 03 '19 at 14:31
1

You might get some traction by establishing a filechannel lock on the file before renaming it (and deleting the file you're going to overwrite once you have the lock). -r

rogerdpack
  • 62,887
  • 36
  • 269
  • 388
1

If this should be cross-platform I suggest 2 options:

  1. Implement an intermediate service that is responsible for all the file accesses. Here you can use several mechanisms for synchronizing the requests. Each client java app accesses the file only through this service.
  2. Create a control file each time you need to perform synchronized operations. Each java app that accesses the file is responsible checking for the control file and waiting while this control file exists. (almost like a semaphore). The process doing the delete/rename operation is responsible for creating/deleting the control file.
bruno conde
  • 47,767
  • 15
  • 98
  • 117
  • What about the user deleting the file by hand outside of Java? You cannot control that all file access goes through the Java service. – TofuBeer Feb 27 '09 at 17:34
  • Please remember that using a control file is not sufficient to implement safe file locking! – user3151902 Sep 28 '15 at 06:15
1

If the purpose of the rename is to replace resource.txt on the fly and you have control over all the programs involved, and the frequency of replacement is not high, you could do the following.

To open/read the file:

  1. Open "resource.txt", if that fails
  2. Open "resource.old.txt", if that fails
  3. Open "resource.txt" again, if that fails
  4. You have an error condition.

To replace the file:

  1. Rename "resource.txt" to "resource.old.txt", then
  2. Rename "resource.new.txt" to "resource.txt", then
  3. Delete "resource.old.txt".

Which will ensure all your readers always find a valid file.

But, easier, would be to simply try your opening in a loop, like:

InputStream inp=null;
StopWatch   tmr=new StopWatch();                     // made up class, not std Java
IOException err=null;

while(inp==null && tmr.elapsed()<5000) {             // or some approp. length of time
    try { inp=new FileInputStream("resource.txt"); }
    catch(IOException thr) { err=thr; sleep(100); }  // or some approp. length of time
    }

if(inp==null) {
     // handle error here - file did not turn up after required elapsed time
     throw new IOException("Could not obtain data from resource.txt file");
     }

... carry on
Lawrence Dol
  • 63,018
  • 25
  • 139
  • 189
0

I solve with a simple rename function.

Calling :

File newPath = new File("...");
newPath = checkName(newPath);
Files.copy(file.toPath(), newPath.toPath(), StandardCopyOption.REPLACE_EXISTING);

The checkName function checks if exits. If exits then concat a number between two bracket (1) to the end of the filename. Functions:

private static File checkName(File newPath) {
    if (Files.exists(newPath.toPath())) {

        String extractRegExSubStr = extractRegExSubStr(newPath.getName(), "\\([0-9]+\\)");
        if (extractRegExSubStr != null) {
            extractRegExSubStr = extractRegExSubStr.replaceAll("\\(|\\)", "");
            int parseInt = Integer.parseInt(extractRegExSubStr);
            int parseIntPLus = parseInt + 1;

            newPath = new File(newPath.getAbsolutePath().replace("(" + parseInt + ")", "(" + parseIntPLus + ")"));
            return checkName(newPath);
        } else {
            newPath = new File(newPath.getAbsolutePath().replace(".pdf", " (" + 1 + ").pdf"));
            return checkName(newPath);
        }

    }
    return newPath;

}

private static String extractRegExSubStr(String row, String patternStr) {
    Pattern pattern = Pattern.compile(patternStr);
    Matcher matcher = pattern.matcher(row);
    if (matcher.find()) {
        return matcher.group(0);
    }
    return null;
}

EDIT: Its only works for pdf. If you want other please replace the .pdf or create an extension paramter for it. NOTE: If the file contains additional numbers between brackets '(' then it may mess up your file names.

SüniÚr
  • 826
  • 1
  • 16
  • 33