/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.cache.interceptors;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.Node;
import org.jboss.cache.ReplicationException;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.Option;
import org.jboss.cache.lock.NodeLock;
import org.jboss.cache.marshall.MethodCall;
import org.jboss.cache.marshall.MethodCallFactory;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.OptimisticTransactionEntry;
import org.jboss.cache.transaction.TransactionEntry;
import org.jgroups.Address;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* This interceptor is the new default at the head of all interceptor chains,
* and makes transactional attributes available to all interceptors in the chain.
* This interceptor is also responsible for registering for synchronisation on
* transaction completion.
*
* @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
* @author <a href="mailto:stevew@jofti.com">Steve Woodcock (stevew@jofti.com)</a>
*/
public class TxInterceptor extends BaseTransactionalContextInterceptor implements TxInterceptorMBean
{
private final static Object NULL = new Object();
/**
* List <Transaction>that we have registered for
*/
private Map transactions = new ConcurrentHashMap(16);
private Map rollbackTransactions = new ConcurrentHashMap(16);
private long m_prepares = 0;
private long m_commits = 0;
private long m_rollbacks = 0;
public TxInterceptor()
{
initLogger();
}
@SuppressWarnings("unchecked")
protected Object handleOptimisticPrepareMethod(InvocationContext ctx, GlobalTransaction gtx, List modifications, Map data, Address address, boolean onePhaseCommit) throws Throwable
{
Object result = null;
boolean scrubTxsOnExit = false;
// this is a prepare, commit, or rollback.
if (log.isDebugEnabled()) log.debug("Got gtx from invocation context " + ctx.getGlobalTransaction());
try
{
if (ctx.getGlobalTransaction().isRemote())
{
result = handleRemotePrepare(ctx, modifications, onePhaseCommit);
scrubTxsOnExit = true;
if (configuration.getExposeManagementStatistics() && getStatisticsEnabled())
{
m_prepares++;
}
}
else
{
if (trace) log.trace("received my own message (discarding it)");
result = null;
}
}
catch (Throwable e)
{
throwIfNeeded(ctx, e);
}
finally
{
scrubOnExist(ctx, scrubTxsOnExit);
}
return result;
}
protected Object handlePrepareMethod(InvocationContext ctx, GlobalTransaction gtx, List modification, Address coordinator, boolean onePhaseCommit) throws Throwable
{
return handleOptimisticPrepareMethod(ctx, gtx, modification, null, coordinator, onePhaseCommit);
}
@SuppressWarnings("Unchecked")
protected Object handleCommitMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
Object result = null;
boolean scrubTxsOnExit = false;
try
{
if (trace)
{
log.trace("(" + cache.getLocalAddress() + ") call on method [" + ctx.getMethodCall() + "]");
}
if (ctx.getGlobalTransaction().isRemote())
{
result = handleRemoteCommitRollback(ctx);
scrubTxsOnExit = true;
}
else
{
if (trace) log.trace("received my own message (discarding it)");
result = null;
}
}
catch (Throwable throwable)
{
throwIfNeeded(ctx, throwable);
}
finally
{
scrubOnExist(ctx, scrubTxsOnExit);
}
return result;
}
protected Object handleRollbackMethod(InvocationContext ctx, GlobalTransaction globalTransaction) throws Throwable
{
return handleCommitMethod(ctx, globalTransaction);
}
protected Object handleLockMethod(InvocationContext ctx, Fqn fqn, NodeLock.LockType lockType, boolean recursive) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveDataVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, boolean createUndoOps, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveKeyVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Object key, boolean createUndoOps, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveNodeVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, boolean createUndoOps, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleDataGravitationCleanupMethod(InvocationContext ctx, Fqn primary, Fqn backup) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutForExternalReadVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Object key, Object value, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutKeyValueVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Object key, Object value, boolean createUndoOps, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutDataVersionedMethod(InvocationContext ctx, GlobalTransaction globalTransaction, Fqn fqn, Map map, Boolean createUndoOps, DataVersion dataVersion) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutDataEraseVersionedMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Map data, boolean createUndoOps, boolean eraseContent, DataVersion dv) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleExistsMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleEvictVersionedNodeMethod(InvocationContext ctx, Fqn fqn, DataVersion dataVersion) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleEvictMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveDataMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveKeyMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Object key, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleRemoveNodeMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleGetDataMapMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleGetKeysMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePrintMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleReleaseAllLocksMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleGetChildrenNamesMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleGetNodeMethod(InvocationContext ctx, Fqn fqn) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleGetKeyValueMethod(InvocationContext ctx, Fqn fqn, Object key, boolean sendNodeEvent) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleAddChildMethod(InvocationContext ctx, GlobalTransaction tx, Fqn parentFqn, Object childName, Node cn, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handleMoveMethod(InvocationContext ctx, Fqn from, Fqn to) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutKeyValueMethod(InvocationContext ctx, GlobalTransaction gtx, Fqn fqn, Object key, Object value, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutForExternalReadMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Object key, Object value) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutDataMethod(InvocationContext ctx, GlobalTransaction tx, Fqn fqn, Map data, boolean createUndoOps) throws Throwable
{
return handleNonTxMethod(ctx);
}
protected Object handlePutDataEraseMethod(InvocationContext ctx, GlobalTransaction gt, Fqn fqn, Map newData, boolean createUndoOps, boolean eraseContents) throws Throwable
{
return handleNonTxMethod(ctx);
}
private boolean throwIfNeeded(InvocationContext ctx, Throwable e) throws Throwable
{
Option optionOverride = ctx.getOptionOverrides();
boolean shouldRethtrow = optionOverride == null || !optionOverride.isFailSilently();
if (!shouldRethtrow)
{
if (trace)
log.trace("There was a problem handling this request, but failSilently was set, so suppressing exception", e);
}
throw e;
}
/**
* we should scrub txs after every call to prevent race conditions
* basically any other call coming in on the same thread and hijacking any running tx's
* was highlighted in JBCACHE-606
*/
private void scrubOnExist(InvocationContext ctx, boolean scrubTxsOnExit)
{
if (scrubTxsOnExit)
{
setTransactionalContext(null, null, ctx);
}
}
public long getPrepares()
{
return m_prepares;
}
public long getCommits()
{
return m_commits;
}
public long getRollbacks()
{
return m_rollbacks;
}
public void resetStatistics()
{
m_prepares = 0;
m_commits = 0;
m_rollbacks = 0;
}
public Map<String, Object> dumpStatistics()
{
Map<String, Object> retval = new HashMap<String, Object>(3);
retval.put("Prepares", m_prepares);
retval.put("Commits", m_commits);
retval.put("Rollbacks", m_rollbacks);
return retval;
}
// --------------------------------------------------------------
private Object handleRemotePrepare(InvocationContext ctx, List<MethodCall> modifications, boolean onePhase) throws Throwable
{
GlobalTransaction gtx = ctx.getGlobalTransaction();
// Is there a local transaction associated with GTX ?
Transaction ltx = txTable.getLocalTransaction(gtx);
Transaction currentTx = txManager.getTransaction();
Object retval = null;
try
{
if (ltx == null)
{
if (currentTx != null) txManager.suspend();
ltx = createLocalTxForGlobalTx(gtx, ctx);// creates new LTX and associates it with a GTX
if (log.isDebugEnabled())
{
log.debug("Started new local TX as result of remote PREPARE: local TX=" + ltx + " (Status=" + ltx.getStatus() + "), global TX=" + gtx);
}
}
else
{
//this should be valid
if (!isValid(ltx)) throw new CacheException("Transaction " + ltx + " not in correct state to be prepared");
//associate this thread with this ltx if this ltx is NOT the current tx.
if (currentTx == null || !ltx.equals(currentTx))
{
txManager.suspend();
txManager.resume(ltx);
}
}
if (trace)
{
log.trace("Resuming existing transaction " + ltx + ", global TX=" + gtx);
}
// at this point we have a non-null ltx
// Asssociate the local TX with the global TX. Create new
// entry for TX in txTable, the modifications
// below will need this entry to add their modifications
// under the GlobalTx key
TransactionEntry entry;
if (txTable.get(gtx) == null)
{
// create a new transaction entry
entry = configuration.isNodeLockingOptimistic() ? new OptimisticTransactionEntry(ltx) : new TransactionEntry(ltx);
log.debug("creating new tx entry");
txTable.put(gtx, entry);
if (trace) log.trace("TxTable contents: " + txTable);
}
else
{
entry = txTable.get(gtx);
}
setTransactionalContext(ltx, gtx, ctx);
// register a sync handler for this tx.
registerHandler(ltx, new RemoteSynchronizationHandler(gtx, ltx), ctx, entry);
if (configuration.isNodeLockingOptimistic())
{
retval = handleOptimisticPrepare(ctx, gtx, modifications, onePhase, ltx);
}
else
{
retval = handlePessimisticPrepare(ctx, ctx.getMethodCall(), gtx, modifications, onePhase, ltx);
}
}
finally
{
txManager.suspend();// suspends ltx - could be null
// resume whatever else we had going.
if (currentTx != null) txManager.resume(currentTx);
if (log.isDebugEnabled()) log.debug("Finished remote prepare " + gtx);
}
return retval;
}
// handler methods.
// --------------------------------------------------------------
/**
* Tests if we already have a tx running. If so, register a sync handler for this method invocation.
* if not, create a local tx if we're using opt locking.
*
* @return
* @throws Throwable
*/
private Object handleNonTxMethod(InvocationContext ctx) throws Throwable
{
Object result;
try
{
MethodCall m = ctx.getMethodCall();
Transaction tx = ctx.getTransaction();
// if there is no current tx and we're using opt locking, we need to use an implicit tx.
boolean implicitTransaction = configuration.isNodeLockingOptimistic() && tx == null;
if (implicitTransaction)
{
tx = createLocalTx();
// we need to attach this tx to the InvocationContext.
ctx.setTransaction(tx);
}
if (tx != null)
attachGlobalTransaction(ctx, tx, m);
GlobalTransaction gtx = ctx.getGlobalTransaction();
try
{
result = nextInterceptor(ctx);
if (implicitTransaction)
{
copyInvocationScopeOptionsToTxScope(ctx);
copyForcedCacheModeToTxScope(ctx);
txManager.commit();
}
}
catch (Throwable t)
{
if (implicitTransaction)
{
log.warn("Rolling back, exception encountered", t);
result = t;
try
{
setTransactionalContext(tx, gtx, ctx);
txManager.rollback();
}
catch (Throwable th)
{
log.warn("Roll back failed encountered", th);
}
}
else
{
throw t;
}
}
return result;
}
catch (Throwable throwable)
{
throwIfNeeded(ctx, throwable);
return null;
}
}
/**
* @param ctx
*/
private void copyForcedCacheModeToTxScope(InvocationContext ctx)
{
Option optionOverride = ctx.getOptionOverrides();
if (optionOverride != null
&& (optionOverride.isForceAsynchronous() || optionOverride.isForceSynchronous()))
{
TransactionEntry entry = txTable.get(ctx.getGlobalTransaction());
if (entry != null)
{
if (optionOverride.isForceAsynchronous())
entry.setForceAsyncReplication(true);
else
entry.setForceSyncReplication(true);
}
}
}
private MethodCall attachGlobalTransaction(InvocationContext ctx, Transaction tx, MethodCall m) throws Exception
{
if (trace)
{
log.trace(" local transaction exists - registering global tx if not present for " + Thread.currentThread());
}
if (trace)
{
GlobalTransaction tempGtx = txTable.get(tx);
log.trace("Associated gtx in txTable is " + tempGtx);
}
// register a sync handler for this tx - only if the gtx is not remotely initiated.
GlobalTransaction gtx = registerTransaction(tx, ctx);
if (gtx != null)
{
m = replaceGtx(m, gtx);
}
else
{
// get the current gtx from the txTable.
gtx = txTable.get(tx);
}
// make sure we attach this gtx to the invocation context.
ctx.setGlobalTransaction(gtx);
return m;
}
/**
* This is called by invoke() if we are in a remote gtx's prepare() phase.
* Finds the appropriate tx, suspends any existing txs, registers a sync handler
* and passes up the chain.
* <p/>
* Resumes any existing txs before returning.
*
* @return
* @throws Throwable
*/
private Object handleOptimisticPrepare(InvocationContext ctx, GlobalTransaction gtx, List<MethodCall> modifications, boolean onePhase, Transaction ltx) throws Throwable
{
Object retval;
if (log.isDebugEnabled()) log.debug("Handling optimistic remote prepare " + gtx);
replayModifications(modifications, ctx, true);
retval = nextInterceptor(ctx);
// JBCACHE-361 Confirm that the transaction is ACTIVE
if (!isActive(ltx))
{
throw new ReplicationException("prepare() failed -- " +
"local transaction status is not STATUS_ACTIVE;" +
" is " + ltx.getStatus());
}
return retval;
}
private Object handlePessimisticPrepare(InvocationContext ctx, MethodCall m, GlobalTransaction gtx, List<MethodCall> modifications, boolean commit, Transaction ltx) throws Exception
{
boolean success = true;
Object retval;
try
{
// now pass up the prepare method itself.
try
{
replayModifications(modifications, ctx, false);
if (m.isOnePhaseCommitPrepareMehod())
{
if (trace)
log.trace("Using one-phase prepare. Not propagating the prepare call up the stack until called to do so by the sync handler.");
}
else
{
nextInterceptor(ctx);
}
// JBCACHE-361 Confirm that the transaction is ACTIVE
if (!isActive(ltx))
{
throw new ReplicationException("prepare() failed -- " +
"local transaction status is not STATUS_ACTIVE;" +
" is " + ltx.getStatus());
}
}
catch (Throwable th)
{
log.error("prepare method invocation failed", th);
retval = th;
success = false;
if (retval instanceof Exception)
{
throw (Exception) retval;
}
}
}
finally
{
if (trace)
{
log.trace("Are we running a 1-phase commit? " + commit);
}
// 4. If commit == true (one-phase-commit): commit (or rollback) the TX; this will cause
// {before/after}Completion() to be called in all registered interceptors: the TransactionInterceptor
// will then commit/rollback against the cache
if (commit)
{
try
{
// invokeOnePhaseCommitMethod(gtx, modifications.size() > 0, success);
if (success)
{
ltx.commit();
}
else
{
ltx.rollback();
}
}
catch (Throwable t)
{
log.error("Commit/rollback failed.", t);
if (success)
{
// try another rollback...
try
{
log.info("Attempting anotehr rollback");
//invokeOnePhaseCommitMethod(gtx, modifications.size() > 0, false);
ltx.rollback();
}
catch (Throwable t2)
{
log.error("Unable to rollback", t2);
}
}
}
finally
{
transactions.remove(ltx);// JBAS-298
}
}
}
return null;
}
private Object replayModifications(List<MethodCall> modifications, InvocationContext ctx, boolean injectDataVersions)
{
Object retval = null;
MethodCall originalMethodCall = ctx.getMethodCall();
Option originalOption = ctx.getOptionOverrides();
if (modifications != null)
{
for (MethodCall modification : modifications)
{
try
{
if (injectDataVersions && !MethodDeclarations.isDataGravitationMethod(modification.getMethodId()))
{
Object[] origArgs = modification.getArgs();
// there may be instances (e.g., data gravitation calls) where a data version is not passed in or not even relevant.
// make sure we are aware of this.
Option o = null;
if (origArgs[origArgs.length - 1] instanceof DataVersion)
{
o = new Option();
o.setDataVersion((DataVersion) origArgs[origArgs.length - 1]);
}
// modify the call to the non-dataversioned counterpart since we've popped out the data version
Object[] args = new Object[origArgs.length - 1];
System.arraycopy(origArgs, 0, args, 0, args.length);
ctx.setMethodCall(MethodCallFactory.create(MethodDeclarations.getUnversionedMethodId(modification.getMethodId()), args));
if (o != null) ctx.setOptionOverrides(o);
}
else
{
ctx.setMethodCall(modification);
}
retval = nextInterceptor(ctx);
if (!isActive(ctx.getTransaction()))
{
throw new ReplicationException("prepare() failed -- " + "local transaction status is not STATUS_ACTIVE; is " + ctx.getTransaction().getStatus());
}
}
catch (Throwable t)
{
log.error("method invocation failed", t);
retval = t;
}
finally
{
// reset any options
if (injectDataVersions) ctx.setOptionOverrides(originalOption);
ctx.setMethodCall(originalMethodCall);
}
if (retval != null && retval instanceof Exception)
{
if (retval instanceof RuntimeException)
throw (RuntimeException) retval;
else
throw new RuntimeException((Exception) retval);
}
}
}
// need to pass up the prepare as well and return value from that
return retval;
}
/**
* Handles a commit or a rollback for a remote gtx. Called by invoke().
*
* @return
* @throws Throwable
*/
private Object handleRemoteCommitRollback(InvocationContext ctx) throws Throwable
{
Transaction ltx;
GlobalTransaction gtx = ctx.getGlobalTransaction();
MethodCall m = ctx.getMethodCall();
try
{
ltx = getLocalTxForGlobalTx(gtx);
}
catch (IllegalStateException e)
{
if (m.getMethodId() == MethodDeclarations.rollbackMethod_id)
{
log.warn("No local transaction for this remotely originating rollback. Possibly rolling back before a prepare call was broadcast?");
return null;
}
else
{
throw e;
}
}
// disconnect if we have a current tx associated
Transaction currentTx = txManager.getTransaction();
boolean resumeCurrentTxOnCompletion = false;
try
{
if (!ltx.equals(currentTx))
{
currentTx = txManager.suspend();
resumeCurrentTxOnCompletion = true;
txManager.resume(ltx);
// make sure we set this in the ctx
ctx.setTransaction(ltx);
}
if (log.isDebugEnabled()) log.debug(" executing " + m + "() with local TX " + ltx + " under global tx " + gtx);
// pass commit up the chain
// nextInterceptor(ctx);
// commit or rollback the tx.
if (m.getMethodId() == MethodDeclarations.commitMethod_id)
{
txManager.commit();
if (configuration.getExposeManagementStatistics() && getStatisticsEnabled())
{
m_commits++;
}
}
else
{
txManager.rollback();
if (configuration.getExposeManagementStatistics() && getStatisticsEnabled())
{
m_rollbacks++;
}
}
}
finally
{
//resume the old transaction if we suspended it
if (resumeCurrentTxOnCompletion)
{
if (trace) log.trace("Resuming suspended transaction " + currentTx);
txManager.suspend();
if (currentTx != null)
{
txManager.resume(currentTx);
ctx.setTransaction(currentTx);
}
}
// remove from local lists.
transactions.remove(ltx);
// this tx has completed. Clean up in the tx table.
txTable.remove(gtx);
txTable.remove(ltx);
}
if (log.isDebugEnabled()) log.debug("Finished remote commit/rollback method for " + gtx);
return null;
}
private Transaction getLocalTxForGlobalTx(GlobalTransaction gtx) throws IllegalStateException
{
Transaction ltx = txTable.getLocalTransaction(gtx);
if (ltx != null)
{
if (log.isDebugEnabled()) log.debug("Found local TX=" + ltx + ", global TX=" + gtx);
}
else
{
throw new IllegalStateException(" found no local TX for global TX " + gtx);
}
return ltx;
}
/**
* Handles a commit or a rollback. Called by the synch handler. Simply tests that we are in the correct tx and
* passes the meth call up the interceptor chain.
*
* @return
* @throws Throwable
*/
private Object handleCommitRollback(InvocationContext ctx, MethodCall call) throws Throwable
{
//GlobalTransaction gtx = findGlobalTransaction(m.getArgs());
GlobalTransaction gtx = ctx.getGlobalTransaction();
Object result;
// this must have a local transaction associated if a prepare has been
// callled before
//Transaction ltx = getLocalTxForGlobalTx(gtx);
// Transaction currentTx = txManager.getTransaction();
//if (!ltx.equals(currentTx)) throw new IllegalStateException(" local transaction " + ltx + " transaction does not match running tx " + currentTx);
MethodCall originalCall = ctx.getMethodCall();
ctx.setMethodCall(call);
try
{
result = nextInterceptor(ctx);
}
finally
{
ctx.setMethodCall(originalCall);
}
if (log.isDebugEnabled()) log.debug("Finished local commit/rollback method for " + gtx);
return result;
}
// --------------------------------------------------------------
// Transaction phase runners
// --------------------------------------------------------------
/**
* creates a commit() MethodCall and feeds it to handleCommitRollback();
*
* @param gtx
*/
protected void runCommitPhase(InvocationContext ctx, GlobalTransaction gtx, Transaction tx, List modifications, List clModifications, boolean onePhaseCommit)
{
// set the hasMods flag in the invocation ctx. This should not be replicated, just used locally by the interceptors.
ctx.setTxHasMods(modifications != null && modifications.size() > 0);
ctx.setCacheLoaderHasMods(clModifications != null && clModifications.size() > 0);
try
{
MethodCall commitMethod;
if (onePhaseCommit)
{
// running a 1-phase commit.
if (configuration.isNodeLockingOptimistic())
{
commitMethod = MethodCallFactory.create(MethodDeclarations.optimisticPrepareMethod_id,
gtx, modifications, null, cache.getLocalAddress(), true);
}
else
{
commitMethod = MethodCallFactory.create(MethodDeclarations.prepareMethod_id,
gtx, modifications, cache.getLocalAddress(),
true);
}
}
else
{
commitMethod = MethodCallFactory.create(MethodDeclarations.commitMethod_id, gtx);
}
if (trace)
{
log.trace(" running commit for " + gtx);
}
handleCommitRollback(ctx, commitMethod);
}
catch (Throwable e)
{
log.warn("Commit failed. Clearing stale locks.");
try
{
cleanupStaleLocks(gtx);
}
catch (RuntimeException re)
{
log.error("Unable to clear stale locks", re);
throw re;
}
catch (Throwable e2)
{
log.error("Unable to clear stale locks", e2);
throw new RuntimeException(e2);
}
if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new RuntimeException("Commit failed.", e);
}
}
private void cleanupStaleLocks(GlobalTransaction gtx) throws Throwable
{
TransactionEntry entry = txTable.get(gtx);
if (entry != null)
{
entry.releaseAllLocksLIFO(gtx);
}
}
/**
* creates a rollback() MethodCall and feeds it to handleCommitRollback();
*
* @param gtx
*/
protected void runRollbackPhase(InvocationContext ctx, GlobalTransaction gtx, Transaction tx, List modifications)
{
//Transaction ltx = null;
try
{
ctx.setTxHasMods(modifications != null && modifications.size() > 0);
// JBCACHE-457
// MethodCall rollbackMethod = MethodCall(CacheImpl.rollbackMethod, new Object[]{gtx, hasMods ? true : false});
MethodCall rollbackMethod = MethodCallFactory.create(MethodDeclarations.rollbackMethod_id, gtx);
if (trace)
{
log.trace(" running rollback for " + gtx);
}
//JBCACHE-359 Store a lookup for the gtx so a listener
// callback can find it
//ltx = getLocalTxForGlobalTx(gtx);
rollbackTransactions.put(tx, gtx);
handleCommitRollback(ctx, rollbackMethod);
}
catch (Throwable e)
{
log.warn("Rollback had a problem", e);
}
finally
{
if (tx != null) rollbackTransactions.remove(tx);
}
}
/**
* Handles a local prepare - invoked by the sync handler. Tests if the current tx matches the gtx passed in to the
* method call and passes the prepare() call up the chain.
*
* @return
* @throws Throwable
*/
protected Object runPreparePhase(InvocationContext ctx, GlobalTransaction gtx, List modifications) throws Throwable
{
// build the method call
MethodCall prepareMethod;
// if (cache.getCacheModeInternal() != CacheImpl.REPL_ASYNC)
// {
// running a 2-phase commit.
if (configuration.isNodeLockingOptimistic())
{
prepareMethod = MethodCallFactory.create(MethodDeclarations.optimisticPrepareMethod_id, gtx, modifications, null, cache.getLocalAddress(), false);
}
else if (configuration.getCacheMode() != Configuration.CacheMode.REPL_ASYNC)
{
prepareMethod = MethodCallFactory.create(MethodDeclarations.prepareMethod_id,
gtx, modifications, cache.getLocalAddress(),
false);// don't commit or rollback - wait for call
}
//}
else
{
// this is a REPL_ASYNC call - do 1-phase commit. break!
if (trace) log.trace("This is a REPL_ASYNC call (1 phase commit) - do nothing for beforeCompletion()");
return null;
}
// passes a prepare call up the local interceptor chain. The replication interceptor
// will do the broadcasting if needed. This is so all requests (local/remote) are
// treated the same
Object result;
// Is there a local transaction associated with GTX ?
Transaction ltx = ctx.getTransaction();
//if ltx is not null and it is already running
if (txManager.getTransaction() != null && ltx != null && txManager.getTransaction().equals(ltx))
{
MethodCall originalCall = ctx.getMethodCall();
ctx.setMethodCall(prepareMethod);
try
{
result = nextInterceptor(ctx);
}
finally
{
ctx.setMethodCall(originalCall);
}
}
else
{
log.warn("Local transaction does not exist or does not match expected transaction " + gtx);
throw new CacheException(" local transaction " + ltx + " does not exist or does not match expected transaction " + gtx);
}
return result;
}
// --------------------------------------------------------------
// Private helper methods
// --------------------------------------------------------------
/**
* Creates a gtx (if one doesnt exist), a sync handler, and registers the tx.
*
* @param tx
* @return
* @throws Exception
*/
private GlobalTransaction registerTransaction(Transaction tx, InvocationContext ctx) throws Exception
{
GlobalTransaction gtx;
if (isValid(tx) && transactions.put(tx, NULL) == null)
{
gtx = cache.getCurrentTransaction(tx, true);
if (gtx.isRemote())
{
// should be no need to register a handler since this a remotely initiated gtx
if (trace)
{
log.trace("is a remotely initiated gtx so no need to register a tx for it");
}
}
else
{
if (trace)
{
log.trace("Registering sync handler for tx " + tx + ", gtx " + gtx);
}
// see the comment in the LocalSyncHandler for the last isOriginLocal param.
LocalSynchronizationHandler myHandler = new LocalSynchronizationHandler(gtx, tx, !ctx.isOriginLocal());
registerHandler(tx, myHandler, ctx, txTable.get(gtx));
}
}
else if ((gtx = (GlobalTransaction) rollbackTransactions.get(tx)) != null)
{
if (trace) log.trace("Transaction " + tx + " is already registered and is rolling back.");
}
else
{
if (trace) log.trace("Transaction " + tx + " is already registered.");
}
return gtx;
}
/**
* Registers a sync hander against a tx.
*
* @param tx
* @param handler
* @throws Exception
*/
private void registerHandler(Transaction tx, Synchronization handler, InvocationContext ctx, TransactionEntry entry) throws Exception
{
OrderedSynchronizationHandler orderedHandler = entry.getOrderedSynchronizationHandler(); //OrderedSynchronizationHandler.getInstance(tx);
if (trace) log.trace("registering for TX completion: SynchronizationHandler(" + handler + ")");
orderedHandler.registerAtHead(handler);// needs to be invoked first on TX commit
cache.getNotifier().notifyTransactionRegistered(tx, ctx);
}
/**
* Replaces the global transaction in a method call with a new global transaction passed in.
*/
private MethodCall replaceGtx(MethodCall m, GlobalTransaction gtx)
{
Class[] argClasses = m.getMethod().getParameterTypes();
Object[] args = m.getArgs();
for (int i = 0; i < argClasses.length; i++)
{
if (argClasses[i].equals(GlobalTransaction.class))
{
if (!gtx.equals(args[i]))
{
args[i] = gtx;
m.setArgs(args);
}
break;
}
}
return m;
}
/**
* Creates and starts a local tx
*
* @return
* @throws Exception
*/
private Transaction createLocalTx() throws Exception
{
if (trace)
{
log.trace("Creating transaction for thread " + Thread.currentThread());
}
Transaction localTx;
if (txManager == null) throw new Exception("Failed to create local transaction; TransactionManager is null");
txManager.begin();
localTx = txManager.getTransaction();
return localTx;
}
/**
* Creates a new local transaction for a given global transaction.
*
* @param gtx
* @return
* @throws Exception
*/
private Transaction createLocalTxForGlobalTx(GlobalTransaction gtx, InvocationContext ctx) throws Exception
{
Transaction localTx = createLocalTx();
txTable.put(localTx, gtx);
// attach this to the context
ctx.setTransaction(localTx);
if (trace) log.trace("Created new tx for gtx " + gtx);
return localTx;
}
// ------------------------------------------------------------------------
// Synchronization classes
// ------------------------------------------------------------------------
// this controls the whole transaction
private class RemoteSynchronizationHandler implements Synchronization
{
Transaction tx = null;
GlobalTransaction gtx = null;
// CacheSPI cache = null;
List modifications = null;
TransactionEntry entry = null;
protected InvocationContext ctx; // the context for this call.
RemoteSynchronizationHandler(GlobalTransaction gtx, Transaction tx)
{
this.gtx = gtx;
this.tx = tx;
}
public void beforeCompletion()
{
if (trace) log.trace("Running beforeCompletion on gtx " + gtx);
entry = txTable.get(gtx);
if (entry == null)
{
log.error("Transaction has a null transaction entry - beforeCompletion() will fail.");
log.error("TxTable contents: " + txTable);
throw new IllegalStateException("cannot find transaction entry for " + gtx);
}
modifications = entry.getModifications();
ctx = cache.getInvocationContext();
if (ctx.isOptionsUninitialised() && entry.getOption() != null) ctx.setOptionOverrides(entry.getOption());
assertCanContinue();
ctx.setOriginLocal(false);
}
// this should really not be done here -
// it is supposed to be post commit not actually run the commit
public void afterCompletion(int status)
{
// could happen if a rollback is called and beforeCompletion() doesn't get called.
if (ctx == null)
{
ctx = cache.getInvocationContext();
}
entry = txTable.get(gtx);
if (ctx.isOptionsUninitialised() && entry != null && entry.getOption() != null)
{
// use the options from the transaction entry instead
ctx.setOptionOverrides(entry.getOption());
}
try
{
assertCanContinue();
setTransactionalContext(tx, gtx, ctx);
try
{
if (txManager.getTransaction() != null && !txManager.getTransaction().equals(tx)) txManager.resume(tx);
}
catch (Exception e)
{
log.error("afterCompletion error: " + status, e);
}
if (trace) log.trace("calling aftercompletion for " + gtx);
List cacheLoaderModifications = null;
// set any transaction wide options as current for this thread.
if (entry != null)
{
modifications = entry.getModifications();
cacheLoaderModifications = entry.getCacheLoaderModifications();
ctx.setOptionOverrides(entry.getOption());
}
if (tx != null) transactions.remove(tx);
switch (status)
{
case Status.STATUS_COMMITTED:
// if this is optimistic or sync repl
boolean onePhaseCommit = !configuration.isNodeLockingOptimistic() && configuration.getCacheMode() == Configuration.CacheMode.REPL_ASYNC;
if (log.isDebugEnabled()) log.debug("Running commit phase. One phase? " + onePhaseCommit);
runCommitPhase(ctx, gtx, tx, modifications, cacheLoaderModifications, onePhaseCommit);
log.debug("Finished commit phase");
break;
case Status.STATUS_UNKNOWN:
log.warn("Received JTA STATUS_UNKNOWN in afterCompletion()! XA resources may not be in sync. The app should manually clean up resources at this point.");
case Status.STATUS_MARKED_ROLLBACK:
case Status.STATUS_ROLLEDBACK:
log.debug("Running rollback phase");
runRollbackPhase(ctx, gtx, tx, modifications);
log.debug("Finished rollback phase");
break;
default:
throw new IllegalStateException("illegal status: " + status);
}
}
finally
{
// clean up the tx table
txTable.remove(gtx);
txTable.remove(tx);
setTransactionalContext(null, null, ctx);
cleanupInternalState();
}
}
private void assertCanContinue()
{
if (!cache.getCacheStatus().allowInvocations() && (ctx.getOptionOverrides() == null || !ctx.getOptionOverrides().isSkipCacheStatusCheck()))
throw new IllegalStateException("Cache not in STARTED state!");
}
/**
* Cleans out (nullifies) member variables held by the sync object for easier gc. Could be (falsely) seen as a mem
* leak if the TM implementation hangs on to the synchronizations for an unnecessarily long time even after the tx
* completes. See JBCACHE-1007.
*/
private void cleanupInternalState()
{
tx = null;
gtx = null;
modifications = null;
if (entry != null) entry.reset();
entry = null;
}
public String toString()
{
return "TxInterceptor.RemoteSynchronizationHandler(gtx=" + gtx + ", tx=" + getTxAsString() + ")";
}
protected String getTxAsString()
{
// JBCACHE-1114 -- don't call toString() on tx or it can lead to stack overflow
if (tx == null)
return null;
return tx.getClass().getName() + "@" + System.identityHashCode(tx);
}
}
private class LocalSynchronizationHandler extends RemoteSynchronizationHandler
{
private boolean localRollbackOnly = true;
// a VERY strange situation where a tx has remote origins, but since certain buddy group org methods perform local
// cleanups even when remotely triggered, and optimistic locking is used, you end up with an implicit local tx.
// This is STILL remotely originating though and this needs to be made explicit here.
// this can be checked by inspecting the InvocationContext.isOriginLocal() at the time of registering the sync.
private boolean remoteLocal = false;
private Option originalOptions, transactionalOptions;
/**
* A Synchronization for locally originating txs.
* <p/>
* a VERY strange situation where a tx has remote origins, but since certain buddy group org methods perform local
* cleanups even when remotely triggered, and optimistic locking is used, you end up with an implicit local tx.
* This is STILL remotely originating though and this needs to be made explicit here.
* this can be checked by inspecting the InvocationContext.isOriginLocal() at the time of registering the sync.
*
* @param gtx
* @param tx
* @param remoteLocal
*/
LocalSynchronizationHandler(GlobalTransaction gtx, Transaction tx, boolean remoteLocal)
{
super(gtx, tx);
this.remoteLocal = remoteLocal;
}
@Override
public void beforeCompletion()
{
super.beforeCompletion();
ctx.setOriginLocal(!remoteLocal); // this is the LOCAL sync handler after all!
// fetch the modifications before the transaction is committed
// (and thus removed from the txTable)
setTransactionalContext(tx, gtx, ctx);
if (!entry.existModifications())
{
if (trace) log.trace("No modifications in this tx. Skipping beforeCompletion()");
return;
}
// set any transaction wide options as current for this thread.
originalOptions = ctx.getOptionOverrides();
transactionalOptions = entry.getOption();
ctx.setOptionOverrides(transactionalOptions);
try
{
switch (tx.getStatus())
{
// if we are active or preparing then we can go ahead
case Status.STATUS_ACTIVE:
case Status.STATUS_PREPARING:
// run a prepare call.
Object result = runPreparePhase(ctx, gtx, modifications);
if (result instanceof Throwable)
{
if (log.isDebugEnabled())
log.debug("Transaction needs to be rolled back - the cache returned an instance of Throwable for this prepare call (tx=" + tx + " and gtx=" + gtx + ")", (Throwable) result);
tx.setRollbackOnly();
throw (Throwable) result;
}
break;
default:
throw new CacheException("transaction " + tx + " in status " + tx.getStatus() + " unable to start transaction");
}
}
catch (Throwable t)
{
if (log.isWarnEnabled()) log.warn("Caught exception, will now set transaction to roll back", t);
try
{
tx.setRollbackOnly();
}
catch (SystemException se)
{
throw new RuntimeException("setting tx rollback failed ", se);
}
if (t instanceof RuntimeException)
throw (RuntimeException) t;
else
throw new RuntimeException("", t);
}
finally
{
localRollbackOnly = false;
setTransactionalContext(null, null, ctx);
ctx.setOptionOverrides(originalOptions);
}
}
@Override
public void afterCompletion(int status)
{
// could happen if a rollback is called and beforeCompletion() doesn't get called.
if (ctx == null) ctx = cache.getInvocationContext();
ctx.setLocalRollbackOnly(localRollbackOnly);
ctx.setOptionOverrides(transactionalOptions);
ctx.setTransaction(tx);
ctx.setGlobalTransaction(gtx);
try
{
super.afterCompletion(status);
}
finally
{
ctx.setOptionOverrides(originalOptions);
}
}
public String toString()
{
return "TxInterceptor.LocalSynchronizationHandler(gtx=" + gtx + ", tx=" + getTxAsString() + ")";
}
}
}