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

com.goeuro.sync4j.sync.Lock Maven / Gradle / Ivy

package com.goeuro.sync4j.sync;

import com.goeuro.sync4j.fs.LoosePath;
import com.goeuro.sync4j.fs.Path;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.io.UncheckedIOException;
import java.nio.channels.ClosedByInterruptException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;

import static com.goeuro.sync4j.sync.Lock.LockResult.*;
import static com.goeuro.sync4j.sync.LockFile.lockFile;
import static com.goeuro.sync4j.sync.LockFileContent.lockFileContent;
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import static java.time.Duration.*;
import static java.time.OffsetDateTime.now;
import static java.util.Optional.*;
import static java.util.Optional.of;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;

public class Lock implements AutoCloseable {

    private static final Logger LOG = Logger.getLogger(Lock.class.getName());

    @Nonnull
    public static  Builder lock(@Nonnull Path file) {
        return new Builder<>(file);
    }

    @Nonnull
    public static final Duration DEFAULT_REFRESH_EVERY = parse("PT15S");
    @Nonnull
    public static final Duration DEFAULT_TIMEOUT = parse("PT5M");
    @Nonnull
    public static final Duration DEFAULT_RETRY_EVERY = parse("PT15S");

    @Nonnull
    private final UUID id;
    @Nonnull
    private final LockFile file;
    @Nonnull
    private final Optional owner;
    @Nonnull
    private final Duration refreshEvery;
    @Nonnull
    private final Duration timeout;
    @Nonnull
    private final Duration retryEvery;
    @Nonnull
    private Optional refreshThread = empty();

    protected Lock(
        @Nonnull UUID id,
        @Nonnull Path file,
        @Nonnull Optional owner,
        @Nonnull Duration refreshEvery,
        @Nonnull Duration timeout,
        @Nonnull Duration retryEvery
    ) {
        this.id = id;
        this.file = lockFile(file)
            .build();
        this.owner = owner;
        this.refreshEvery = refreshEvery;
        this.timeout = timeout;
        this.retryEvery = retryEvery;
    }

    @Nonnull
    public LockResult tryLock() {
        return tryLock(null);
    }

    @Nonnull
    public LockResult tryLock(@Nullable Duration timeout) {
        return tryLock(timeout, null);
    }

    @Nonnull
    public LockResult tryLock(@Nullable Duration timeout, @Nullable RetryNotifier notifier) {
        synchronized (this) {
            refreshThread.ifPresent(ignored -> {
                throw new IllegalStateException("Already locked.");
            });
            final LocalDateTime start = LocalDateTime.now();
            LockResult result;
            try {
                result = tryAcquireLock();
            } catch (final InterruptedException ignored) {
                currentThread().interrupt();
                return interrupted;
            }
            while (!result.success() && timeout != null) {
                final Duration elapsed = between(start, now());
                final Duration left = timeout.minus(elapsed);
                if (left.compareTo(ZERO) <= 0) {
                    return blocked;
                }
                if (notifier != null && !notifier.nextAcquireRetryAllowed(this, elapsed, timeout)) {
                    return retryRejected;
                }
                final Duration sleepFor = left.compareTo(retryEvery) > 0 ? retryEvery : left;
                try {
                    sleep(sleepFor.toMillis());
                    result = tryAcquireLock();
                } catch (final InterruptedException ignored) {
                    currentThread().interrupt();
                    return interrupted;
                }
            }
            if (!result.success()) {
                return result;
            }
            refresh();
            refreshThread = of(createRefreshThread());
            return result;
        }
    }

    @Nonnull
    protected LockResult tryAcquireLock() throws InterruptedException {
        try {
            return file().read()
                .map(this::allowsOverwrite)
                .orElse(locked);
        } catch (final UncheckedIOException e) {
            if (e.getCause() instanceof ClosedByInterruptException) {
                final InterruptedException target = new InterruptedException();
                target.initCause(e.getCause());
                //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
                throw target;
            }
            throw e;
        }
    }

    public void unlock() {
        synchronized (this) {
            refreshThread.ifPresent(this::unlockBasedOn);
        }
    }

    @Nonnull
    protected LockResult allowsOverwrite(@Nonnull LockFileContent content) {
        final Duration lastPingSince = between(content.lastPing(), now());
        if (lastPingSince.compareTo(timeout()) > 0) {
            LOG.log(FINE, () -> "Found old existing lock file '" + file() + "'(" + content + ") which is timed out. Take it over now.");
            return lockedByTakeover;
        }
        LOG.log(FINE, () -> "Found existing lock file '" + file() + "' (" + content + ") which is still active. Block lock request.");
        return blocked;
    }

