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 extends LoosePath> lock, @Nonnull Duration elapsed, @Nonnull Duration timeout);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy