/*
* eXist Open Source Native XML Database
* Copyright (C) 2005-2007 The eXist Project
* http://exist-db.org
*
* 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 2
* of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Original code is
*
* Copyright 2001-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* $Id$
*/
package org.exist.storage.lock;
import org.apache.log4j.Logger;
import org.exist.util.DeadlockException;
import org.exist.util.LockException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
/**
* A reentrant read/write lock, which allows multiple readers to acquire a lock.
* Waiting writers are preferred.
* <p/>
* This is an adapted and bug-fixed version of code taken from Apache's Turbine
* JCS.
*/
public class MultiReadReentrantLock implements Lock {
private final static Logger LOG = Logger.getLogger(MultiReadReentrantLock.class);
private Object id;
/**
* Number of threads waiting to read.
*/
private int waitingForReadLock = 0;
/**
* Number of threads reading.
*/
private List<LockOwner> outstandingReadLocks = new ArrayList<LockOwner>(4);
/**
* The thread that has the write lock or null.
*/
private Thread writeLockedThread;
/**
* The number of (nested) write locks that have been requested from
* writeLockedThread.
*/
private int outstandingWriteLocks = 0;
/**
* Threads waiting to get a write lock are tracked in this ArrayList to
* ensure that write locks are issued in the same order they are requested.
*/
private List<WaitingThread> waitingForWriteLock = null;
/**
* Default constructor.
*/
public MultiReadReentrantLock(Object id) {
this.id = id;
}
public String getId() {
return id.toString();
}
/* @deprecated Use other method
* @see org.exist.storage.lock.Lock#acquire()
*/
public boolean acquire() throws LockException {
return acquire(Lock.READ_LOCK);
}
public boolean acquire(int mode) throws LockException {
if (mode == Lock.NO_LOCK) {
LOG.warn("acquired with no lock !");
return true;
}
switch (mode) {
case Lock.WRITE_LOCK:
return writeLock(true);
default:
return readLock(true);
}
}
/* (non-Javadoc)
* @see org.exist.util.Lock#attempt(int)
*/
public boolean attempt(int mode) {
try {
switch (mode) {
case Lock.WRITE_LOCK:
return writeLock(false);
default:
return readLock(false);
}
} catch (final LockException e) {
return false;
}
}
/**
* Issue a read lock if there is no outstanding write lock or threads
* waiting to get a write lock. Caller of this method must be careful to
* avoid synchronizing the calling code so as to avoid deadlock.
* @param waitIfNecessary whether to wait if the lock is not available right away
*/
private synchronized boolean readLock(boolean waitIfNecessary) throws LockException {
final Thread thisThread = Thread.currentThread();
if (writeLockedThread == thisThread) {
// add acquired lock to the current list of read locks
outstandingReadLocks.add(new LockOwner(thisThread));
//LOG.debug("Thread already holds a write lock");
return true;
}
deadlockCheck();
waitingForReadLock++;
if (writeLockedThread != null) {
if (!waitIfNecessary) {return false;}
final WaitingThread waiter = new WaitingThread(thisThread, this, this, Lock.READ_LOCK);
DeadlockDetection.addResourceWaiter(thisThread, waiter);
while (writeLockedThread != null) {
//LOG.debug("readLock wait by " + thisThread.getName() + " for " + getId());
waiter.doWait();
//LOG.debug("wake up from readLock wait");
}
DeadlockDetection.clearResourceWaiter(thisThread);
} waitingForReadLock--;
//Add acquired lock to the current list of read locks
outstandingReadLocks.add(new LockOwner(thisThread));
return true;
}
/**
* Issue a write lock if there are no outstanding read or write locks.
* Caller of this method must be careful to avoid synchronizing the calling
* code so as to avoid deadlock.
* @param waitIfNecessary whether to wait if the lock is not available right away
*/
private boolean writeLock(boolean waitIfNecessary) throws LockException {
Thread thisThread = Thread.currentThread();
WaitingThread waiter;
synchronized (this) {
if (writeLockedThread == thisThread) {
outstandingWriteLocks++;
return true;
}
if (writeLockedThread == null && grantWriteLock()) {
writeLockedThread = thisThread;
outstandingWriteLocks++;
return true;
}
if (!waitIfNecessary)
{return false;}
deadlockCheck();
if (waitingForWriteLock == null)
{waitingForWriteLock = new ArrayList<WaitingThread>(3);}
waiter = new WaitingThread(thisThread, thisThread, this, Lock.WRITE_LOCK);
addWaitingWrite(waiter);
DeadlockDetection.addResourceWaiter(thisThread, waiter);
}
List<WaitingThread> deadlockedThreads = null;
LockException exceptionCaught = null;
synchronized (thisThread) {
if (thisThread != writeLockedThread) {
while (thisThread != writeLockedThread && deadlockedThreads == null) {
if (LockOwner.DEBUG) {
final StringBuffer buf = new StringBuffer("Waiting for write: ");
for (int i = 0; i < waitingForWriteLock.size(); i++) {
buf.append(' ');
buf.append((waitingForWriteLock.get(i)).getThread().getName());
}
LOG.debug(buf.toString());
debugReadLocks("WAIT");
}
deadlockedThreads = checkForDeadlock(thisThread);
if (deadlockedThreads == null) {
try {
waiter.doWait();
} catch (LockException e) {
//Don't throw the exception now, leave the synchronized block and clean up first
exceptionCaught = e;
break;
}
}
}
}
if (deadlockedThreads == null && exceptionCaught == null)
{outstandingWriteLocks++;}
}
synchronized (this) {
DeadlockDetection.clearResourceWaiter(thisThread);
removeWaitingWrite(waiter);
}
if (exceptionCaught != null)
{throw exceptionCaught;}
if (deadlockedThreads != null) {
for (final WaitingThread wt : deadlockedThreads) {
wt.signalDeadlock();
}
throw new DeadlockException();
}
return true;
}
private void addWaitingWrite(WaitingThread waiter) {
waitingForWriteLock.add(waiter);
}
private void removeWaitingWrite(WaitingThread waiter) {
for (int i = 0; i < waitingForWriteLock.size(); i++) {
final WaitingThread next = waitingForWriteLock.get(i);
if (next.getThread() == waiter.getThread()) {
waitingForWriteLock.remove(i);
break;
}
}
}
/* @deprecated : use other method
* @see org.exist.storage.lock.Lock#release()
*/
public void release() {
release(Lock.READ_LOCK);
}
public void release(int mode) {
switch (mode) {
case Lock.NO_LOCK:
break;
case Lock.WRITE_LOCK:
releaseWrite(1);
break;
//TODO : use READ_LOCK ? -pb
default:
releaseRead(1);
break;
}
}
public void release(int mode, int count) {
switch (mode) {
case Lock.WRITE_LOCK:
releaseWrite(count);
break;
//TODO : use READ_LOCK ? -pb
default:
releaseRead(count);
break;
}
}
private synchronized void releaseWrite(int count) {
if (Thread.currentThread() == writeLockedThread) {
if (outstandingWriteLocks > 0)
{outstandingWriteLocks -= count;}
if (outstandingWriteLocks > 0) {
return;
}
//If another thread is waiting for a write lock, we immediately
//pass control to it. No further checks should be required here.
if (grantWriteLockAfterRead()) {
final WaitingThread waiter = waitingForWriteLock.get(0);
removeWaitingWrite(waiter);
DeadlockDetection.clearResourceWaiter(waiter.getThread());
writeLockedThread = waiter.getThread();
synchronized (writeLockedThread) {
writeLockedThread.notifyAll();
}
} else {
writeLockedThread = null;
if (waitingForReadLock > 0) {
//Wake up pending read locks
notifyAll();
}
}
} else {
LOG.warn("Possible lock problem: a thread released a write lock it didn't hold. Either the " +
"thread was interrupted or it never acquired the lock.", new Throwable());
//TODO : throw exception ? -pb
}
}
/**
* Threads call this method to relinquish a lock that they previously got
* from this object.
*
* @throws IllegalStateException if called when there are no outstanding locks or there is a
* write lock issued to a different thread.
*/
private synchronized void releaseRead(int count) {
if (!outstandingReadLocks.isEmpty()) {
removeReadLock(count);
if (writeLockedThread == null && grantWriteLockAfterRead()) {
final WaitingThread waiter = waitingForWriteLock.get(0);
removeWaitingWrite(waiter);
DeadlockDetection.clearResourceWaiter(waiter.getThread());
writeLockedThread = waiter.getThread();
synchronized (writeLockedThread) {
writeLockedThread.notifyAll();
}
}
return;
} else {
LOG.warn("Possible lock problem: thread " + Thread.currentThread().getName() +
" released a read lock it didn't hold. Either the " +
"thread was interrupted or it never acquired the lock. " +
"Write lock: " + (writeLockedThread != null ? writeLockedThread.getName() : "null"),
new Throwable());
if (LockOwner.DEBUG)
{debugReadLocks("ILLEGAL RELEASE");}
//TODO : throw exception ? -pb
}
}
public synchronized boolean isLockedForWrite() {
return writeLockedThread != null || (waitingForWriteLock != null && waitingForWriteLock.size() > 0);
}
public synchronized boolean hasLock() {
return !outstandingReadLocks.isEmpty() || isLockedForWrite();
}
public synchronized boolean isLockedForRead(Thread owner) {
for (int i = outstandingReadLocks.size() - 1; i > -1; i--) {
if ((outstandingReadLocks.get(i)).getOwner() == owner)
{return true;}
}
return false;
}
private void removeReadLock(int count) {
final Object owner = Thread.currentThread();
for (int i = outstandingReadLocks.size() - 1; i > -1 && count > 0; i--) {
final LockOwner current = outstandingReadLocks.get(i);
if (current.getOwner() == owner) {
outstandingReadLocks.remove(i);
--count;
}
}
}
private void deadlockCheck() throws DeadlockException {
for (final LockOwner next : outstandingReadLocks) {
final Lock lock = DeadlockDetection.isWaitingFor(next.getOwner());
if (lock != null) {
lock.wakeUp();
}
}
}
/**
* Detect circular wait on different resources: thread A has a write lock on
* resource R1; thread B has a write lock on resource R2; thread A tries to
* acquire lock on R2; thread B now tries to acquire lock on R1. Solution:
* suspend existing write lock of thread A and grant it to B.
*
* @return true if the write lock should be granted to the current thread
*/
private List<WaitingThread> checkForDeadlock(Thread waiter) {
final ArrayList<WaitingThread> waiters = new ArrayList<WaitingThread>(10);
if (DeadlockDetection.wouldDeadlock(waiter, writeLockedThread, waiters)) {
LOG.warn("Potential deadlock detected on lock " + getId() + "; killing threads: " + waiters.size());
return waiters.size() > 0 ? waiters : null;
}
return null;
}
/**
* Check if a write lock can be granted, either because there are no
* read locks, the read lock belongs to the current thread and can be
* upgraded or the thread which holds the lock is blocked by another
* lock held by the current thread.
*
* @return true if the write lock can be granted
*/
private boolean grantWriteLock() {
if (outstandingReadLocks.isEmpty()) {
return true;
}
final Thread waiter = Thread.currentThread();
//Walk through outstanding read locks
for (final LockOwner next : outstandingReadLocks) {
//If the read lock is owned by the current thread, all is OK and we continue
if (next.getOwner() != waiter) {
//Otherwise, check if the lock belongs to a thread which is currently blocked
//by a lock owned by the current thread. if yes, it will be safe to grant the
//write lock: the other thread will be blocked anyway.
if (!DeadlockDetection.isBlockedBy(waiter, next.getOwner())) {
return false;
}
}
}
return true;
}
/**
* Check if a write lock can be granted, either because there are no
* read locks or the read lock belongs to the current thread and can be
* upgraded. This method is called whenever a lock is released.
*
* @return true if the write lock can be granted
*/
private boolean grantWriteLockAfterRead() {
//Waiting write locks?
if (waitingForWriteLock != null && waitingForWriteLock.size() > 0) {
//Yes, check read locks
final int size = outstandingReadLocks.size();
if (size > 0) {
//Grant lock if all read locks are held by the write thread
final WaitingThread waiter = waitingForWriteLock.get(0);
return isCompatible(waiter.getThread());
}
return true;
}
return false;
}
/**
* Check if the specified thread has a read lock on the resource.
*
* @param owner the thread
* @return true if owner has a read lock
*/
private boolean hasReadLock(Thread owner) {
for (final LockOwner next : outstandingReadLocks) {
if (next.getOwner() == owner)
{return true;}
}
return false;
}
public Thread getWriteLockedThread() {
return writeLockedThread;
}
/**
* Check if the specified thread holds either a write or a read lock
* on the resource.
*
* @param owner the thread
* @return true if owner has a lock
*/
public boolean hasLock(Thread owner) {
if (writeLockedThread == owner)
{return true;}
return hasReadLock(owner);
}
public void wakeUp() {
//Nothing to do
}
/**
* Check if the pending request for a write lock is compatible
* with existing read locks and other write requests. A lock request is
* compatible with another lock request if: (a) it belongs to the same thread,
* (b) it belongs to a different thread, but this thread is also waiting for a write lock.
*
* @param waiting
* @return true if the lock request is compatible with all other requests and the
* lock can be granted.
*/
private boolean isCompatible(Thread waiting) {
for (final LockOwner next : outstandingReadLocks) {
//If the read lock is owned by the current thread, all is OK and we continue
if (next.getOwner() != waiting) {
//Otherwise, check if the lock belongs to a thread which is currently blocked
//by a lock owned by the current thread. if yes, it will be safe to grant the
//write lock: the other thread will be blocked anyway.
if (!DeadlockDetection.isBlockedBy(waiting, next.getOwner())) {
return false;
}
}
}
return true;
}
public synchronized LockInfo getLockInfo() {
LockInfo info;
String[] readers = new String[0];
if (outstandingReadLocks != null) {
readers = new String[outstandingReadLocks.size()];
for (int i = 0; i < outstandingReadLocks.size(); i++) {
final LockOwner owner = outstandingReadLocks.get(i);
readers[i] = owner.getOwner().getName();
}
}
if (writeLockedThread != null) {
info = new LockInfo(LockInfo.RESOURCE_LOCK, LockInfo.WRITE_LOCK, getId(),
new String[] {writeLockedThread.getName()});
info.setReadLocks(readers);
} else {
info = new LockInfo(LockInfo.RESOURCE_LOCK, LockInfo.READ_LOCK, getId(), readers);
}
if (waitingForWriteLock != null) {
final String waitingForWrite[] = new String[waitingForWriteLock.size()];
for (int i = 0; i < waitingForWriteLock.size(); i++) {
waitingForWrite[i] = waitingForWriteLock.get(i).getThread().getName();
}
info.setWaitingForWrite(waitingForWrite);
}
return info;
}
private void debugReadLocks(String msg) {
for (final LockOwner owner : outstandingReadLocks) {
LOG.debug(msg + ": " + owner.getOwner(), owner.getStack());
}
}
@Override
public void debug(PrintStream out) {
getLockInfo().debug(out);
}
}