package co.codewizards.cloudstore.core.io;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.util.AssertUtil;
class LockFileImpl implements LockFile {
private static final Logger logger = LoggerFactory.getLogger(LockFileImpl.class);
private final LockFileFactory lockFileFactory;
private final File file;
private final String thisID = Integer.toHexString(System.identityHashCode(this));
/**
* Counter tracking how many {@link LockFileFactory#acquire(File, long)} methods are running in parallel
* for the same lock-file.
* <p>
* This field is synchronised via {@link LockFileFactory#mutex} - not {@code this.mutex}!
*/
protected int acquireRunningCounter = 0;
private int lockCounter = 0;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
private final Lock lock = new ReentrantLock();
private final Object mutex = this;
protected LockFileImpl(final LockFileFactory lockFileFactory, final File file) {
this.lockFileFactory = AssertUtil.assertNotNull(lockFileFactory, "lockFileFactory");
this.file = AssertUtil.assertNotNull(file, "file");
// this.mutex = lockFileFactory.mutex;
logger.debug("[{}]<init>: file='{}'", thisID, file);
}
@Override
public File getFile() {
return file;
}
private boolean tryAcquire() {
logger.trace("[{}]tryAcquire: entered. lockCounter={}", thisID, lockCounter);
synchronized (mutex) {
logger.trace("[{}]tryAcquire: inside synchronized", thisID);
try {
if (randomAccessFile == null) {
logger.trace("[{}]tryAcquire: acquiring underlying FileLock for file={}.", thisID, Integer.toHexString(System.identityHashCode(file)));
randomAccessFile = file.createRandomAccessFile("rw");
try {
fileLock = randomAccessFile.getChannel().tryLock(0, Long.MAX_VALUE, false);
} catch (final OverlappingFileLockException x) {
doNothing();
// It was not successfully locked - no need to do anything.
//
// This should never happen when working with the LockFileImpl alone, because we're
// in a synchronized block, but it may happen due to external causes: If some other
// code in the same JVM sets a FileLock onto the file, this exception might be thrown
// here.
//
// We encountered this exception already with LockFileImpl alone, but this was caused
// by another bug. After we fixed the other bug, this exception did not fly, anymore
// (I temporarily commented this block out for testing). Thus, it's sure now, that
// only external reasons (in the same JVM but outside the LockFileImpl) can cause this
// exception to happen.
} finally {
if (fileLock == null) {
logger.trace("[{}]tryAcquire: fileLock was NOT acquired. Closing randomAccessFile now.", thisID);
randomAccessFile.close();
randomAccessFile = null;
}
}
if (fileLock == null) {
logger.debug("[{}]tryAcquire: returning false. lockCounter={}", thisID, lockCounter);
return false;
}
}
++lockCounter;
logger.debug("[{}]tryAcquire: returning true. lockCounter={}", thisID, lockCounter);
return true;
} catch (final IOException x) {
throw new RuntimeException(x);
}
}
}
public void acquire(final long timeoutMillis) throws TimeoutException {
if (timeoutMillis < 0)
throw new IllegalArgumentException("timeoutMillis < 0");
final long beginTimestamp = System.currentTimeMillis();
while (!tryAcquire()) {
try {
Thread.sleep(300);
} catch (final InterruptedException e) { doNothing(); }
if (timeoutMillis > 0 && System.currentTimeMillis() - beginTimestamp > timeoutMillis) {
throw new TimeoutException(String.format("Could not lock '%s' within timeout of %s ms!",
file.getAbsolutePath(), timeoutMillis));
}
}
}
/**
* Releases the lock.
* <p>
* <b>Important:</b> In contrast to the documentation of the API method {@link LockFile#release()}, the
* implementation of this method in {@link LockFileImpl} is invoked multiple times: Once
* for every {@link LockFile}-API-instance (i.e. {@link LockFileProxy} instance). The actual implementation
* uses reference-counting to know when to release the real, underlying lock.
*/
@Override
public void release() {
logger.trace("[{}]release: entered. lockCounter={}", thisID, lockCounter);
synchronized (mutex) {
logger.trace("[{}]release: inside synchronized", thisID);
final int lockCounterValue = --lockCounter;
if (lockCounterValue > 0) {
logger.debug("[{}]release: NOT releasing underlying FileLock. lockCounter={}", thisID, lockCounter);
return;
}
if (lockCounterValue < 0)
throw new IllegalStateException("Trying to release more often than was acquired!!!");
logger.debug("[{}]release: releasing underlying FileLock. lockCounter={}", thisID, lockCounter);
try {
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
} catch (final IOException x) {
throw new RuntimeException(x);
}
}
lockFileFactory.postRelease(this);
}
@Override
public void close() {
throw new UnsupportedOperationException("Only the LockFileProxy should be used! This method should therefore never be invoked!");
}
protected int getLockCounter() {
return lockCounter;
}
@Override
public Lock getLock() {
return lock;
}
@Override
public IInputStream createInputStream() throws IOException {
return new LockFileInputStream();
}
@Override
public IOutputStream createOutputStream() throws IOException {
return new LockFileOutputStream();
}
private class LockFileInputStream extends InputStream implements IInputStream {
private long position;
public LockFileInputStream() {
lock.lock();
}
@Override
public int read() throws IOException {
randomAccessFile.seek(position);
final int result = randomAccessFile.read();
position = randomAccessFile.getFilePointer();
return result;
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
randomAccessFile.seek(position);
final int result = randomAccessFile.read(b, off, len);
position = randomAccessFile.getFilePointer();
return result;
}
@Override
public void close() throws IOException {
super.close();
lock.unlock();
}
}
private class LockFileOutputStream extends OutputStream implements IOutputStream {
private long position;
public LockFileOutputStream() throws IOException {
lock.lock();
randomAccessFile.setLength(0);
}
@Override
public void write(final int b) throws IOException {
randomAccessFile.seek(position);
randomAccessFile.write(b);
position = randomAccessFile.getFilePointer();
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
randomAccessFile.seek(position);
randomAccessFile.write(b, off, len);
position = randomAccessFile.getFilePointer();
}
@Override
public void close() throws IOException {
super.close();
lock.unlock();
}
}
}