package co.codewizards.cloudstore.core.io;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.util.AssertUtil;
/**
* Factory creating {@link LockFile} instances.
* <p>
* All methods of this class are thread-safe.
* @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
*/
public class LockFileFactory {
private static final Logger logger = LoggerFactory.getLogger(LockFileFactory.class);
private static class LockFileFactoryHolder {
public static final LockFileFactory instance = new LockFileFactory();
}
private final Object mutex = this;
protected LockFileFactory() { }
public static LockFileFactory getInstance() {
return LockFileFactoryHolder.instance;
}
private final Map<File, LockFileImpl> file2LockFileImpl = new HashMap<File, LockFileImpl>();
/**
* Acquire an exclusive lock on the specified file.
* <p>
* <b>Important:</b> You <i>must</i> invoke {@link LockFile#release()} on the returned object! Use a try-finally-block
* to ensure it:
* <pre> LockFile lockFile = LockFileFactory.getInstance().acquire(theFile, theTimeout);
* try {
* // do something
* } finally {
* lockFile.release();
* }</pre>
* <p>
* Since Java 7, it is alternatively possible to use the try-with-resources clause like this:
* <pre> try ( LockFile lockFile = LockFileFactory.getInstance().acquire(theFile, theTimeout); ) {
* // do something while the file represented by 'lockFile' is locked.
* }</pre>
* <p>
* If the JVM is interrupted or shut down before {@code release()}, the file-lock is released by the
* operating system, but a missing {@code release()} causes the file to be locked for the entire remaining runtime
* of the JVM! This problem does not exist using the new try-with-resources-clause (since Java 7).
* <p>
* <b>Important:</b> This is <i>not</i> usable for the synchronization of multiple threads within the same Java virtual machine!
* Multiple {@link LockFile}s on the same {@link File} are possible within the same JVM! This locking mechanism
* only locks against separate processes! Since this implementation is based on {@link java.nio.channels.FileLock FileLock},
* please consult its Javadoc for further information.
* <p>
* To make it possible to synchronise multiple threads in the same JVM, too, there's {@link LockFile#getLock()}.
* <p>
* Multiple invocations of this method on the same given {@code file} return multiple different {@code LockFile} instances.
* The actual lock is held until the last {@code LockFile} instance was {@linkplain LockFile#release() released}.
* <p>
* This method is thread-safe.
* @param file the file to be locked. Must not be <code>null</code>. If this file does not exist in the file system,
* it is created by this method.
* @param timeoutMillis the timeout to wait for the lock to be acquired in milliseconds. The value 0 means to
* wait forever.
* @return the {@code LockFile}. Never <code>null</code>. This <i>must</i> be
* {@linkplain java.nio.channels.FileLock#release() released}
* (use a try-finally-block)!
* @throws TimeoutException if the {@code LockFile} could not be acquired within the timeout specified by {@code timeoutMillis}.
* @see LockFile#release()
*/
public LockFile acquire(File file, final long timeoutMillis) throws TimeoutException {
AssertUtil.assertNotNull(file, "file");
try {
file = file.getCanonicalFile();
} catch (final IOException e) {
throw new RuntimeException(e);
}
LockFileImpl lockFileImpl;
synchronized (mutex) {
lockFileImpl = file2LockFileImpl.get(file);
if (lockFileImpl == null) {
lockFileImpl = new LockFileImpl(this, file);
file2LockFileImpl.put(file, lockFileImpl);
logger.trace("acquire: Adding file='{}' lockFileImpl={}", file, lockFileImpl);
}
++lockFileImpl.acquireRunningCounter;
}
boolean exceptionThrown = true;
try {
// The following must NOT be synchronised! Otherwise we might wait here longer than the current timeout
// (as long as the longest timeout of all acquire methods running concurrently).
lockFileImpl.acquire(timeoutMillis);
exceptionThrown = false;
} finally {
synchronized (mutex) {
final int lockCounter = lockFileImpl.getLockCounter();
final int acquireRunningCounter = --lockFileImpl.acquireRunningCounter;
if (lockCounter < 1 && acquireRunningCounter < 1) {
logger.trace("acquire: Removing lockFileImpl={}", lockFileImpl);
final LockFileImpl removed = file2LockFileImpl.remove(file);
if (removed != lockFileImpl)
throw new IllegalStateException(String.format("file2LockFileImpl.remove(file) != lockFileImpl :: %s != %s", removed, lockFileImpl));
}
if (lockCounter < 1 && ! exceptionThrown)
throw new IllegalStateException("lockCounter < 1, but no exception thrown!");
}
}
return new LockFileProxy(lockFileImpl);
}
/**
* Callback from {@link LockFileImpl#release()}.
* @param lockFileImpl the {@code LockFileImpl} which notifies this factory about being released.
*/
protected void postRelease(final LockFileImpl lockFileImpl) {
synchronized (mutex) {
final LockFileImpl lockFileImpl2 = file2LockFileImpl.get(lockFileImpl.getFile());
if (lockFileImpl != lockFileImpl2)
throw new IllegalArgumentException(String.format("Unknown lockFileImpl instance (not managed by this registry)! file2LockFileImpl.get(lockFileImpl.getFile()) != lockFileImpl :: %s != %s ", lockFileImpl2, lockFileImpl));
final int lockCounter = lockFileImpl.getLockCounter();
final int acquireRunningCounter = lockFileImpl.acquireRunningCounter;
if (lockCounter < 1 && acquireRunningCounter < 1) {
logger.trace("postRelease: Removing lockFileImpl={}", lockFileImpl);
final LockFileImpl removed = file2LockFileImpl.remove(lockFileImpl.getFile());
if (removed != lockFileImpl)
throw new IllegalStateException(String.format("file2LockFileImpl.remove(file) != lockFileImpl :: %s != %s", removed, lockFileImpl));
}
}
}
}