/* * Copyright (c) 2002-2009 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.kernel.impl.transaction; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.concurrent.atomic.AtomicInteger; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.neo4j.graphdb.TransactionFailureException; import org.neo4j.kernel.DeadlockDetectedException; import org.neo4j.kernel.impl.util.ArrayMap; /** * The Resource Allocation Graph manager is used for deadlock detection. It * keeps track of all locked resources and transactions waiting for resources. * When a {@link RWLock} cannot give the lock to a transaction the tx has to * wait and that may lead to a deadlock. So before the tx is put into wait mode * the {@link RagManager#checkWaitOn} method is invoked to check if a wait of * this transaction will lead to a deadlock. * <p> * The <CODE>checkWaitOn</CODE> throws a {@link DeadlockDetectedException} if * a deadlock would occur when the transaction would wait for the resource. That * will guarantee that a deadlock never occurs on a RWLock basis. * <p> * Think of the resource allocation graph as a node space. We have two node * types, resource nodes (R) and tx/process nodes (T). When a transaction * acquires lock on some resource a relationship is added from the resource to * the tx (R->T) and when a transaction waits for a resource a relationship is * added from the tx to the resource (T->R). The only thing we need to do to see * if a deadlock occurs when some transaction waits for a resource is to * traverse node nodespace starting on the resource and see if we can get back * to the tx ( T1 wants to wait on R1 and R1->T2->R2->T3->R8->T1 <==> * deadlock!). */ class RagManager { // if a runtime exception is thrown from any method it means that the // RWLock class hasn't kept the contract to the RagManager // The contract is: // o When a transaction gets a lock on a resource and both the readCount and // writeCount for that transaction on the resource was 0 // RagManager.lockAcquired( resource ) must be invoked // o When a tx releases a lock on a resource and both the readCount and // writeCount for that transaction on the resource goes down to zero // RagManager.lockReleased( resource ) must be invoked // o After invoke to the checkWaitOn( resource ) method that didn't result // in a DeadlockDetectedException the transaction must wait // o When the transaction wakes up from waiting on a resource the // stopWaitOn( resource ) method must be invoked private final Map<Object,List<Transaction>> resourceMap = new HashMap<Object,List<Transaction>>(); private final ArrayMap<Transaction,Object> waitingTxMap = new ArrayMap<Transaction,Object>( 5, false, true ); private final TransactionManager tm; private final AtomicInteger deadlockCount = new AtomicInteger(); RagManager( TransactionManager tm ) { this.tm = tm; } long getDeadlockCount() { return deadlockCount.longValue(); } synchronized void lockAcquired( Object resource, Transaction tx ) { List<Transaction> lockingTxList = resourceMap.get( resource ); if ( lockingTxList != null ) { assert !lockingTxList.contains( tx ); lockingTxList.add( tx ); } else { lockingTxList = new LinkedList<Transaction>(); lockingTxList.add( tx ); resourceMap.put( resource, lockingTxList ); } } synchronized void lockReleased( Object resource, Transaction tx ) { List<Transaction> lockingTxList = resourceMap.get( resource ); if ( lockingTxList == null ) { throw new LockException( resource + " not found in resource map" ); } if ( !lockingTxList.remove( tx ) ) { throw new LockException( tx + "not found in locking tx list" ); } if ( lockingTxList.size() == 0 ) { resourceMap.remove( resource ); } } synchronized void stopWaitOn( Object resource, Transaction tx ) { if ( waitingTxMap.remove( tx ) == null ) { throw new LockException( tx + " not waiting on " + resource ); } } // after invoke the transaction must wait on the resource synchronized void checkWaitOn( Object resource, Transaction tx ) throws DeadlockDetectedException { List<Transaction> lockingTxList = resourceMap.get( resource ); if ( lockingTxList == null ) { throw new LockException( "Illegal resource[" + resource + "], not found in map" ); } if ( waitingTxMap.get( tx ) != null ) { throw new LockException( tx + " already waiting for resource" ); } Iterator<Transaction> itr = lockingTxList.iterator(); List<Transaction> checkedTransactions = new LinkedList<Transaction>(); Stack<Object> graphStack = new Stack<Object>(); // has resource,transaction interleaved graphStack.push( resource ); while ( itr.hasNext() ) { Transaction lockingTx = itr.next(); // the if statement bellow is valid because: // t1 -> r1 -> t1 (can happened with RW locks) is ok but, // t1 -> r1 -> t1&t2 where t2 -> r1 is a deadlock // think like this, we have two transactions and one resource // o t1 takes read lock on r1 // o t2 takes read lock on r1 // o t1 wanna take write lock on r1 but has to wait for t2 // to release the read lock ( t1->r1->(t1&t2), ok not deadlock yet // o t2 wanna take write lock on r1 but has to wait for t1 // to release read lock.... // DEADLOCK t1->r1->(t1&t2) and t2->r1->(t1&t2) ===> // t1->r1->t2->r1->t1, t2->r1->t1->r1->t2 etc... // to allow the first three steps above we check if lockingTx == // waitingTx on first level. // because of this special case we have to keep track on the // already "checked" tx since it is (now) legal for one type of // circular reference to exist (t1->r1->t1) otherwise we may // traverse t1->r1->t2->r1->t2->r1->t2... until SOE // ... KISS to you too if ( lockingTx.equals( tx ) ) { continue; } graphStack.push( tx ); checkWaitOnRecursive( lockingTx, tx, checkedTransactions, graphStack ); graphStack.pop(); } // ok no deadlock, we can wait on resource waitingTxMap.put( tx, resource ); } private synchronized void checkWaitOnRecursive( Transaction lockingTx, Transaction waitingTx, List<Transaction> checkedTransactions, Stack<Object> graphStack ) throws DeadlockDetectedException { if ( lockingTx.equals( waitingTx ) ) { StringBuffer circle = null; Object resource = null; do { lockingTx = (Transaction) graphStack.pop(); resource = graphStack.pop(); if ( circle == null ) { circle = new StringBuffer(); circle.append( lockingTx + " <- " + resource ); } else { circle.append( " <- " + lockingTx + " <- " + resource ); } } while ( !graphStack.isEmpty() ); deadlockCount.incrementAndGet(); throw new DeadlockDetectedException( waitingTx + " can't wait on resource " + resource + " since => " + circle ); } checkedTransactions.add( lockingTx ); Object resource = waitingTxMap.get( lockingTx ); if ( resource != null ) { graphStack.push( resource ); // if the resource doesn't exist in resorceMap that means all the // locks on the resource has been released // it is possible when this tx was in RWLock.acquire and // saw it had to wait for the lock the scheduler changes to some // other tx that will release the locks on the resource and // remove it from the map // this is ok since current tx or any other tx will wake // in the synchronized block and will be forced to do the deadlock // check once more if lock cannot be acquired List<Transaction> lockingTxList = resourceMap.get( resource ); if ( lockingTxList != null ) { Iterator<Transaction> itr = lockingTxList.iterator(); while ( itr.hasNext() ) { lockingTx = itr.next(); // so we don't if ( !checkedTransactions.contains( lockingTx ) ) { graphStack.push( lockingTx ); checkWaitOnRecursive( lockingTx, waitingTx, checkedTransactions, graphStack ); graphStack.pop(); } } } graphStack.pop(); } } synchronized void dumpStack() { System.out.print( "Waiting list: " ); Iterator<Transaction> transactions = waitingTxMap.keySet().iterator(); if ( !transactions.hasNext() ) { System.out.println( "No transactions waiting on resources" ); } else { System.out.println(); } while ( transactions.hasNext() ) { Transaction tx = transactions.next(); System.out.println( "" + tx + "->" + waitingTxMap.get( tx ) ); } System.out.print( "Resource lock list: " ); Iterator<?> resources = resourceMap.keySet().iterator(); if ( !resources.hasNext() ) { System.out.println( "No locked resources found" ); } else { System.out.println(); } while ( resources.hasNext() ) { Object resource = resources.next(); System.out.print( "" + resource + "->" ); Iterator<Transaction> itr = resourceMap.get( resource ).iterator(); if ( !itr.hasNext() ) { System.out.println( " Error empty list found" ); } while ( itr.hasNext() ) { System.out.print( "" + itr.next() ); if ( itr.hasNext() ) { System.out.print( "," ); } else { System.out.println(); } } } } Transaction getCurrentTransaction() { try { return tm.getTransaction(); } catch ( SystemException e ) { throw new TransactionFailureException( "Could not get current transaction.", e ); } } }