    @Nonnull
    protected Thread createRefreshThread() {
        final Thread result = new Thread(this::automaticRefresh, toString());
        result.setDaemon(true);
        result.start();
        return result;
    }

    protected void automaticRefresh() {
        while (!currentThread().isInterrupted()) {
            try {
                //noinspection BusyWait
                sleep(refreshEvery().toMillis());
                refresh();
            } catch (final InterruptedException ignored) {
                currentThread().interrupt();
            } catch (final Exception e) {
                LOG.log(WARNING, e, () -> "Cannot update write lock file '" + file() + "'." +
                    " This is only serious if another process also tries to write to the target location.");
            }
        }
    }

    protected void refresh() {
        file().write(lockFileContent()
            .withId(id())
            .lastPingedAt(now(ZoneId.of("Z")))
            .withOwner(owner())
            .build()
        );
    }

    @Nonnull
    public LockFile file() {
        return file;
    }

    @Nonnull
    public UUID id() {
        return id;
    }

    @Nonnull
    public Duration refreshEvery() {
        return refreshEvery;
    }

    @Nonnull
    public Duration retryEvery() {
        return retryEvery;
    }

    @Nonnull
    public Duration timeout() {
        return timeout;
    }

    @Nonnull
    public Optional owner() {
        return owner;
    }

    @Nonnull
    protected Optional refreshThread() {
        synchronized (this) {
            return refreshThread;
        }
    }

    @Override
    public void close() {
        unlock();
    }

    @GuardedBy("this")
    protected void unlockBasedOn(@Nonnull Thread refreshThread) {
        try {
            try {
                boolean wasInterrupted = currentThread().isInterrupted();
                while (refreshThread.isAlive()) {
                    refreshThread.interrupt();
                    try {
                        refreshThread.join(SECONDS.toMillis(2));
                        if (refreshThread.isAlive()) {
                            LOG.warning(refreshThread + " was interrupted but is still alive. Continue waiting...");
                        }
                    } catch (final InterruptedException ignored) {
                        wasInterrupted = true;
                    }
                }
                if (wasInterrupted) {
                    currentThread().interrupt();
                }
            } finally {
                file().delete();
            }
        } finally {
            this.refreshThread = empty();
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + ":" + id();
    }

    public static class Builder {

        @Nonnull
        private final Path file;
        @Nonnull
        private Optional id = empty();
        @Nonnull
        private Optional owner = empty();
        @Nonnull
        private Optional refreshEvery = empty();
        @Nonnull
        private Optional timeout = empty();
        @Nonnull
        private Optional retryEvery = empty();

        protected Builder(@Nonnull Path file) {
            this.file = file;
        }

        @Nonnull
        public Builder withId(@Nullable UUID id) {
            this.id = ofNullable(id);
            return this;
        }

        @Nonnull
        public Builder withOwner(@Nullable String owner) {
            this.owner = ofNullable(owner);
            return this;
        }

        @Nonnull
        public Builder whichRefreshesEvery(@Nullable Duration refreshEvery) {
            this.refreshEvery = ofNullable(refreshEvery);
            return this;
        }

        @Nonnull
        public Builder withTimeoutAfter(@Nullable Duration timeout) {
            this.timeout = ofNullable(timeout);
            return this;
        }

        @Nonnull
        public Builder withRetryEvery(@Nullable Duration retryEvery) {
            this.retryEvery = ofNullable(retryEvery);
            return this;
        }

        @Nonnull
        public Lock build() {
            return new Lock<>(
                id.orElseGet(UUID::randomUUID),
                file,
                owner,
                refreshEvery.orElse(DEFAULT_REFRESH_EVERY),
                timeout.orElse(DEFAULT_TIMEOUT),
                retryEvery.orElse(DEFAULT_RETRY_EVERY)
            );
        }
    }

    public enum LockResult {
        locked(true),
        lockedByTakeover(true),
        blocked(false),
        interrupted(false),
        retryRejected(false);

        private final boolean success;

        LockResult(boolean success) {
            this.success = success;
        }

        public boolean success() {
            return success;
        }

    }

    @FunctionalInterface
    public interface RetryNotifier {
        boolean nextAcquireRetryAllowed(@Nonnull Lock lock, @Nonnull Duration elapsed, @Nonnull Duration timeout);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy