package org.jboss.cache.lock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.factories.CommandsFactory;
import org.jboss.cache.statetransfer.StateTransferManager;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.TransactionEntry;
import org.jboss.cache.transaction.TransactionTable;
import javax.transaction.Status;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class LockUtil
{
private final static Log log = LogFactory.getLog(StateTransferManager.class);
private static final boolean trace = log.isTraceEnabled();
private static interface TransactionLockStatus extends Status
{
int STATUS_BROKEN = Integer.MIN_VALUE;
}
public static boolean breakTransactionLock(NodeSPI node,
LockManager lockManager,
GlobalTransaction gtx,
boolean localTx,
TransactionTable tx_table, TransactionManager tm)
{
boolean broken = false;
int tryCount = 0;
int lastStatus = TransactionLockStatus.STATUS_BROKEN;
while (!broken && lockManager.ownsLock(node.getFqn(), gtx))
{
int status = breakTransactionLock(gtx, node, lockManager, tx_table, tm, localTx, lastStatus, tryCount);
if (status == TransactionLockStatus.STATUS_BROKEN)
{
broken = true;
}
else if (status != lastStatus)
{
tryCount = 0;
}
lastStatus = status;
tryCount++;
}
return broken;
}
/**
* Attempts to release the lock held by <code>gtx</code> by altering the
* underlying transaction. Different strategies will be employed
* depending on the status of the transaction and param
* <code>tryCount</code>. Transaction may be rolled back or marked
* rollback-only, or the lock may just be broken, ignoring the tx. Makes an
* effort to not affect the tx or break the lock if tx appears to be in
* the process of completion; param <code>tryCount</code> is used to help
* make decisions about this.
* <p/>
* This method doesn't guarantee to have broken the lock unless it returns
* {@link TransactionLockStatus#STATUS_BROKEN}.
*
* @param gtx the gtx holding the lock
* @param lastStatus the return value from a previous invocation of this
* method for the same lock, or Status.STATUS_UNKNOW
* for the first invocation.
* @param tryCount number of times this method has been called with
* the same gtx, lock and lastStatus arguments. Should
* be reset to 0 anytime lastStatus changes.
* @return the current status of the Transaction associated with
* <code>gtx</code>, or {@link TransactionLockStatus#STATUS_BROKEN}
* if the lock held by gtx was forcibly broken.
*/
private static int breakTransactionLock(GlobalTransaction gtx,
NodeSPI node, LockManager lockManager,
TransactionTable transactionTable,
TransactionManager tm,
boolean localTx,
int lastStatus,
int tryCount)
{
int status = Status.STATUS_UNKNOWN;
Transaction tx = transactionTable.getLocalTransaction(gtx);
if (tx != null)
{
try
{
status = tx.getStatus();
if (status != lastStatus)
{
tryCount = 0;
}
switch (status)
{
case Status.STATUS_ACTIVE:
case Status.STATUS_MARKED_ROLLBACK:
case Status.STATUS_PREPARING:
case Status.STATUS_UNKNOWN:
if (tryCount == 0)
{
if (trace)
{
log.trace("Attempting to break transaction lock held " +
" by " + gtx + " by rolling back local tx");
}
// This thread has to join the tx
tm.resume(tx);
try
{
tx.rollback();
}
finally
{
tm.suspend();
}
}
else if (tryCount > 100)
{
// Something is wrong; our initial rollback call
// didn't generate a valid state change; just force it
lockManager.unlock(node.getFqn(), gtx);
status = TransactionLockStatus.STATUS_BROKEN;
}
break;
case Status.STATUS_COMMITTING:
case Status.STATUS_ROLLING_BACK:
// We'll try up to 10 times before just releasing
if (tryCount < 10)
{
break;// let it finish
}
// fall through and release
case Status.STATUS_COMMITTED:
case Status.STATUS_ROLLEDBACK:
case Status.STATUS_NO_TRANSACTION:
lockManager.unlock(node.getFqn(), gtx);
status = TransactionLockStatus.STATUS_BROKEN;
break;
case Status.STATUS_PREPARED:
// If the tx was started here, we can still abort the commit,
// otherwise we are in the middle of a remote commit() call
// and the status is just about to change
if (tryCount == 0 && localTx)
{
// We can still abort the commit
if (trace)
{
log.trace("Attempting to break transaction lock held " +
"by " + gtx + " by marking local tx as " +
"rollback-only");
}
tx.setRollbackOnly();
break;
}
else if (tryCount < 10)
{
// EITHER tx was started elsewhere (in which case we'll
// wait a bit to allow the commit() call to finish;
// same as STATUS_COMMITTING above)
// OR we marked the tx rollbackOnly above and are just
// waiting a bit for the status to change
break;
}
// fall through and release
default:
lockManager.unlock(node.getFqn(), gtx);
status = TransactionLockStatus.STATUS_BROKEN;
}
}
catch (Exception e)
{
log.error("Exception breaking locks held by " + gtx, e);
lockManager.unlock(node.getFqn(), gtx);
status = TransactionLockStatus.STATUS_BROKEN;
}
}
else
{
// Race condition; globalTransaction was cleared from txTable.
// Just double check if globalTransaction still holds a lock
if (lockManager.ownsLock(node.getFqn(), gtx))
{
// perhaps we should throw an exception?
lockManager.unlock(node.getFqn(), gtx);
status = TransactionLockStatus.STATUS_BROKEN;
}
}
return status;
}
/**
* Test if this node needs to be 'undeleted'
* reverse the "remove" if the node has been previously removed in the same tx, if this operation is a put()
*/
public static void manageReverseRemove(InvocationContext ctx, NodeSPI childNode, boolean reverseRemoveCheck, List createdNodes, CommandsFactory commandsFactory)
{
if (ctx.getGlobalTransaction() != null) //if no tx then reverse remove does not make sense
{
Fqn fqn = childNode.getFqn();
TransactionEntry entry = ctx.getTransactionEntry();
boolean needToReverseRemove = reverseRemoveCheck && childNode.isDeleted() && entry != null && entry.getRemovedNodes().contains(fqn);
if (!needToReverseRemove) return;
childNode.markAsDeleted(false);
//if we'll rollback the tx data should be added to the node again
Map oldData = new HashMap(childNode.getDataDirect());
PutDataMapCommand command = commandsFactory.buildPutDataMapCommand(ctx.getGlobalTransaction(), fqn, oldData);
// txTable.get(gtx).addUndoOperation(command); --- now need to make sure this is added to the normal mods list instead
entry.addModification(command);
//we're prepared for rollback, now reset the node
childNode.clearDataDirect();
if (createdNodes != null)
{
createdNodes.add(childNode);
}
}
}
}