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}