Как гарантировать атомарное перемещение или исключение файла в Java?

В одном из моих проектов у меня есть одновременный доступ на запись к одному файлу в пределах одной JRE, и я хочу обработать его, сначала записав во временный файл, а затем переместив этот временный файл к цели, используя атомарное перемещение. Мне не важен порядок доступа для записи или тому подобное, все, что мне нужно, это гарантировать, что в любой момент времени можно использовать один файл. Я уже осведомлен о Files.move и так далее, моя проблема в том, что я посмотрел хотя бы одну реализацию для этого метода, и это вызвало некоторые сомнения относительно того, действительно ли реализации гарантируют атомарные перемещения. Пожалуйста, посмотрите на следующий код:

Files.move на GrepCode для OpenJDK

1342        FileSystemProvider provider = provider(source);
1343        if (provider(target) == provider) {
1344            // same provider
1345            provider.move(source, target, options);
1346        } else {
1347            // different providers
1348            CopyMoveHelper.moveToForeignTarget(source, target, options);
1349        }

Проблема в том, что опция ATOMIC_MOVE учитывается не во всех случаях, но местоположение исходного и целевого пути - единственное, что имеет значение в первую очередь. Это не то, что я хочу, и как я понимаю документацию:

Если перемещение не может быть выполнено как операция атомарной файловой системы, то генерируется исключение AtomicMoveNotSupportedException. Это может возникнуть, например, когда целевое местоположение находится в другом хранилище файлов и потребует копирования файла или если целевое местоположение связано с другим поставщиком для этого объекта.

Приведенный выше код явно нарушает эту документацию, поскольку он использует стратегию копирования-удаления, не распознавая ATOMIC_MOVE вообще. Исключение было бы вполне приемлемым в моем случае, потому что с этим хостер нашего сервиса мог изменить свою настройку, чтобы использовать только одну файловую систему, которая поддерживает атомарные перемещения, как это то, что мы ожидаем в системных требованиях в любом случае. То, с чем я не хочу иметь дело, - это то, что вещи молча терпят неудачу только потому, что реализация использует стратегию копирования-удаления, которая может привести к повреждению данных в целевом файле. Итак, насколько я понимаю, просто не безопасно полагаться на Files.move для атомарных операций, потому что не всегда происходит сбой, если они не поддерживаются, но реализации могут прибегать к стратегии копирования-удаления.

Является ли такое поведение ошибкой в ​​реализации, и его необходимо зарегистрировать или документация допускает такое поведение, и я понимаю, что это неправильно? Имеет ли это какое-то значение, если я уже знаю, что такие, возможно, испорченные реализации используются там? В этом случае мне нужно было бы самостоятельно синхронизировать доступ на запись...

3 ответа

Решение

Вы смотрите не в том месте. Если поставщики файловой системы не совпадают, операция будет делегирована moveToForeignTarget как вы видели в фрагменте кода, который вы опубликовали. Метод moveToForeignTarget Однако будет использовать метод convertMoveToCopyOptions (обратите внимание на говорящее имя…) для получения необходимых параметров копирования для переведенной операции. А также convertMoveToCopyOptions бросит AtomicMoveNotSupportedException если он сталкивается с ATOMIC_MOVE вариант, так как нет способа преобразовать этот параметр перемещения в действительный параметр копирования.

Таким образом, нет причин для беспокойства, и в целом рекомендуется избегать поспешных выводов из-за того, что вы видите не более десяти строк кода (особенно если не пробовали ни одного теста)…

Стандартная библиотека Java не обеспечивает способ выполнения атомарного перемещения во всех случаях.

Files.move () не гарантирует атомарного перемещения. Вы можете пройти ATOMIC_MOVE как вариант, но если перемещение не может быть выполнено как атомарная операция, AtomicMoveNotSupportedException выбрасывается (это тот случай, когда целевое местоположение находится в другом хранилище файлов и требует копирования файла).

Вы должны реализовать это самостоятельно, если вам это действительно нужно. Одним из решений может быть поймать AtomicMoveNotSupportedException и затем сделайте это: попробуйте переместить файл без ATOMIC_MOVE вариант, но перехватывать исключения и удалять цель, если во время копирования произошла ошибка.

Я столкнулся с подобной проблемой, которую нужно решить:

  • Один процесс часто обновляет файл через « сохранить в временный файл -> переместить временный файл в окончательный файл », используя Files.move(tmp, out, ATOMIC_MOVE, REPLACE_EXISTING);
  • Другой или несколько процессов читают этот файл - полностью, сразу и немедленно закрываются. Файл небольшой - меньше 50к.

И это просто не работает надежно, по крайней мере, на окнах. При большой нагрузке читатель иногда получает NoSuchFileException - это означает Files.move это не так ATOMIC даже в той же файловой системе :(

Мой env: Windows 10 + java 11.0.12

Вот код, с которым можно поиграть:

      import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Locale.US;

public class SomeTest {

    static int nWrite = 0;
    static int nRead = 0;
    static int cErrors = 0;
    static boolean writeFinished;
    static boolean useFileChannels = true;
    static String filePath = "c:/temp/test.out";

    @Test
    public void testParallelFileAccess() throws Exception {
        new Writer().start();
        new Reader().start();

        while( !writeFinished ) {
            Thread.sleep(10);
        }

        System.out.println("cErrors: " + cErrors);
    }

    static class Writer extends Thread {

        public Writer() {
            setDaemon(true);
        }

        @Override
        public void run() {
            File outFile = new File("c:/temp/test.out");
            File outFileTmp = new File(filePath + "tmp");
            byte[] bytes = "test".getBytes(UTF_8);

            for( nWrite = 1; nWrite <= 100000; nWrite++ ) {
                if( (nWrite % 1000) == 0 )
                    System.out.println("nWrite: " + nWrite + ", cReads: " + nRead);

                try( FileOutputStream fos = new FileOutputStream(outFileTmp) ) {
                    fos.write(bytes);
                }
                catch( Exception e ) {
                    logException("write", e);
                }

                int maxAttemps = 10;
                for( int i = 0; i <= maxAttemps; i++ ) {
                    try {
                        Files.move(outFileTmp.toPath(), outFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
                        break;
                    }
                    catch( IOException e ) {
                        try {
                            Thread.sleep(1);
                        }
                        catch( InterruptedException ex ) {
                            break;
                        }
                        if( i == maxAttemps )
                            logException("move", e);
                    }
                }
            }

            System.out.println("Write finished ...");
            writeFinished = true;
        }
    }

    static class Reader extends Thread {

        public Reader() {
            setDaemon(true);
        }

        @Override
        public void run() {
            File inFile = new File(filePath);
            Path inPath = inFile.toPath();
            byte[] bytes = new byte[100];
            ByteBuffer buffer = ByteBuffer.allocateDirect(100);

            try { Thread.sleep(100); } catch( InterruptedException e ) { }

            for( nRead = 0; !writeFinished; nRead++ ) {
                if( useFileChannels ) {
                    try ( ByteChannel channel = Files.newByteChannel(inPath, Set.of()) ) {
                        channel.read(buffer);
                    }
                    catch( Exception e ) {
                        logException("read", e);
                    }
                }
                else {
                    try( InputStream fis = Files.newInputStream(inFile.toPath()) ) {
                        fis.read(bytes);
                    }
                    catch( Exception e ) {
                        logException("read", e);
                    }
                }
            }
        }
    }

    private static void logException(String action, Exception e) {
        cErrors++;
        System.err.printf(US, "%s: %s - wr=%s, rd=%s:, %s%n", cErrors, action, nWrite, nRead, e);
    }
}
Другие вопросы по тегам