com.bigdata.btree.ReadWriteLockManager Maven / Gradle / Ivy
/*
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
[email protected]
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on Sep 2, 2014
*/
package com.bigdata.btree;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.bigdata.journal.ICommitter;
/**
* Base class for managing read/write locks for unisolated {@link ICommitter}s.
*
* @author Bryan Thompson
*
* @see AssertionError: Child does
* not have persistent identity
*/
public class ReadWriteLockManager implements IReadWriteLockManager {
// private static final Logger log = Logger.getLogger(ReadWriteLockManager.class);
/**
* The #of milliseconds that the class will wait for a read or write lock. A
* (wrapped) {@link InterruptedException} will be thrown if this timeout is
* exceeded. The default is {@value #LOCK_TIMEOUT_MILLIS} milliseconds. Use
* {@link Long#MAX_VALUE} for no timeout.
*
* TODO There may be no reason to have a timeout when waiting for a lock in
* which case we can get rid of this field. Also, there is no means
* available to configure the timeout (in a similar fashion you can not
* configure the fairness policy for the {@link ReentrantReadWriteLock}).
*
* If we get rid of this field, then the {@link WrappedReadLock} and
* {@link WrappedWriteLock} classes can be simplified to have normal lock
* semantics rather than tryLock() based semantics.
*/
private static final long LOCK_TIMEOUT_MILLIS = Long.MAX_VALUE;// 10000;
/*
* Note: This creates a hard reference that defeats the weak keys in the
* hash map.
*/
// /**
// * The unisolated persistence capable data structure.
// */
// final private ICheckpointProtocol committer;
/**
* True iff the caller's {@link ICheckpointProtocol} object was read-only.
*/
final private boolean readOnly;
/**
* The {@link Lock} used to permit concurrent readers on an unisolated index
* while serializing access to that index when a writer must run.
*/
final private WrappedWriteLock writeLock;
/**
* The {@link Lock} ensures that any code path that obtains the read lock
* also maintains the per-thread read-lock counter.
*/
final private Lock readLock;
/**
* Canonicalizing mapping for the {@link ReadWriteLockManager} objects.
*/
static final private WeakHashMap locks = new WeakHashMap();
@Override
public int getReadLockCount() {
if (readOnly) {
// No locks are actually taken.
return 0;
}
// Return the locks actually held by this thread.
final Integer readLockCounter = ((WrappedReadLock) readLock).threadLockMap
.get(Thread.currentThread().getId());
if (readLockCounter == null) {
// No read locks are held.
return 0;
}
return readLockCounter.intValue();
}
@Override
public Lock readLock() {
return readLock;
}
@Override
public Lock writeLock() {
if (readOnly)
throw new UnsupportedOperationException(
AbstractBTree.ERROR_READ_ONLY);
return writeLock;
}
@Override
public boolean isReadOnly() {
/*
* Note: This method is grounded out without delegation to avoid
* recursion through the target persistence capable data structure.
*/
return readOnly;
}
/**
* {@link WrappedReadLock} is used to intercept lock/unlock calls to the
* readLock to trigger calls to the logic that tracks the #of reentrant
* read-locks by read and which can be used to identify whether the readlock
* is held by the current thread.
*
* This is tested in the touch() methods for the BTree and HTree classe to
* determine whether the touch should be ignored or trigger potential
* evictions.
*
* @see AssertionError: Child
* does not have persistent identity
*/
private class WrappedReadLock implements Lock {
private final Lock delegate;
/**
* Maintain count of readLocks on by Thread. This is used to avoid having
* read-only operations protected by an {@link ReadWriteLockManager}
* causing evictions of dirty nodes and leaves.
*
* @see AssertionError: Child
* does not have persistent identity
*/
private final ConcurrentHashMap threadLockMap;
/**
* Track the #of read locks by thread IFF this is a read/write index
* view.
*/
private void readLockedThread() {
final long thisThreadId = Thread.currentThread().getId();
final Integer entry = threadLockMap.get(thisThreadId);
final Integer newVal = entry == null ? 1 : 1 + entry.intValue();
threadLockMap.put(thisThreadId, newVal);
}
/**
* Track the #of read locks by thread IFF this is a read/write index
* view.
*/
private void readUnlockedThread() {
final long thisThreadId = Thread.currentThread().getId();
final Integer entry = threadLockMap.get(thisThreadId);
assert entry != null;
if (entry.intValue() == 1) {
threadLockMap.remove(thisThreadId);
} else {
threadLockMap.put(thisThreadId, entry.intValue() - 1);
}
}
WrappedReadLock(final Lock delegate) {
if (delegate == null)
throw new IllegalArgumentException();
this.delegate = delegate;
/*
* Configure parallelism default.
*
* Note: This CHM is ONLY used by mutable index views. So what
* matters here is the #of threads that contend for a mutable index
* view. I suspect that this is significantly fewer threads than we
* observe for concurrent read-only index views. Therefore I have
* set the parameters for the map based on the notion that only a
* few threads are contending for the mutable index object in order
* to reduce the heap burden associated with these CHM instances. If
* this map is observed to be hot spot, then we can simply use the
* defaults (initialCapacity = concurrencyLevel = 16). We only have
* this for the mutable index views and there are typically not that
* many instances of those open at the same time.
*/
final int initialCapacity = 4;
final int concurrencyLevel = initialCapacity;
final float loadFactor = .75f;
this.threadLockMap = new ConcurrentHashMap(
initialCapacity, loadFactor, concurrencyLevel);
}
@Override
public void lock() {
try {
/*
* Note: The original UnisolatedReadWriteLock semantics are
* always those of a tryLock with a default timeout. Make sure
* that we keep this in place!
*/
lockInterruptibly();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
/*
* Note: The order in which we obtain the real read lock and
* increment (and decrement) the per-thread read lock counter on the
* AbstractBTree is not critical because AbstractBTree.touch()
* relies on the thread both owning the read lock and having the
* per-thread read lock counter incremented for that thread.
*
* Note: The original UnisolatedReadWriteLock semantics are always
* those of a tryLock with a default timeout. Make sure that we keep
* this in place!
*/
// delegate.lock();
if (!delegate.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timeout");
}
readLockedThread();
}
@Override
public boolean tryLock() {
final boolean ret = delegate.tryLock();
if (ret) {
readLockedThread();
}
return ret;
}
@Override
public boolean tryLock(final long time, final TimeUnit unit)
throws InterruptedException {
final boolean ret = delegate.tryLock(time, unit);
if (ret) {
readLockedThread();
}
return ret;
}
@Override
public void unlock() {
/*
* Note: The unlock order does not really matter. See the
* comments on lock() and AbstractBTree.touch().
*/
delegate.unlock();
/*
* Do this after the unlock() in case the lock/unlock are not
* correctly paired.
*/
readUnlockedThread();
}
@Override
public Condition newCondition() {
return delegate.newCondition();
}
} // class WrappedReadLock
/**
* Wraps the write lock to provide interruptable tryLock() with timeout
* semantics for all write lock acquisitions.
*
* @author Bryan
* Thompson
*
* @see AssertionError: Child
* does not have persistent identity
*/
private class WrappedWriteLock implements Lock {
private final Lock delegate;
WrappedWriteLock(final Lock delegate) {
if (delegate == null)
throw new IllegalArgumentException();
this.delegate = delegate;
}
@Override
public void lock() {
try {
/*
* Note: The original UnisolatedReadWriteLock semantics are
* always those of a tryLock with a default timeout. Make sure
* that we keep this in place!
*/
lockInterruptibly();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
/*
* Note: The original UnisolatedReadWriteLock semantics are always
* those of a tryLock with a default timeout. Make sure that we keep
* this in place!
*/
// delegate.lock();
if (!delegate.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timeout");
}
}
@Override
public boolean tryLock() {
return delegate.tryLock();
}
@Override
public boolean tryLock(final long time, final TimeUnit unit)
throws InterruptedException {
return delegate.tryLock(time, unit);
}
@Override
public void unlock() {
delegate.unlock();
}
@Override
public Condition newCondition() {
return delegate.newCondition();
}
} // class WrappedWriteLock
/**
* Class used for read lock for read-only data structures.
*
* @author Bryan
* Thompson
*/
private static class ConcurrentReaderLock implements Lock {
@Override
public void lock() {
// NOP
}
@Override
public void lockInterruptibly() throws InterruptedException {
// NOP
}
@Override
public boolean tryLock() {
return true;
}
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return true;
}
@Override
public void unlock() {
// NOP
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
private static final Lock READ_ONLY_LOCK = new ConcurrentReaderLock();
/**
* Canonicalizing factory for the {@link ReadWriteLock} for an
* {@link ICommitter}.
*
* Note: This method CAN NOT be exposed since that breaks encapsulation for
* the {@link WrappedReadLock}.
*
* @param index
* The btree.
* @return The lock.
*
* @throws IllegalArgumentException
* if the argument is null
.
*/
static public ReadWriteLockManager getLockManager(
final ICheckpointProtocol index) {
if (index == null)
throw new IllegalArgumentException();
synchronized (locks) {
ReadWriteLockManager lockManager = locks.get(index);
if (lockManager == null) {
lockManager = new ReadWriteLockManager(index);
locks.put(index, lockManager);
}
return lockManager;
}
}
/**
* Note: ONLY accessed through the canonicalizing pattern!
*/
private ReadWriteLockManager(final ICheckpointProtocol index) {
// this.committer = index;
if (this.readOnly = index.isReadOnly()) {
/*
* Since the index does not allow mutation, wrap with a NOP lock.
*
* Note: Concurrent readers are automatically supported by our
* persistent capable data structures so we return a NOP Lock
* implementation if the data structure is read-only. Also note that
* read-only data structures are (by definition) not mutable so we
* do not need to track the #of reentrant locks held for a read-only
* data structure (per above).
*/
this.readLock = READ_ONLY_LOCK;
this.writeLock = null;
} else {
/*
* Note: fairness is NOT required for the locks. I believe that this
* is supposed to provide better throughput, but that has not been
* tested. Also, this has not been tested with a simple mutex lock
* vs a read-write lock. The use case for which this class was
* originally developed was computing the fix point of a set of
* rules. In that use case, we do a lot of concurrent reading and
* periodically flush the computed solutions onto the relations. It
* is likely that a read-write lock will do well for this situation.
*/
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(
false/* fair */);
/**
* If the index allows mutation, then wrap with tryLock() and
* lock-counting semantics. This allows us to test for the #of
* reentrant locks held by the current thread in
* AbstractBTree.touch() and is the primary basis for the fix the
* ticket below.
*
* @see
* AssertionError: Child does not have persistent identity
*/
this.readLock = new WrappedReadLock(readWriteLock.readLock());
// Wrap with tryLock() semantics.
this.writeLock = new WrappedWriteLock(readWriteLock.writeLock());
}
}
// @Override
// final public String toString() {
//
// return getClass().getName() + "{committer=" + committer + ",readOnly="
// + readOnly + "}";
//
// }
}