/* * eXist Open Source Native XML Database * Copyright (C) 2001-2015 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 org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 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 Logger LOG = LogManager.getLogger(DeadlockDetection.class); private final static Map<Thread, WaitingThread> waitForResource = new HashMap<>(); private final static Map<Thread, Lock> waitForCollection = new HashMap<>(); /** * 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(final Thread thread, final WaitingThread waiter) { synchronized (DeadlockDetection.class) { waitForResource.put(thread, waiter); } } /** * Deregister a waiting thread. * * @param thread * @return lock */ public static Lock clearResourceWaiter(final Thread thread) { synchronized (DeadlockDetection.class) { final WaitingThread waiter = waitForResource.remove(thread); if (waiter != null) {return waiter.getLock();} return null; } } public static WaitingThread getResourceWaiter(final Thread thread) { synchronized (DeadlockDetection.class) { return waitForResource.get(thread); } } /** * 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(final Thread threadA, final Thread threadB) { synchronized (DeadlockDetection.class) { //Check if threadB is waiting for a resource lock final 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) ? waitingThread : null; } return null; } } /** * 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(final Thread threadA, final Thread threadB) { synchronized (DeadlockDetection.class) { //Check if threadB is waiting for a resource lock final 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; } } public static boolean wouldDeadlock(final Thread waiter, final Thread owner, final List<WaitingThread> waiters) { synchronized (DeadlockDetection.class) { final WaitingThread wt = 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); final Lock l = wt.getLock(); final Thread t = ((MultiReadReentrantLock) l).getWriteLockedThread(); if (t == owner) { return false; } if (t != null) { if (t == waiter) {return true;} return wouldDeadlock(waiter, t, waiters); } return false; } final Lock l = waitForCollection.get(owner); if (l != null) { final Thread t = ((ReentrantReadWriteLock) l).getOwner(); if (t == owner) { return false; } if (t != null) { if (t == waiter) {return true;} return wouldDeadlock(waiter, t, waiters); } } return false; } } /** * Register a thread as waiting for a resource lock. * * @param waiter the thread * @param lock the lock object */ public static void addCollectionWaiter(final Thread waiter, final Lock lock) { synchronized (DeadlockDetection.class) { waitForCollection.put(waiter, lock); } } public static Lock clearCollectionWaiter(final Thread waiter) { synchronized (DeadlockDetection.class) { return waitForCollection.remove(waiter); } } public static Lock isWaitingFor(final Thread waiter) { synchronized (DeadlockDetection.class) { return waitForCollection.get(waiter); } } public static Map<String, LockInfo> getWaitingThreads() { final Map<String, LockInfo> table = new HashMap<>(); for (final WaitingThread waitingThread : waitForResource.values()) { table.put(waitingThread.getThread().getName(), waitingThread.getLock().getLockInfo()); } for (final Map.Entry<Thread, Lock> entry : waitForCollection.entrySet()) { table.put(entry.getKey().getName(), entry.getValue().getLockInfo()); } return table; } public static void debug(final String name, final LockInfo info) { try(final StringWriter sout = new StringWriter(); final PrintWriter writer = new PrintWriter(sout)) { debug(writer, name, info); System.out.println(sout.toString()); } catch(final IOException e) { LOG.error(e.getMessage(), e); } } public static void debug(final PrintWriter writer, final String name, final LockInfo info) { writer.println("Thread: " + name); if (info != null) { writer.format("%20s: %s\n", "Lock type", info.getLockType()); writer.format("%20s: %s\n", "Lock mode", info.getLockMode()); writer.format("%20s: %s\n", "Lock id", info.getId()); writer.format("%20s: %s\n", "Held by", Arrays.toString(info.getOwners())); writer.format("%20s: %s\n", "Held by", Arrays.toString(info.getOwners())); writer.format("%20s: %s\n", "Held by", Arrays.toString(info.getOwners())); writer.format("%20s: %s\n", "Waiting for read", Arrays.toString(info.getWaitingForRead())); writer.format("%20s: %s\n\n", "Waiting for write", Arrays.toString(info.getWaitingForWrite())); } } public static void debug(final PrintWriter writer) { writer.println("Threads currently waiting for a lock:"); writer.println("====================================="); final Map<String, LockInfo> threads = getWaitingThreads(); for (final Map.Entry<String, LockInfo> entry : threads.entrySet()) { debug(writer, entry.getKey(), entry.getValue()); } } }