/*
* eXist Open Source Native XML Database
* Copyright (C) 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.
*
* $Id$
*/
package org.exist.storage.lock;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* Deadlock detection for resource and collection locks. The static methods in this class
* keep track of all waiting threads, which are currently waiting on a resource or collection
* lock. In some scenarios (e.g. a complex XQuery which modifies resources), a single thread
* may acquire different read/write locks on resources in a collection. The locks can be arbitrarily
* nested. For example, a thread may first acquire a read lock on a collection, then a read lock on
* a resource and later acquires a write lock on the collection to remove the resource.
*
* Since we have locks on both, collections and resources, deadlock situations are sometimes
* unavoidable. For example, imagine the following scenario:
*
* <ul>
* <li>T1 owns write lock on resource</li>
* <li>T2 owns write lock on collection</li>
* <li>T2 wants to acquire write lock on resource locked by T1</li>
* <li>T1 tries to acquire write lock on collection currently locked by T2</li>
* <li>DEADLOCK</li>
* </ul>
*
* The code should probably be redesigned to avoid this kind of crossed collection-resource
* locking, which easily leads to circular wait conditions. However, this needs to be done with care. In
* the meantime, DeadlockDetection is used to detect deadlock situations as the one described
* above. The lock classes can
* then try to resolve the deadlock by suspending one thread.
*/
public class DeadlockDetection {
private final static ReentrantLock lock = new ReentrantLock();
private final static Map waitForResource = new HashMap();
private final static Map waitForCollection = new HashMap();
public static ReentrantLock getLock() {
return lock;
}
/**
* Register a thread as waiting for a resource lock.
*
* @param thread the thread
* @param waiter the WaitingThread object which wraps around the thread
*/
public static void addResourceWaiter(Thread thread, WaitingThread waiter) {
lock.lock();
try {
waitForResource.put(thread, waiter);
} finally {
lock.unlock();
}
}
/**
* Deregister a waiting thread.
*
* @param thread
* @return lock
*/
public static Lock clearResourceWaiter(Thread thread) {
lock.lock();
try {
WaitingThread waiter = (WaitingThread) waitForResource.remove(thread);
if (waiter != null)
return waiter.getLock();
return null;
} finally {
lock.unlock();
}
}
public static WaitingThread getResourceWaiter(Thread thread) {
lock.lock();
try {
return (WaitingThread) waitForResource.get(thread);
} finally {
lock.unlock();
}
}
/**
* Check if there's a risk for a circular wait between threadA and threadB. The method tests if
* threadB is currently waiting for a resource lock (read or write). It then checks
* if threadA holds a lock on this resource. If yes, the {@link org.exist.storage.lock.WaitingThread}
* object for threadB is returned. This object can be used to suspend the waiting thread
* in order to temporarily yield the lock to threadA.
*
* @param threadA
* @param threadB
* @return waiting thread
*/
public static WaitingThread deadlockCheckResource(Thread threadA, Thread threadB) {
lock.lock();
try {
// check if threadB is waiting for a resource lock
WaitingThread waitingThread = (WaitingThread) waitForResource.get(threadB);
// if lock != null, check if thread B waits for a resource lock currently held by thread A
if (waitingThread != null) {
// LOG.debug("deadlockCheck: " + threadB.getName() + " -> " + waitingThread.getLock().hasLock(threadA));
return waitingThread.getLock().hasLock(threadA) ? waitingThread : null;
}
return null;
} finally {
lock.unlock();
}
}
/**
* Check if the second thread is currently waiting for a resource lock and
* is blocked by the first thread.
*
* @param threadA the thread whose lock might be blocking threadB
* @param threadB the thread to check
* @return true if threadB is currently blocked by a lock held by threadA
*/
public static boolean isBlockedBy(Thread threadA, Thread threadB) {
lock.lock();
try {
// check if threadB is waiting for a resource lock
WaitingThread waitingThread = (WaitingThread) waitForResource.get(threadB);
// if lock != null, check if thread B waits for a resource lock currently held by thread A
if (waitingThread != null) {
return waitingThread.getLock().hasLock(threadA);
}
return false;
} finally {
lock.unlock();
}
}
public static boolean wouldDeadlock(Thread waiter, Thread owner, List waiters) {
lock.lock();
try {
WaitingThread wt = (WaitingThread) waitForResource.get(owner);
if (wt != null) {
if (waiters.contains(wt)) {
// probably a deadlock, but not directly connected to the current thread
// return to avoid endless loop
return false;
}
waiters.add(wt);
Lock l = wt.getLock();
Thread t = ((MultiReadReentrantLock) l).getWriteLockedThread();
if (t == owner) {
// System.out.println("Waiter: " + waiter.getName() + " Thread: " + t.getName() + " == " + owner.getName() +
// " type: " + wt.getLockType());
// debug(t.getName(), l.getLockInfo());
// the thread acquired the lock in the meantime
return false;
}
if (t != null) {
if (t == waiter)
return true;
return wouldDeadlock(waiter, t, waiters);
} else
return false;
} else {
Lock l = (Lock) waitForCollection.get(owner);
if (l != null) {
Thread t = ((ReentrantReadWriteLock) l).getOwner();
if (t == owner) {
// System.out.println("Thread " + t.getName() + " == " + owner.getName());
// debug(t.getName(), l.getLockInfo());
// the thread acquired the lock in the meantime
return false;
}
if (t != null) {
if (t == waiter)
return true;
return wouldDeadlock(waiter, t, waiters);
}
}
}
return false;
} finally {
lock.unlock();
}
}
/**
* Register a thread as waiting for a resource lock.
*
* @param waiter the thread
* @param collectionLock the lock object
*/
public static void addCollectionWaiter(Thread waiter, Lock collectionLock) {
lock.lock();
try {
waitForCollection.put(waiter, collectionLock);
} finally {
lock.unlock();
}
}
public static Lock clearCollectionWaiter(Thread waiter) {
lock.lock();
try {
return (Lock) waitForCollection.remove(waiter);
} finally {
lock.unlock();
}
}
public static Lock isWaitingFor(Thread waiter) {
lock.lock();
try {
return (Lock) waitForCollection.get(waiter);
} finally {
lock.unlock();
}
}
public static Map getWaitingThreads() {
Map table = new HashMap();
for (Iterator i = waitForResource.values().iterator(); i.hasNext(); ) {
WaitingThread waitingThread = (WaitingThread) i.next();
table.put(waitingThread.getThread().getName(), waitingThread.getLock().getLockInfo());
}
for (Iterator i = waitForCollection.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry) i.next();
Thread thread = (Thread) entry.getKey();
table.put(thread.getName(), ((Lock)entry.getValue()).getLockInfo());
}
return table;
}
public static void debug(String name, LockInfo info) {
StringWriter sout = new StringWriter();
PrintWriter writer = new PrintWriter(sout);
debug(writer, name, info);
writer.flush();
writer.close();
System.out.println(sout.toString());
}
public static void debug(PrintWriter writer, String name, LockInfo info) {
writer.println("THREAD: " + name);
if (info != null) {
writer.println("Lock type: " + info.getLockType());
writer.println("Lock mode: " + info.getLockMode());
writer.println("Lock id: " + info.getId());
writer.println("Held by: " + arrayToString(info.getOwners()));
writer.println("Read locks: " + arrayToString(info.getReadLocks()));
writer.println("Wait for read: " + arrayToString(info.getWaitingForRead()));
writer.println("Wait for write: " + arrayToString(info.getWaitingForWrite()));
}
}
public static void debug() {
StringWriter sout = new StringWriter();
PrintWriter writer = new PrintWriter(sout);
Map threads = getWaitingThreads();
for (Iterator i = threads.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
debug(writer, entry.getKey().toString(), (LockInfo) entry.getValue());
}
writer.close();
System.out.println(sout.toString());
}
private static String arrayToString(Object[] a) {
if (a == null)
return "null";
if (a.length == 0)
return "[]";
StringBuffer buf = new StringBuffer();
for (int i = 0; i < a.length; i++) {
if (i == 0)
buf.append('[');
else
buf.append(", ");
buf.append(a[i] == null ? "null" : a[i].toString());
}
buf.append("]");
return buf.toString();
}
}