/*
* 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.Iterator;
import java.util.LinkedList;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;
import javax.transaction.xa.XAResource;
import org.neo4j.kernel.DeadlockDetectedException;
import org.neo4j.kernel.impl.util.ArrayMap;
/**
* A read/write lock is a lock that will allow many transactions to acquire read
* locks as long as there is no transaction holding the write lock.
* <p>
* When a transaction has write lock no other tx is allowed to acquire read or
* write lock on that resource but the tx holding the write lock. If one tx has
* acquired write lock and another tx needs a lock on the same resource that tx
* must wait. When the lock is released the other tx is notified and wakes up so
* it can acquire the lock.
* <p>
* Waiting for locks may lead to a deadlock. Consider the following scenario. Tx
* T1 acquires write lock on resource R1. T2 acquires write lock on R2. Now T1
* tries to acquire read lock on R2 but has to wait since R2 is locked by T2. If
* T2 now tries to acquire a lock on R1 it also has to wait because R1 is locked
* by T1. T2 cannot wait on R1 because that would lead to a deadlock where T1
* and T2 waits forever.
* <p>
* Avoiding deadlocks can be done by keeping a resource allocation graph. This
* class works together with the {@link RagManager} to make sure no deadlocks
* occur.
* <p>
* Waiting transactions are put into a queue and when some tx releases the lock
* the queue is checked for waiting txs. This implementation tries to avoid lock
* starvation and increase performance since only waiting txs that can acquire
* the lock are notified.
*/
class RWLock
{
private int writeCount = 0; // total writeCount
private int readCount = 0; // total readCount
private int marked = 0; // synch helper in LockManager
private final Object resource; // the resource for this RWLock
private final LinkedList<WaitElement> waitingThreadList =
new LinkedList<WaitElement>();
private final ArrayMap<Transaction,TxLockElement> txLockElementMap =
new ArrayMap<Transaction,TxLockElement>( 5, false, true );
private final RagManager ragManager;
RWLock( Object resource, RagManager ragManager )
{
this.resource = resource;
this.ragManager = ragManager;
}
// keeps track of a transactions read and write lock count on this RWLock
private static class TxLockElement
{
final Transaction tx;
int readCount = 0;
int writeCount = 0;
TxLockElement( Transaction tx )
{
this.tx = tx;
}
}
// keeps track of what type of lock a thread is waiting for
private static class WaitElement
{
final TxLockElement element;
final LockType lockType;
final Thread waitingThread;
WaitElement( TxLockElement element, LockType lockType, Thread thread )
{
this.element = element;
this.lockType = lockType;
this.waitingThread = thread;
}
}
synchronized void mark()
{
this.marked++;
}
synchronized boolean isMarked()
{
return marked > 0;
}
/**
* Tries to acquire read lock for current transaction. If
* <CODE>this.writeCount</CODE> is greater than the currents tx's write
* count the transaction has to wait and the {@link RagManager#checkWaitOn}
* method is invoked for deadlock detection.
* <p>
* If the lock can be acquires the lock count is updated on <CODE>this</CODE>
* and the transaction lock element (tle).
*
* @throws DeadlockDetectedException
* if a deadlock is detected
*/
synchronized void acquireReadLock() throws DeadlockDetectedException
{
Transaction tx = ragManager.getCurrentTransaction();
if ( tx == null )
{
tx = new PlaceboTransaction();
}
TxLockElement tle = txLockElementMap.get( tx );
if ( tle == null )
{
tle = new TxLockElement( tx );
}
try
{
while ( writeCount > tle.writeCount )
{
ragManager.checkWaitOn( this, tx );
waitingThreadList.addFirst( new WaitElement( tle,
LockType.READ, Thread.currentThread() ) );
try
{
wait();
}
catch ( InterruptedException e )
{
Thread.interrupted();
}
ragManager.stopWaitOn( this, tx );
}
if ( tle.readCount == 0 && tle.writeCount == 0 )
{
ragManager.lockAcquired( this, tx );
}
readCount++;
tle.readCount++;
// TODO: this put could be optimized?
txLockElementMap.put( tx, tle );
}
finally
{
// if deadlocked, remove marking so lock is removed when empty
marked--;
}
}
/**
* Releases the read lock held by current transaction. If there are waiting
* transactions in the queue they will be interrupted if they can acquire
* the lock.
*/
synchronized void releaseReadLock() throws LockNotFoundException
{
Transaction tx = ragManager.getCurrentTransaction();
if ( tx == null )
{
tx = new PlaceboTransaction();
}
TxLockElement tle = txLockElementMap.get( tx );
if ( tle == null )
{
throw new LockNotFoundException(
"No transaction lock element found for " + tx );
}
if ( tle.readCount == 0 )
{
throw new LockNotFoundException( "" + tx + " don't have readLock" );
}
readCount--;
tle.readCount--;
if ( tle.readCount == 0 && tle.writeCount == 0 )
{
if ( !this.isMarked() )
{
txLockElementMap.remove( tx );
}
ragManager.lockReleased( this, tx );
}
if ( waitingThreadList.size() > 0 )
{
WaitElement we = waitingThreadList.getLast();
if ( we.lockType == LockType.WRITE )
{
// this one is tricky...
// if readCount > 0 we either have to find a waiting read lock
// in the queue or a waiting write lock that has all read
// locks, if none of these are found it means that there
// is a (are) thread(s) that will release read lock(s) in the
// near future...
if ( readCount == we.element.readCount )
{
// found a write lock with all read locks
waitingThreadList.removeLast();
we.waitingThread.interrupt();
}
else
{
java.util.ListIterator<WaitElement> listItr =
waitingThreadList.listIterator(
waitingThreadList.lastIndexOf( we ) );
// hm am I doing the first all over again?
// think I am if cursor is at lastIndex + 0.5 oh well...
while ( listItr.hasPrevious() )
{
we = listItr.previous();
if ( we.lockType == LockType.WRITE
&& readCount == we.element.readCount )
{
// found a write lock with all read locks
listItr.remove();
we.waitingThread.interrupt();
// ----
break;
}
else if ( we.lockType == LockType.READ )
{
// found a read lock, let it do the job...
listItr.remove();
we.waitingThread.interrupt();
}
}
}
}
else
{
// some thread may have the write lock and released a read lock
// if writeCount is down to zero we can interrupt the waiting
// readlock
if ( writeCount == 0 )
{
waitingThreadList.removeLast();
we.waitingThread.interrupt();
}
}
}
}
/**
* Tries to acquire write lock for current transaction. If
* <CODE>this.writeCount</CODE> is greater than the currents tx's write
* count or the read count is greater than the currents tx's read count the
* transaction has to wait and the {@link RagManager#checkWaitOn} method is
* invoked for deadlock detection.
* <p>
* If the lock can be acquires the lock count is updated on <CODE>this</CODE>
* and the transaction lock element (tle).
*
* @throws DeadlockDetectedException
* if a deadlock is detected
*/
synchronized void acquireWriteLock() throws DeadlockDetectedException
{
Transaction tx = ragManager.getCurrentTransaction();
if ( tx == null )
{
tx = new PlaceboTransaction();
}
TxLockElement tle = txLockElementMap.get( tx );
if ( tle == null )
{
tle = new TxLockElement( tx );
}
try
{
while ( writeCount > tle.writeCount || readCount > tle.readCount )
{
ragManager.checkWaitOn( this, tx );
waitingThreadList.addFirst( new WaitElement( tle,
LockType.WRITE, Thread.currentThread() ) );
try
{
wait();
}
catch ( InterruptedException e )
{
Thread.interrupted();
}
ragManager.stopWaitOn( this, tx );
}
if ( tle.readCount == 0 && tle.writeCount == 0 )
{
ragManager.lockAcquired( this, tx );
}
writeCount++;
tle.writeCount++;
// TODO optimize this put?
txLockElementMap.put( tx, tle );
}
finally
{
// if deadlocked, remove marking so lock is removed when empty
marked--;
}
}
/**
* Releases the write lock held by current tx. If write count is zero and
* there are waiting transactions in the queue they will be interrupted if
* they can acquire the lock.
*/
synchronized void releaseWriteLock() throws LockNotFoundException
{
Transaction tx = ragManager.getCurrentTransaction();
if ( tx == null )
{
tx = new PlaceboTransaction();
}
TxLockElement tle = txLockElementMap.get( tx );
if ( tle == null )
{
throw new LockNotFoundException(
"No transaction lock element found for " + tx );
}
if ( tle.writeCount == 0 )
{
throw new LockNotFoundException( "" + tx + " don't have writeLock" );
}
writeCount--;
tle.writeCount--;
if ( tle.readCount == 0 && tle.writeCount == 0 )
{
if ( !this.isMarked() )
{
txLockElementMap.remove( tx );
}
ragManager.lockReleased( this, tx );
}
// the threads in the waitingList cannot be currentThread
// so we only have to wake other elements if writeCount is down to zero
// (that is: If writeCount > 0 a waiting thread in the queue cannot be
// the thread that holds the write locks because then it would never
// have been put into wait mode)
if ( writeCount == 0 && waitingThreadList.size() > 0 )
{
// wake elements in queue until a write lock is found or queue is
// empty
do
{
WaitElement we = waitingThreadList.removeLast();
we.waitingThread.interrupt();
if ( we.lockType == LockType.WRITE )
{
break;
}
}
while ( waitingThreadList.size() > 0 );
}
}
int getWriteCount()
{
return writeCount;
}
int getReadCount()
{
return readCount;
}
synchronized int getWaitingThreadsCount()
{
return waitingThreadList.size();
}
synchronized void dumpStack()
{
System.out.println( "Total lock count: readCount=" + readCount
+ " writeCount=" + writeCount + " for " + resource );
System.out.println( "Waiting list:" );
Iterator<WaitElement> wElements = waitingThreadList.iterator();
while ( wElements.hasNext() )
{
WaitElement we = wElements.next();
System.out.print( "[" + we.waitingThread + "("
+ we.element.readCount + "r," + we.element.writeCount + "w),"
+ we.lockType + "]" );
if ( wElements.hasNext() )
{
System.out.print( "," );
}
else
{
System.out.println();
}
}
System.out.println( "Locking transactions:" );
Iterator<TxLockElement> lElements = txLockElementMap.values()
.iterator();
while ( lElements.hasNext() )
{
TxLockElement tle = lElements.next();
System.out.println( "" + tle.tx + "(" + tle.readCount + "r,"
+ tle.writeCount + "w)" );
}
}
public String toString()
{
return "RWLock[" + resource + "]";
}
private static class PlaceboTransaction implements Transaction
{
private final Thread currentThread;
PlaceboTransaction()
{
this.currentThread = Thread.currentThread();
}
public boolean equals( Object o )
{
if ( !(o instanceof PlaceboTransaction) )
{
return false;
}
return this.currentThread
.equals( ((PlaceboTransaction) o).currentThread );
}
public int hashCode()
{
return currentThread.hashCode();
}
public void commit()
{
throw new UnsupportedOperationException();
}
public boolean delistResource( XAResource arg0, int arg1 )
{
throw new UnsupportedOperationException();
}
public boolean enlistResource( XAResource arg0 )
{
throw new UnsupportedOperationException();
}
public int getStatus()
{
throw new UnsupportedOperationException();
}
public void registerSynchronization( Synchronization arg0 )
{
throw new UnsupportedOperationException();
}
public void rollback()
{
throw new UnsupportedOperationException();
}
public void setRollbackOnly()
{
}
public String toString()
{
return "Placebo tx for thread " + currentThread;
}
}
}