001package co.codewizards.cloudstore.core.io; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.IOException; 006import java.util.HashMap; 007import java.util.Map; 008 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import co.codewizards.cloudstore.core.oio.File; 013import co.codewizards.cloudstore.core.util.AssertUtil; 014 015/** 016 * Factory creating {@link LockFile} instances. 017 * <p> 018 * All methods of this class are thread-safe. 019 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 020 */ 021public class LockFileFactory { 022 023 private static final Logger logger = LoggerFactory.getLogger(LockFileFactory.class); 024 025 private static class LockFileFactoryHolder { 026 public static final LockFileFactory instance = new LockFileFactory(); 027 } 028 029 private final Object mutex = this; 030 031 protected LockFileFactory() { } 032 033 public static LockFileFactory getInstance() { 034 return LockFileFactoryHolder.instance; 035 } 036 037 private final Map<File, LockFileImpl> file2LockFileImpl = new HashMap<File, LockFileImpl>(); 038 039 /** 040 * Acquire an exclusive lock on the specified file. 041 * <p> 042 * <b>Important:</b> You <i>must</i> invoke {@link LockFile#release()} on the returned object! Use a try-finally-block 043 * to ensure it: 044 * <pre> LockFile lockFile = LockFileFactory.getInstance().acquire(theFile, theTimeout); 045 * try { 046 * // do something 047 * } finally { 048 * lockFile.release(); 049 * }</pre> 050 * <p> 051 * Since Java 7, it is alternatively possible to use the try-with-resources clause like this: 052 * <pre> try ( LockFile lockFile = LockFileFactory.getInstance().acquire(theFile, theTimeout); ) { 053 * // do something while the file represented by 'lockFile' is locked. 054 * }</pre> 055 * <p> 056 * If the JVM is interrupted or shut down before {@code release()}, the file-lock is released by the 057 * operating system, but a missing {@code release()} causes the file to be locked for the entire remaining runtime 058 * of the JVM! This problem does not exist using the new try-with-resources-clause (since Java 7). 059 * <p> 060 * <b>Important:</b> This is <i>not</i> usable for the synchronization of multiple threads within the same Java virtual machine! 061 * Multiple {@link LockFile}s on the same {@link File} are possible within the same JVM! This locking mechanism 062 * only locks against separate processes! Since this implementation is based on {@link java.nio.channels.FileLock FileLock}, 063 * please consult its Javadoc for further information. 064 * <p> 065 * To make it possible to synchronise multiple threads in the same JVM, too, there's {@link LockFile#getLock()}. 066 * <p> 067 * Multiple invocations of this method on the same given {@code file} return multiple different {@code LockFile} instances. 068 * The actual lock is held until the last {@code LockFile} instance was {@linkplain LockFile#release() released}. 069 * <p> 070 * This method is thread-safe. 071 * @param file the file to be locked. Must not be <code>null</code>. If this file does not exist in the file system, 072 * it is created by this method. 073 * @param timeoutMillis the timeout to wait for the lock to be acquired in milliseconds. The value 0 means to 074 * wait forever. 075 * @return the {@code LockFile}. Never <code>null</code>. This <i>must</i> be 076 * {@linkplain java.nio.channels.FileLock#release() released} 077 * (use a try-finally-block)! 078 * @throws TimeoutException if the {@code LockFile} could not be acquired within the timeout specified by {@code timeoutMillis}. 079 * @see LockFile#release() 080 */ 081 public LockFile acquire(File file, final long timeoutMillis) throws TimeoutException { 082 AssertUtil.assertNotNull(file, "file"); 083 try { 084 file = file.getCanonicalFile(); 085 } catch (final IOException e) { 086 throw new RuntimeException(e); 087 } 088 089 LockFileImpl lockFileImpl; 090 synchronized (mutex) { 091 lockFileImpl = file2LockFileImpl.get(file); 092 if (lockFileImpl == null) { 093 lockFileImpl = new LockFileImpl(this, file); 094 file2LockFileImpl.put(file, lockFileImpl); 095 logger.trace("acquire: Adding file='{}' lockFileImpl={}", file, lockFileImpl); 096 } 097 ++lockFileImpl.acquireRunningCounter; 098 } 099 boolean exceptionThrown = true; 100 try { 101 // The following must NOT be synchronised! Otherwise we might wait here longer than the current timeout 102 // (as long as the longest timeout of all acquire methods running concurrently). 103 lockFileImpl.acquire(timeoutMillis); 104 exceptionThrown = false; 105 } finally { 106 synchronized (mutex) { 107 final int lockCounter = lockFileImpl.getLockCounter(); 108 final int acquireRunningCounter = --lockFileImpl.acquireRunningCounter; 109 110 if (lockCounter < 1 && acquireRunningCounter < 1) { 111 logger.trace("acquire: Removing lockFileImpl={}", lockFileImpl); 112 final LockFileImpl removed = file2LockFileImpl.remove(file); 113 if (removed != lockFileImpl) 114 throw new IllegalStateException(String.format("file2LockFileImpl.remove(file) != lockFileImpl :: %s != %s", removed, lockFileImpl)); 115 } 116 117 if (lockCounter < 1 && ! exceptionThrown) 118 throw new IllegalStateException("lockCounter < 1, but no exception thrown!"); 119 } 120 } 121 return new LockFileProxy(lockFileImpl); 122 } 123 124 /** 125 * Callback from {@link LockFileImpl#release()}. 126 * @param lockFileImpl the {@code LockFileImpl} which notifies this factory about being released. 127 */ 128 protected void postRelease(final LockFileImpl lockFileImpl) { 129 synchronized (mutex) { 130 final LockFileImpl lockFileImpl2 = file2LockFileImpl.get(lockFileImpl.getFile()); 131 if (lockFileImpl != lockFileImpl2) 132 throw new IllegalArgumentException(String.format("Unknown lockFileImpl instance (not managed by this registry)! file2LockFileImpl.get(lockFileImpl.getFile()) != lockFileImpl :: %s != %s ", lockFileImpl2, lockFileImpl)); 133 134 final int lockCounter = lockFileImpl.getLockCounter(); 135 final int acquireRunningCounter = lockFileImpl.acquireRunningCounter; 136 137 if (lockCounter < 1 && acquireRunningCounter < 1) { 138 logger.trace("postRelease: Removing lockFileImpl={}", lockFileImpl); 139 final LockFileImpl removed = file2LockFileImpl.remove(lockFileImpl.getFile()); 140 if (removed != lockFileImpl) 141 throw new IllegalStateException(String.format("file2LockFileImpl.remove(file) != lockFileImpl :: %s != %s", removed, lockFileImpl)); 142 } 143 } 144 } 145 146}