All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.io.LockingFileOperations Maven / Gradle / Ivy

package name.remal.io;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import name.remal.lambda.Function1;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonWritableChannelException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static java.lang.Math.min;
import static name.remal.SneakyThrow.sneakyThrow;

public class LockingFileOperations {

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    @NotNull
    public static byte[] lockAndReadBytes(@NotNull File file, @NotNull Duration lockAcquiringTimeout) {
        try {
            File absoluteFile = file.getAbsoluteFile();
            return forLockedFileChannel(absoluteFile, "r", lockAcquiringTimeout, channel -> {
                long size = channel.size();
                if (MAX_ARRAY_SIZE < size) {
                    throw new IOException(String.format(
                        "%s: File can't be read as bytes array because it's length is %d bytes, that's more than manimum java array length - %d",
                        absoluteFile.getPath(),
                        size,
                        MAX_ARRAY_SIZE
                    ));
                }

                byte[] bytes = new byte[(int) size];
                channel.read(ByteBuffer.wrap(bytes));
                return bytes;
            });

        } catch (Throwable throwable) {
            throw sneakyThrow(throwable);
        }
    }

    public static void lockAndWriteBytes(@NotNull File file, @NotNull Duration lockAcquiringTimeout, @NotNull byte[] bytes) {
        try {
            forLockedFileChannel(file, "rw", lockAcquiringTimeout, channel -> {
                channel.write(ByteBuffer.wrap(bytes));
                return null;
            });

        } catch (Throwable throwable) {
            throw sneakyThrow(throwable);
        }
    }

    private static final ConcurrentMap FILE_LOCKS = new ConcurrentHashMap<>();

    /**
     * @see java.io.RandomAccessFile#RandomAccessFile(java.io.File, String)
     */
    @SuppressFBWarnings("SWL_SLEEP_WITH_LOCK_HELD")
    public static  R forLockedFileChannel(@NotNull File file, @NotNull String mode, @NotNull Duration lockAcquiringTimeout, @NotNull Function1 action) {
        file = file.getAbsoluteFile();
        Object fileLock = FILE_LOCKS.computeIfAbsent(file, it -> it);
        synchronized (fileLock) {
            try {
                if (mode.contains("w")) {
                    Files.createDirectories(file.getParentFile().toPath());
                }

                long timeoutNanos = lockAcquiringTimeout.toNanos();
                long sleepMillis = getSleepMillis(timeoutNanos);
                long startNanos = System.nanoTime();
                try (FileChannel channel = new RandomAccessFile(file, mode).getChannel()) {
                    while (true) {
                        FileLock lock;
                        try {
                            lock = channel.tryLock(0, Long.MAX_VALUE, !mode.contains("w"));
                        } catch (NonWritableChannelException e) {
                            if (!mode.contains("w")) {
                                return forLockedFileChannel(file, mode + "w", lockAcquiringTimeout, action);
                            } else {
                                throw e;
                            }
                        } catch (Exception ignored) {
                            lock = null;
                        }

                        if (lock == null) {
                            if (System.nanoTime() - startNanos < timeoutNanos) {
                                Thread.sleep(sleepMillis);
                                continue;
                            } else {
                                throw new FileLockingException(String.format("%s: File lock can't be acquired for %d nanoseconds", file.getPath(), timeoutNanos));
                            }
                        }

                        try {
                            return action.invoke(channel);

                        } finally {
                            lock.release();
                        }
                    }
                }

            } catch (Throwable throwable) {
                throw sneakyThrow(throwable);
            } finally {
                FILE_LOCKS.remove(file);
            }
        }
    }

    public static void lockAndDelete(@NotNull File file, @NotNull Duration lockAcquiringTimeout) {
        try {
            file = file.getAbsoluteFile();
            if (file.isFile()) {
                long timeoutNanos = lockAcquiringTimeout.toNanos();
                long sleepMillis = getSleepMillis(timeoutNanos);
                long startNanos = System.nanoTime();
                while (true) {
                    if (file.delete()) {
                        return;

                    } else {
                        if (System.nanoTime() - startNanos < timeoutNanos) {
                            Thread.sleep(sleepMillis);
                        } else {
                            throw new IOException(String.format("%s: File can't be deleted for %d nanoseconds", file.getPath(), timeoutNanos));
                        }
                    }
                }

            } else if (file.isDirectory()) {
                throw new IllegalArgumentException("File " + file + " is a directory");
            }

        } catch (Throwable throwable) {
            throw sneakyThrow(throwable);
        }
    }

    private static final long MILLI_NANOS = ChronoUnit.MILLIS.getDuration().toNanos();

    private static final long SECOND_MILLIS = ChronoUnit.SECONDS.getDuration().toMillis();

    private static long getSleepMillis(long timeoutNanos) {
        if (timeoutNanos < MILLI_NANOS) {
            return 1;
        } else {
            return min(SECOND_MILLIS, timeoutNanos / MILLI_NANOS / 100);
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy