/** * Copyright (C) 2012-2013 Selventa, Inc. * * This file is part of the OpenBEL Framework. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The OpenBEL Framework is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>. * * Additional Terms under LGPL v3: * * This license does not authorize you and you are prohibited from using the * name, trademarks, service marks, logos or similar indicia of Selventa, Inc., * or, in the discretion of other licensors or authors of the program, the * name, trademarks, service marks, logos or similar indicia of such authors or * licensors, in any marketing or advertising materials relating to your * distribution of the program or any covered product. This restriction does * not waive or limit your obligation to keep intact all copyright notices set * forth in the program as delivered to you. * * If you distribute the program in whole or in part, or any modified version * of the program, and you assume contractual liability to the recipient with * respect to the program or modified version, then you will indemnify the * authors and licensors of the program for any liabilities that these * contractual assumptions directly impose on those licensors and authors. */ package org.openbel.framework.common.lock; import static org.openbel.framework.common.BELUtilities.getPID; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.openbel.framework.common.InvalidArgument; /** * {@link LockAdviser} is responsible for obtaining and releasing {@link Lock * file locks} for read and write access. The {@link Lock file locks} are stored * on disk at the specified {@link File lock path directory}. This locking * mechanism allows for multiple read locks to be obtained, but only one write * lock. * <p> * Each unique {@link File lock path directory} will have a single instance of * {@link LockAdviser lock adviser}. {@link LockAdviser} is thread-safe which * allows multiple threads to obtain and release {@link Lock locks} from the * same {@link LockAdviser lock adviser instance} without concurrency problems. * </p> * TODO Document Lockable implementations with LockAdviser-specific details. * TODO Review and refactor if necessary. TODO Document fully once you're happy * with the design. * * @author Anthony Bargnesi {@code <abargnesi@selventa.com>} */ public class LockAdviser implements Lockable { private static final String LOCK_EXTENSION = ".lck"; private static final String READER_LOCK_PREFIX = "rdr_"; private static final String WRITER_LOCK_PREFIX = "wtr_"; private static LockFileFilter lockFilter; private static Map<File, LockAdviser> instances; private final Map<String, Lock> locks; private final File lockPath; /** * Private constructor for {@link LockAdviser lock advisers} that uses a * {@link File lock path directory} where {@link Lock read / write locks} * will be stored. * * @param lockPath {@link File}, the lock path directory where {@link Lock * read / write locks} will be strored, which cannot be <tt>null</tt>, must * exist as a directory, and must be readable and writeable. * @throws InvalidArgument Thrown if <tt>lockPath</tt> is <tt>null</tt>, * does not exist as a directory, or is not readable or writeable. */ private LockAdviser(final File lockPath) { if (lockPath == null) { throw new InvalidArgument("lockPath", lockPath); } if (!lockPath.exists() || !lockPath.isDirectory()) { throw new InvalidArgument( "lockPath directory does not exist"); } if (!lockPath.canRead() || !lockPath.canWrite()) { throw new InvalidArgument( "lockPath is not readable/writeable"); } this.lockPath = lockPath; this.locks = new HashMap<String, LockAdviser.Lock>(); } public synchronized static final LockAdviser instance(final File dirPath) { if (instances == null) { lockFilter = new LockFileFilter(); instances = new HashMap<File, LockAdviser>(); Runtime.getRuntime().addShutdownHook(new Thread(new LockCleanup())); } LockAdviser lr = instances.get(dirPath); if (lr == null) { lr = new LockAdviser(dirPath); instances.put(dirPath, lr); } return lr; } /** * {@inheritDoc} * <p> * A {@link Lock read lock} cannot be obtained if the {@link Lock write * lock} has been obtained. * </p> */ @Override public synchronized Lock obtainReadLock() { File writeLock = getWriteLock(); if (writeLock != null && writeLock.exists()) { return null; } return createLock(false); } /** * {@inheritDoc} * <p> * This will create a {@link File write lock file} at * {@link LockAdviser#lockPath} that represents the write lock. * </p> * <p> * A {@link Lock write lock} cannot be obtained if a {@link Lock read lock} * is held. * </p> */ @Override public synchronized Lock obtainWriteLock() { Set<File> readLocks = getReadLocks(); if (readLocks.isEmpty()) { final File writeLock = getWriteLock(); if (writeLock == null) { return createLock(true); } } return null; } /** * {@inheritDoc} * <p> * This will remove the {@link File read lock file} in * {@link LockAdviser#readLocks} that represents the read lock. * </p> */ @Override public synchronized void releaseReadLock(final Lock lock) { if (lock == null || lock.name == null || lock.name.isEmpty()) { throw new InvalidArgument("lock is invalid"); } if (locks.containsKey(lock.name)) { locks.remove(lock.name); final Set<File> readLocks = getReadLocks(); for (final File readLock : readLocks) { if (readLock.getName().endsWith(lock.name)) { if (!readLock.delete()) { throw new IllegalStateException( "read lock could not be deleted"); } readLocks.remove(readLock); return; } } } throw new InvalidArgument("read lock '" + lock.name + "' is unknown"); } /** * {@inheritDoc} * <p> * This will remove the {@link File read lock file} in * {@link LockAdviser#readLocks} that represents the read lock. * </p> */ @Override public synchronized void releaseWriteLock(final Lock lock) { if (lock == null || lock.name == null || lock.name.isEmpty()) { throw new InvalidArgument("lock is invalid"); } locks.remove(lock.name); final File writeLock = getWriteLock(); if (!writeLock.delete()) { throw new IllegalStateException( "write lock could not be deleted"); } } /** * Reads {@link LockAdviser#lockPath lock path directory} and finds the * {@link File read lock files}, if any. * * @return the {@link Set set} of {@link read lock files}, or an empty * {@link Set} if no read locks exist */ private Set<File> getReadLocks() { final Set<File> readLocks = new HashSet<File>(); File[] lckFiles = this.lockPath.listFiles(lockFilter); for (final File lckFile : lckFiles) { if (lockFilter.isReaderLockFile(lckFile)) { readLocks.add(lckFile); } } return readLocks; } /** * Reads {@link LockAdviser#lockPath lock path directory} and finds the * {@link File write lock file}, if any. * * @return the {@link write lock file}, or <tt>null</tt> if no write lock * exists */ private File getWriteLock() { File[] lckFiles = this.lockPath.listFiles(lockFilter); for (final File lckFile : lckFiles) { if (lockFilter.isWriterLockFile(lckFile)) { return lckFile; } } return null; } /** * Creates a {@link Lock lock} on the this instance of the * {@link LockAdviser}. If <tt>writer</tt> is false then a {@link File read * lock file} is created, otherwise a {@link File write lock file is * created}. * * @param writer, <tt>true</tt> if a {@link File write lock file} should be * created, <tt>false</tt> otherwise * @return the created {@link Lock} * @throws IllegalStateException Thrown if the {@link File lock file} could * not be created */ private Lock createLock(boolean writer) { final int pid = getPID(); final String baseLock; if (!writer) { baseLock = READER_LOCK_PREFIX + pid + LOCK_EXTENSION; } else { baseLock = WRITER_LOCK_PREFIX + pid + LOCK_EXTENSION; } int i = 0; while (locks.containsKey(baseLock + (++i))) { // no logic needed } final String lockName = baseLock + i; final Lock lock = new Lock(lockName); final File lockFile = new File(lockPath, lockName); try { if (!lockFile.createNewFile()) { throw new IllegalStateException("could not create lock"); } } catch (IOException e) { throw new IllegalStateException("could not create lock", e); } locks.put(lockName, lock); return lock; } public static class Lock { private final String name; private final int hashcode; private Lock(String name) { if (name == null || name.isEmpty()) { throw new InvalidArgument("name is invalid"); } this.name = name; this.hashcode = generateHash(); } private int generateHash() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } /** * {@inheritDoc} */ @Override public int hashCode() { return hashcode; } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } Lock other = (Lock) o; if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } } private static final class LockFileFilter implements FileFilter { private static final Pattern LOCK_PATTERN = Pattern .compile("(rdr_|wtr_)\\d+\\.lck\\d+"); /** * {@inheritDoc} */ @Override public boolean accept(final File file) { if (file == null) { return false; } return LOCK_PATTERN.matcher(file.getName()).matches(); } public boolean isReaderLockFile(final File file) { if (file == null) { return false; } return file.getName().startsWith(READER_LOCK_PREFIX); } public boolean isWriterLockFile(final File file) { if (file == null) { return false; } return file.getName().startsWith(WRITER_LOCK_PREFIX); } } private static final class LockCleanup implements Runnable { /** * {@inheritDoc} */ @Override public void run() { Collection<LockAdviser> advisercol = instances.values(); if (advisercol.isEmpty()) { return; } final LockAdviser[] advisers = advisercol .toArray(new LockAdviser[advisercol.size()]); for (final LockAdviser adviser : advisers) { final Collection<Lock> lockcol = adviser.locks.values(); final Lock[] locks = lockcol.toArray(new Lock[lockcol.size()]); for (final Lock lock : locks) { if (lock.name.startsWith(READER_LOCK_PREFIX)) { adviser.releaseReadLock(lock); } else { adviser.releaseWriteLock(lock); } } } } } }