/**
* Alipay.com Inc.
* Copyright (c) 2004-2012 All Rights Reserved.
*/
package com.alipay.zdal.datasource.tm;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.transaction.InvalidTransactionException;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import org.apache.log4j.Logger;
import com.alipay.zdal.datasource.resource.spi.work.Work;
import com.alipay.zdal.datasource.resource.spi.work.WorkCompletedException;
import com.alipay.zdal.datasource.resource.spi.work.WorkException;
import com.alipay.zdal.datasource.resource.util.UnexpectedThrowable;
import com.alipay.zdal.datasource.resource.util.UnreachableStatementException;
import com.alipay.zdal.datasource.tm.integrity.TransactionIntegrity;
import com.alipay.zdal.datasource.transaction.HeuristicMixedException;
import com.alipay.zdal.datasource.transaction.HeuristicRollbackException;
import com.alipay.zdal.datasource.transaction.NotSupportedException;
import com.alipay.zdal.datasource.transaction.RollbackException;
import com.alipay.zdal.datasource.transaction.Status;
import com.alipay.zdal.datasource.transaction.SystemException;
import com.alipay.zdal.datasource.transaction.Transaction;
import com.alipay.zdal.datasource.transaction.TransactionManager;
/**
* Our TransactionManager implementation.
*
*
* @author ����
* @version $Id: TxManager.java, v 0.1 2014-1-6 ����05:49:07 Exp $
*/
public class TxManager implements TransactionManager, TransactionPropagationContextImporter,
TransactionPropagationContextFactory, TransactionLocalDelegate,
TransactionTimeoutConfiguration, JBossXATerminator {
// Constants -----------------------------------------------------
// Attributes ----------------------------------------------------
/** True if the TxManager should keep a map from GlobalIds to transactions. */
private boolean globalIdsEnabled = false;
/** Whether to interrupt threads at transaction timeout */
private boolean interruptThreads = false;
/** Instance logger. */
private final Logger log = Logger.getLogger(this.getClass());
/**
* Default timeout in milliseconds.
* Must be >= 1000!
*/
private long timeOut = 5 * 60 * 1000;
// The following two fields are ints (not longs) because
// volatile 64Bit types are broken (i.e. access is not atomic) in most VMs, and we
// don't want to lock just for a statistic. Additionaly,
// it will take several years on a highly loaded system to
// exceed the int range. Note that we might loose an
// increment every now and then, since the ++ operation is
// not atomic on volatile data types.
/** A count of the transactions that have been committed */
private volatile int commitCount;
/** A count of the transactions that have been rolled back */
private volatile int rollbackCount;
/** The transaction integrity policy */
private TransactionIntegrity integrity;
// Static --------------------------------------------------------
/**
* The singleton instance.
*/
private static TxManager singleton = new TxManager();
/**
* Get a reference to the singleton instance.
*/
public static TxManager getInstance() {
return singleton;
}
// Constructors --------------------------------------------------
/**
* Private constructor for singleton. Use getInstance() to obtain
* a reference to the singleton.
*/
private TxManager() {
//make sure TxCapsule can be used
TransactionImpl.defaultXidFactory();
}
// Public --------------------------------------------------------
/**
* Setter for attribute <code>globalIdsEnabled</code>.
*/
public void setGlobalIdsEnabled(boolean newValue) {
XidImpl.setTrulyGlobalIdsEnabled(newValue);
globalIdsEnabled = newValue;
}
/**
* Getter for attribute <code>globalIdsEnabled</code>.
*/
public boolean getGlobalIdsEnabled() {
return globalIdsEnabled;
}
/**
* Enable/disable thread interruption at transaction timeout.
*
* @param interruptThreads pass true to interrupt threads, false otherwise
*/
public void setInterruptThreads(boolean interruptThreads) {
this.interruptThreads = interruptThreads;
}
/**
* Is thread interruption enabled at transaction timeout
*
* @return true for interrupt threads, false otherwise
*/
public boolean isInterruptThreads() {
return interruptThreads;
}
/**
* Set the transaction integrity policy
*
* @param integrity the transaction integrity policy
*/
public void setTransactionIntegrity(TransactionIntegrity integrity) {
this.integrity = integrity;
}
/**
* Get the transaction integrity policy
*
* @return the transaction integrity policy
*/
public TransactionIntegrity getTransactionIntegrity() {
return integrity;
}
/**
* Begin a new transaction.
* The new transaction will be associated with the calling thread.
*/
public void begin() throws NotSupportedException, SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
if (current.isDone())
disassociateThread(ti);
else
throw new NotSupportedException(
"Transaction already active, cannot nest transactions.");
}
long timeout = (ti.timeout == 0) ? timeOut : ti.timeout;
TransactionImpl tx = new TransactionImpl(timeout);
associateThread(ti, tx);
localIdTx.put(tx.getLocalId(), tx);
if (globalIdsEnabled)
globalIdTx.put(tx.getGlobalId(), tx);
if (log.isDebugEnabled())
log.debug("began tx: " + tx);
}
/**
* Commit the transaction associated with the currently running thread.
*/
public void commit() throws RollbackException, HeuristicMixedException,
HeuristicRollbackException, SecurityException, IllegalStateException,
SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
current.commit();
disassociateThread(ti);
if (log.isDebugEnabled())
log.debug("commited tx: " + current);
} else
throw new IllegalStateException("No transaction.");
}
/**
* Return the status of the transaction associated with the currently
* running thread, or <code>Status.STATUS_NO_TRANSACTION</code> if no
* active transaction is currently associated.
*/
public int getStatus() throws SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
if (current.isDone())
disassociateThread(ti);
else
return current.getStatus();
}
return Status.STATUS_NO_TRANSACTION;
}
/**
* Return the transaction currently associated with the invoking thread,
* or <code>null</code> if no active transaction is currently associated.
*/
public Transaction getTransaction() throws SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null && current.isDone()) {
current = null;
disassociateThread(ti);
}
return current;
}
/**
* Resume a transaction.
*
* Note: This will not enlist any resources involved in this
* transaction. According to JTA1.0.1 specification section 3.2.3,
* that is the responsibility of the application server.
*/
public void resume(Transaction transaction) throws InvalidTransactionException,
IllegalStateException, SystemException {
if (transaction != null && !(transaction instanceof TransactionImpl))
throw new RuntimeException("Not a TransactionImpl, but a "
+ transaction.getClass().getName());
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
if (current.isDone())
current = ti.tx = null;
else
throw new IllegalStateException("Already associated with a tx");
}
if (current != transaction) {
associateThread(ti, (TransactionImpl) transaction);
}
if (log.isDebugEnabled())
log.debug("resumed tx: " + ti.tx);
}
/**
* Suspend the transaction currently associated with the current
* thread, and return it.
*
* Note: This will not delist any resources involved in this
* transaction. According to JTA1.0.1 specification section 3.2.3,
* that is the responsibility of the application server.
*/
public Transaction suspend() throws SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
current.disassociateCurrentThread();
ti.tx = null;
if (log.isDebugEnabled())
log.debug("suspended tx: " + current);
if (current.isDone())
current = null;
}
return current;
}
/**
* Roll back the transaction associated with the currently running thread.
*/
public void rollback() throws IllegalStateException, SecurityException, SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
if (!current.isDone()) {
current.rollback();
if (log.isDebugEnabled())
log.debug("rolled back tx: " + current);
return;
}
disassociateThread(ti);
}
throw new IllegalStateException("No transaction.");
}
/**
* Mark the transaction associated with the currently running thread
* so that the only possible outcome is a rollback.
*/
public void setRollbackOnly() throws IllegalStateException, SystemException {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null) {
if (!current.isDone()) {
current.setRollbackOnly();
if (log.isDebugEnabled())
log.debug("tx marked for rollback only: " + current);
return;
}
ti.tx = null;
}
throw new IllegalStateException("No transaction.");
}
public int getTransactionTimeout() {
return (int) (getThreadInfo().timeout / 1000);
}
/**
* Set the transaction timeout for new transactions started by the
* calling thread.
*/
public void setTransactionTimeout(int seconds) throws SystemException {
getThreadInfo().timeout = 1000 * seconds;
if (log.isDebugEnabled())
log.debug("tx timeout is now: " + seconds + "s");
}
/**
* Set the default transaction timeout for new transactions.
* This default value is used if <code>setTransactionTimeout()</code>
* was never called, or if it was called with a value of <code>0</code>.
*/
public void setDefaultTransactionTimeout(int seconds) {
timeOut = 1000L * seconds;
if (log.isDebugEnabled())
log.debug("default tx timeout is now: " + seconds + "s");
}
/**
* Get the default transaction timeout.
*
* @return Default transaction timeout in seconds.
*/
public int getDefaultTransactionTimeout() {
return (int) (timeOut / 1000);
}
public long getTimeLeftBeforeTransactionTimeout(boolean errorRollback) throws RollbackException {
try {
ThreadInfo ti = getThreadInfo();
TransactionImpl current = ti.tx;
if (current != null && current.isDone()) {
disassociateThread(ti);
return -1;
}
return current.getTimeLeftBeforeTimeout(errorRollback);
} catch (RollbackException e) {
throw e;
} catch (Exception ignored) {
return -1;
}
}
/**
* The following 2 methods are here to provide association and
* disassociation of the thread.
*/
public Transaction disassociateThread() {
return disassociateThread(getThreadInfo());
}
private Transaction disassociateThread(ThreadInfo ti) {
TransactionImpl current = ti.tx;
ti.tx = null;
current.disassociateCurrentThread();
return current;
}
public void associateThread(Transaction transaction) {
if (transaction != null && !(transaction instanceof TransactionImpl))
throw new RuntimeException("Not a TransactionImpl, but a "
+ transaction.getClass().getName());
// Associate with the thread
TransactionImpl transactionImpl = (TransactionImpl) transaction;
ThreadInfo ti = getThreadInfo();
ti.tx = transactionImpl;
transactionImpl.associateCurrentThread();
}
private void associateThread(ThreadInfo ti, TransactionImpl transaction) {
// Associate with the thread
ti.tx = transaction;
transaction.associateCurrentThread();
}
/**
* Return the number of active transactions
*/
public int getTransactionCount() {
return localIdTx.size();
}
/** A count of the transactions that have been committed */
public long getCommitCount() {
return commitCount;
}
/** A count of the transactions that have been rolled back */
public long getRollbackCount() {
return rollbackCount;
}
// Implements TransactionPropagationContextImporter ---------------
/**
* Import a transaction propagation context into this TM.
* The TPC is loosely typed, as we may (at a later time) want to
* import TPCs that come from other transaction domains without
* offloading the conversion to the client.
*
* @param tpc The transaction propagation context that we want to
* import into this TM. Currently this is an instance
* of LocalId. At some later time this may be an instance
* of a transaction propagation context from another
* transaction domain like
* org.omg.CosTransactions.PropagationContext.
*
* @return A transaction representing this transaction propagation
* context, or null if this TPC cannot be imported.
*/
public Transaction importTransactionPropagationContext(Object tpc) {
if (tpc instanceof LocalId) {
LocalId id = (LocalId) tpc;
return (Transaction) localIdTx.get(id);
} else if (globalIdsEnabled && tpc instanceof GlobalId) {
GlobalId id = (GlobalId) tpc;
Transaction tx = (Transaction) globalIdTx.get(id);
if (log.isDebugEnabled() && tx != null) {
log.debug("Successfully imported transaction context " + tpc);
} else if (log.isDebugEnabled() && tx == null) {
log.debug("Could not import transaction context " + tpc);
}
return tx;
}
log.warn("Cannot import transaction propagation context: " + tpc);
return null;
}
// Implements TransactionPropagationContextFactory ---------------
/**
* Return a TPC for the current transaction.
*/
public Object getTransactionPropagationContext() {
return getTransactionPropagationContext(getThreadInfo().tx);
}
/**
* Return a TPC for the argument transaction.
*/
public Object getTransactionPropagationContext(Transaction tx) {
// If no transaction or unknown transaction class, return null.
if (tx == null)
return null;
if (!(tx instanceof TransactionImpl)) {
log.warn("Cannot export transaction propagation context: " + tx);
return null;
}
return ((TransactionImpl) tx).getLocalId();
}
// Implements XATerminator ----------------------------------
public void registerWork(Work work, Xid xid, long timeout) throws WorkCompletedException {
if (log.isDebugEnabled())
log.debug("registering work=" + work + " xid=" + xid + " timeout=" + timeout);
try {
TransactionImpl tx = importExternalTransaction(xid, timeout);
tx.setWork(work);
} catch (WorkCompletedException e) {
throw e;
} catch (Throwable t) {
WorkCompletedException e = new WorkCompletedException("Error registering work", t);
e.setErrorCode(WorkException.TX_RECREATE_FAILED);
throw e;
}
if (log.isDebugEnabled())
log.debug("registered work= " + work + " xid=" + xid + " timeout=" + timeout);
}
public void startWork(Work work, Xid xid) throws WorkCompletedException {
if (log.isDebugEnabled())
log.debug("starting work=" + work + " xid=" + xid);
TransactionImpl tx = getExternalTransaction(xid);
associateThread(tx);
if (log.isDebugEnabled())
log.debug("started work= " + work + " xid=" + xid);
}
public void endWork(Work work, Xid xid) {
if (log.isDebugEnabled())
log.debug("ending work=" + work + " xid=" + xid);
try {
TransactionImpl tx = getExternalTransaction(xid);
tx.setWork(null);
disassociateThread();
} catch (WorkCompletedException e) {
log.error("Unexpected error from endWork ", e);
throw new UnexpectedThrowable(e.toString());
}
if (log.isDebugEnabled())
log.debug("ended work=" + work + " xid=" + xid);
}
public void cancelWork(Work work, Xid xid) {
if (log.isDebugEnabled())
log.debug("cancling work=" + work + " xid=" + xid);
try {
TransactionImpl tx = getExternalTransaction(xid);
tx.setWork(null);
} catch (WorkCompletedException e) {
log.error("Unexpected error from cancelWork ", e);
throw new UnexpectedThrowable(e.toString());
}
if (log.isDebugEnabled())
log.debug("cancled work=" + work + " xid=" + xid);
}
public int prepare(Xid xid) throws XAException {
if (log.isDebugEnabled())
log.debug("preparing xid=" + xid);
try {
TransactionImpl tx = getExternalTransaction(xid);
int result = tx.prepare();
if (log.isDebugEnabled())
log.debug("prepared xid=" + xid + " result=" + result);
return result;
} catch (Throwable t) {
JBossXAException.rethrowAsXAException("Error during prepare", t);
throw new UnreachableStatementException();
}
}
public void rollback(Xid xid) throws XAException {
if (log.isDebugEnabled())
log.debug("rolling back xid=" + xid);
try {
TransactionImpl tx = getExternalTransaction(xid);
tx.rollback();
} catch (Throwable t) {
JBossXAException.rethrowAsXAException("Error during rollback", t);
}
if (log.isDebugEnabled())
log.debug("rolled back xid=" + xid);
}
public void commit(Xid xid, boolean onePhase) throws XAException {
if (log.isDebugEnabled())
log.debug("committing xid=" + xid + " onePhase=" + onePhase);
try {
TransactionImpl tx = getExternalTransaction(xid);
tx.commit(onePhase);
} catch (Throwable t) {
JBossXAException.rethrowAsXAException("Error during commit", t);
}
if (log.isDebugEnabled())
log.debug("committed xid=" + xid);
}
public void forget(Xid xid) throws XAException {
if (log.isDebugEnabled())
log.debug("forgetting xid=" + xid);
try {
TransactionImpl tx = getExternalTransaction(xid);
tx.rollback();
} catch (Throwable t) {
JBossXAException.rethrowAsXAException("Error during forget", t);
}
if (log.isDebugEnabled())
log.debug("forgot xid=" + xid);
}
public Xid[] recover(int flag) throws XAException {
// TODO recover
return new Xid[0];
}
TransactionImpl importExternalTransaction(Xid xid, long timeOut) {
GlobalId gid = new GlobalId(xid);
TransactionImpl tx = (TransactionImpl) globalIdTx.get(gid);
if (tx != null) {
if (log.isDebugEnabled())
log.debug("imported existing transaction xid: " + xid + " tx=" + tx);
} else {
ThreadInfo ti = getThreadInfo();
long timeout = (ti.timeout == 0) ? timeOut : ti.timeout;
tx = new TransactionImpl(gid, timeout);
localIdTx.put(tx.getLocalId(), tx);
if (globalIdsEnabled)
globalIdTx.put(gid, tx);
if (log.isDebugEnabled())
log.debug("imported new transaction xid: " + xid + " tx=" + tx + " timeout="
+ timeout);
}
return tx;
}
TransactionImpl getExternalTransaction(Xid xid) throws WorkCompletedException {
GlobalId gid = new GlobalId(xid);
TransactionImpl tx = (TransactionImpl) globalIdTx.get(gid);
if (tx == null)
throw new WorkCompletedException("Xid not found " + xid,
WorkException.TX_RECREATE_FAILED);
return tx;
}
// Implements TransactionLocalDelegate ----------------------
public void lock(TransactionLocal local, Transaction tx) throws InterruptedException {
TransactionImpl tximpl = (TransactionImpl) tx;
tximpl.lock();
}
public void unlock(TransactionLocal local, Transaction tx) {
TransactionImpl tximpl = (TransactionImpl) tx;
tximpl.unlock();
}
public Object getValue(TransactionLocal local, Transaction tx) {
TransactionImpl tximpl = (TransactionImpl) tx;
return tximpl.getTransactionLocalValue(local);
}
public void storeValue(TransactionLocal local, Transaction tx, Object value) {
TransactionImpl tximpl = (TransactionImpl) tx;
tximpl.putTransactionLocalValue(local, value);
}
public boolean containsValue(TransactionLocal local, Transaction tx) {
TransactionImpl tximpl = (TransactionImpl) tx;
return tximpl.containsTransactionLocal(local);
}
// Package protected ---------------------------------------------
/**
* Release the given TransactionImpl.
*/
void releaseTransactionImpl(TransactionImpl tx) {
localIdTx.remove(tx.getLocalId());
if (globalIdsEnabled)
globalIdTx.remove(tx.getGlobalId());
}
/**
* Increment the commit count
*/
void incCommitCount() {
++commitCount;
}
/**
* Increment the rollback count
*/
void incRollbackCount() {
++rollbackCount;
}
// Protected -----------------------------------------------------
// Private -------------------------------------------------------
/**
* This keeps track of the thread association with transactions
* and timeout values.
* In some cases terminated transactions may not be cleared here.
*/
private final ThreadLocal threadTx = new ThreadLocal();
/**
* This map contains the active transactions as values.
* The keys are the <code>LocalId</code>s of the transactions.
*/
private final Map localIdTx = Collections.synchronizedMap(new HashMap());
/**
* If <code>globalIdsEnabled</code> is true, this map associates
* <code>GlobalId</code>s to active transactions.
*/
private final Map globalIdTx = Collections.synchronizedMap(new HashMap());
/**
* Return the ThreadInfo for the calling thread, and create if not
* found.
*/
private ThreadInfo getThreadInfo() {
ThreadInfo ret = (ThreadInfo) threadTx.get();
if (ret == null) {
ret = new ThreadInfo();
ret.timeout = timeOut;
threadTx.set(ret);
}
return ret;
}
// Inner classes -------------------------------------------------
/**
* A simple aggregate of a thread-associated timeout value
* and a thread-associated transaction.
*/
static class ThreadInfo {
long timeout;
TransactionImpl tx;
}
}