/** * Alipay.com Inc. * Copyright (c) 2004-2012 All Rights Reserved. */ package com.alipay.zdal.datasource.tm; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; 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.timeout.Timeout; import com.alipay.zdal.datasource.resource.util.timeout.TimeoutFactory; import com.alipay.zdal.datasource.resource.util.timeout.TimeoutTarget; 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.RollbackException; import com.alipay.zdal.datasource.transaction.Status; import com.alipay.zdal.datasource.transaction.Synchronization; import com.alipay.zdal.datasource.transaction.SystemException; import com.alipay.zdal.datasource.transaction.Transaction; /** * Our <code>Transaction</code> implementation. * * @see TxManager * * * @author ���� * @version $Id: TransactionImpl.java, v 0.1 2014-1-6 ����05:47:58 Exp $ */ public class TransactionImpl implements Transaction, TimeoutTarget { // Constants ----------------------------------------------------- /** * Code meaning "no heuristics seen", * must not be XAException.XA_HEURxxx */ private static final int HEUR_NONE = XAException.XA_RETRY; // Resource states private final static int RS_NEW = 0; // not yet enlisted private final static int RS_ENLISTED = 1; // enlisted private final static int RS_SUSPENDED = 2; // suspended private final static int RS_ENDED = 3; // not associated private final static int RS_VOTE_READONLY = 4; // voted read-only private final static int RS_VOTE_OK = 5; // voted ok private final static int RS_FORGOT = 6; // RM has forgotten // private static final ReentrantLock lock = new ReentrantLock(); // Attributes ---------------------------------------------------- /** Class logger, we don't want a new logger with every transaction. */ private static Logger log = Logger.getLogger(TransactionImpl.class); /** The ID of this transaction. */ private final XidImpl xid; /** The global id */ private final GlobalId gid; private final HashSet threads = new HashSet(1); private final Map transactionLocalMap = Collections.synchronizedMap(new HashMap()); private Throwable cause; /** * The synchronizations to call back. */ private Synchronization[] sync = new Synchronization[3]; /** * Size of allocated synchronization array. */ private int syncAllocSize = 3; /** * Count of synchronizations for this transaction. */ private int syncCount = 0; /** * A list of the XAResources that have participated in this transaction. */ private ArrayList resources = new ArrayList(3); /** * The XAResource used in the last resource gambit */ private Resource lastResource; /** * Flags that it is too late to enlist new resources. */ private boolean resourcesEnded = false; /** * Last branch id used. */ private long lastBranchId = 0; /** * Status of this transaction. */ private int status; /** * The heuristics status of this transaction. */ private int heuristicCode = HEUR_NONE; /** * The time when this transaction was started. */ private final long start; /** * The timeout handle for this transaction. */ private Timeout timeout; /** * Timeout in millisecs */ private final long timeoutPeriod; /** * Mutex for thread-safety. This should only be changed in the * <code>lock()</code> and <code>unlock()</code> methods. */ private Thread locked = null; /** * The lock depth */ private int lockDepth = 0; /** Any current work associated with the transaction */ private Work work; /** * Flags that we are done with this transaction and that it can be reused. */ private boolean done = false; // Static -------------------------------------------------------- /** * Factory for Xid instances of specified class. * This is set from the <code>TransactionManagerService</code> * MBean. */ static XidFactoryMBean xidFactory = new XidFactory(); // static TransactionManagerService txManagerService; /** The timeout factory */ // static TimeoutFactory timeoutFactory = TimeoutFactory.getSingleton(); /** * This static code is only present for testing purposes so a * tm can be usable without a lot of setup. */ static void defaultXidFactory() { // if (xidFactory == null) { // xidFactory = new XidFactory(); // } } // Constructors -------------------------------------------------- TransactionImpl(long timeout) { xid = xidFactory.newXid(); gid = xid.getTrulyGlobalId(); status = Status.STATUS_ACTIVE; start = System.currentTimeMillis(); this.timeout = TimeoutFactory.createTimeout(start + timeout, this); this.timeoutPeriod = timeout; if (log.isDebugEnabled()) { log.debug("Created new instance for tx=" + toString()); } } TransactionImpl(GlobalId gid, long timeout) { this.gid = gid; xid = xidFactory.newXid(); status = Status.STATUS_ACTIVE; start = System.currentTimeMillis(); this.timeout = TimeoutFactory.createTimeout(start + timeout, this); this.timeoutPeriod = timeout; if (log.isDebugEnabled()) { log.debug("Created new instance for tx=" + toString()); } } // Implements TimeoutTarget -------------------------------------- /** * Called when our timeout expires. */ public void timedOut(Timeout timeout) { lock(); try { log.warn("Transaction " + toString() + " timed out." + " status=" + getStringStatus(status)); if (this.timeout == null) return; // Don't race with timeout cancellation. this.timeout = null; switch (status) { case Status.STATUS_ROLLEDBACK: case Status.STATUS_COMMITTED: case Status.STATUS_NO_TRANSACTION: break; // Transaction done. case Status.STATUS_ROLLING_BACK: break; // Will be done shortly. case Status.STATUS_COMMITTING: // This is _very_ bad: // We are in the second commit phase, and have decided // to commit, but now we get a timeout and should rollback. // So we end up with a mixed decision. gotHeuristic(null, XAException.XA_HEURMIX); status = Status.STATUS_MARKED_ROLLBACK; break; // commit will fail case Status.STATUS_PREPARED: // This is bad: // We are done with the first phase, and are persistifying // our decision. Fortunately this case is currently never // hit, as we do not release the lock between the two phases. case Status.STATUS_ACTIVE: status = Status.STATUS_MARKED_ROLLBACK; // fall through.. case Status.STATUS_MARKED_ROLLBACK: // don't rollback for now, this messes up with the TxInterceptor. interruptThreads(); break; case Status.STATUS_PREPARING: status = Status.STATUS_MARKED_ROLLBACK; break; // commit will fail default: log.warn("Unknown status at timeout, tx=" + toString()); break; } } finally { unlock(); } } // Implements Transaction ---------------------------------------- public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, java.lang.SecurityException, java.lang.IllegalStateException, SystemException { lock(); try { if (log.isDebugEnabled()) log.debug("Committing, tx=" + this + ", status=" + getStringStatus(status)); beforePrepare(); if (status == Status.STATUS_ACTIVE) { switch (getCommitStrategy()) { case 0: // Zero phase commit is really fast ;-) if (log.isDebugEnabled()) log.debug("Zero phase commit " + this + ": No resources."); status = Status.STATUS_COMMITTED; break; case 1: // One phase commit if (log.isDebugEnabled()) log.debug("One phase commit " + this + ": One resource."); commitResources(true); break; default: // Two phase commit if (log.isDebugEnabled()) log.debug("Two phase commit " + this + ": Many resources."); if (!prepareResources()) { boolean commitDecision = status == Status.STATUS_PREPARED && (heuristicCode == HEUR_NONE || heuristicCode == XAException.XA_HEURCOM); // TODO: Save decision to stable storage for recovery // after system crash. if (commitDecision) commitResources(false); } else status = Status.STATUS_COMMITTED; // all was read-only } } if (status != Status.STATUS_COMMITTED) { Throwable causedByThrowable = cause; rollbackResources(); completeTransaction(); // throw jboss rollback exception with the saved off cause throw new JBossRollbackException("Unable to commit, tx=" + toString() + " status=" + getStringStatus(status), causedByThrowable); } completeTransaction(); checkHeuristics(); if (log.isDebugEnabled()) { log.debug("Committed OK, tx=" + this); } } finally { unlock(); } } public void rollback() throws java.lang.IllegalStateException, java.lang.SecurityException, SystemException { lock(); try { if (log.isDebugEnabled()) log.debug("rollback(): Entered, tx=" + toString() + " status=" + getStringStatus(status)); checkWork(); switch (status) { case Status.STATUS_ACTIVE: status = Status.STATUS_MARKED_ROLLBACK; // fall through.. case Status.STATUS_MARKED_ROLLBACK: endResources(); rollbackResources(); completeTransaction(); // Cannot throw heuristic exception, so we just have to // clear the heuristics without reporting. heuristicCode = HEUR_NONE; break; case Status.STATUS_PREPARING: // Set status to avoid race with prepareResources(). status = Status.STATUS_MARKED_ROLLBACK; break; // commit() will do rollback. default: throw new IllegalStateException("Cannot rollback(), " + "tx=" + toString() + " status=" + getStringStatus(status)); } } finally { Thread.interrupted();// clear timeout that did an interrupt unlock(); } } public boolean delistResource(XAResource xaRes, int flag) throws java.lang.IllegalStateException, SystemException { if (xaRes == null) throw new IllegalArgumentException("null xaRes tx=" + this); if (flag != XAResource.TMSUCCESS && flag != XAResource.TMSUSPEND && flag != XAResource.TMFAIL) throw new IllegalArgumentException("Bad flag: " + flag + " tx=" + this); lock(); try { if (log.isDebugEnabled()) log.debug("delistResource(): Entered, tx=" + toString() + " status=" + getStringStatus(status)); Resource resource = findResource(xaRes); if (resource == null) throw new IllegalArgumentException("xaRes not enlisted " + xaRes); switch (status) { case Status.STATUS_ACTIVE: case Status.STATUS_MARKED_ROLLBACK: break; case Status.STATUS_PREPARING: throw new IllegalStateException("Already started preparing. " + this); case Status.STATUS_ROLLING_BACK: throw new IllegalStateException("Already started rolling back. " + this); case Status.STATUS_PREPARED: throw new IllegalStateException("Already prepared. " + this); case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: throw new IllegalStateException("Already committed. " + this); case Status.STATUS_ROLLEDBACK: throw new IllegalStateException("Already rolled back. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } try { return resource.delistResource(xaRes, flag); } catch (XAException xae) { logXAException(xae); status = Status.STATUS_MARKED_ROLLBACK; cause = xae; return false; } } finally { unlock(); } } public boolean enlistResource(XAResource xaRes) throws RollbackException, java.lang.IllegalStateException, SystemException { if (xaRes == null) throw new IllegalArgumentException("null xaRes tx=" + this); lock(); try { if (log.isDebugEnabled()) log.debug("enlistResource(): Entered, tx=" + toString() + " status=" + getStringStatus(status) + " xaRes=" + xaRes); switch (status) { case Status.STATUS_ACTIVE: case Status.STATUS_PREPARING: break; case Status.STATUS_PREPARED: throw new IllegalStateException("Already prepared. " + this); case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: throw new IllegalStateException("Already committed. " + this); case Status.STATUS_MARKED_ROLLBACK: throw new RollbackException("Already marked for rollback " + this); case Status.STATUS_ROLLING_BACK: throw new RollbackException("Already started rolling back. " + this); case Status.STATUS_ROLLEDBACK: throw new RollbackException("Already rolled back. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } if (resourcesEnded) throw new IllegalStateException("Too late to enlist resources " + this); // Add resource try { Resource resource = findResource(xaRes); // Existing resource if (resource != null) { if (resource.isEnlisted()) { if (log.isDebugEnabled()) log.debug("Already enlisted: tx=" + toString() + " status=" + getStringStatus(status) + " xaRes=" + xaRes); return true; // already enlisted } if (resource.isDelisted(xaRes)) // this is a resource that returns false on all calls to // isSameRM. Further, the last resource enlisted has // already been delisted, so it is time to enlist it again. resource = null; else return resource.startResource(); } resource = findResourceManager(xaRes); if (resource != null) { // The xaRes is new. We register the xaRes with the Xid // that the RM has previously seen from this transaction, // and note that it has the same RM. resource = addResource(xaRes, resource.getXid(), resource); return resource.startResource(); } // New resource and new RM: Create a new transaction branch. resource = addResource(xaRes, createXidBranch(), null); return resource.startResource(); } catch (XAException xae) { logXAException(xae); cause = xae; return false; } } finally { unlock(); } } public int getStatus() throws SystemException { if (done) return Status.STATUS_NO_TRANSACTION; return status; } public void registerSynchronization(Synchronization s) throws RollbackException, java.lang.IllegalStateException, SystemException { if (s == null) throw new IllegalArgumentException("Null synchronization " + this); lock(); try { if (log.isDebugEnabled()) { log.debug("registerSynchronization(): Entered, " + "tx=" + toString() + " status=" + getStringStatus(status)); } switch (status) { case Status.STATUS_ACTIVE: case Status.STATUS_PREPARING: break; case Status.STATUS_PREPARED: throw new IllegalStateException("Already prepared. " + this); case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: throw new IllegalStateException("Already committed. " + this); case Status.STATUS_MARKED_ROLLBACK: throw new RollbackException("Already marked for rollback " + this); case Status.STATUS_ROLLING_BACK: throw new RollbackException("Already started rolling back. " + this); case Status.STATUS_ROLLEDBACK: throw new RollbackException("Already rolled back. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } if (syncCount == syncAllocSize) { // expand table syncAllocSize = 2 * syncAllocSize; Synchronization[] sy = new Synchronization[syncAllocSize]; System.arraycopy(sync, 0, sy, 0, syncCount); sync = sy; } sync[syncCount++] = s; } finally { unlock(); } } public void setRollbackOnly() throws java.lang.IllegalStateException, SystemException { lock(); try { if (log.isDebugEnabled()) log.debug("setRollbackOnly(): Entered, tx=" + toString() + " status=" + getStringStatus(status)); switch (status) { case Status.STATUS_ACTIVE: case Status.STATUS_PREPARING: case Status.STATUS_PREPARED: status = Status.STATUS_MARKED_ROLLBACK; // fall through.. case Status.STATUS_MARKED_ROLLBACK: case Status.STATUS_ROLLING_BACK: break; case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: throw new IllegalStateException("Already committed. " + this); case Status.STATUS_ROLLEDBACK: throw new IllegalStateException("Already rolled back. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } } finally { unlock(); } } // Public -------------------------------------------------------- public int getAssociatedThreadCount() { lock(); try { return threads.size(); } finally { unlock(); } } public Set getAssociatedThreads() { lock(); try { return Collections.unmodifiableSet(threads); } finally { unlock(); } } @Override public int hashCode() { return xid.hashCode(); } @Override public String toString() { return "TransactionImpl:" + xidFactory.toString(xid); } @Override public boolean equals(Object obj) { if (obj != null && obj instanceof TransactionImpl) return getLocalIdValue() == (((TransactionImpl) obj).getLocalIdValue()); return false; } /** * Returns the local id of this transaction. The local id is used as * a transaction propagation context within the JBoss server, and * in the TxManager for mapping local transaction ids to transactions. */ public long getLocalIdValue() { return xid.getLocalIdValue(); } /** * Returns the local id of this transaction. The local id is used as * a transaction propagation context within the JBoss server, and * in the TxManager for mapping local transaction ids to transactions. */ public LocalId getLocalId() { return xid.getLocalId(); } /** * Returns the global id of this transaction. Ths global id is used in * the TxManager, which keeps a map from global ids to transactions. */ public GlobalId getGlobalId() { return gid; } /** * Returns the xid of this transaction. */ public XidImpl getXid() { return xid; } // Package protected --------------------------------------------- void associateCurrentThread() { Thread.interrupted(); lock(); try { threads.add(Thread.currentThread()); } finally { unlock(); } } void disassociateCurrentThread() { // Just a tidyup, no need to synchronize if (done) { threads.remove(Thread.currentThread()); } else { // Removing the association for an active transaction lock(); try { threads.remove(Thread.currentThread()); } finally { unlock(); } } Thread.interrupted(); } /** * Lock this instance. */ synchronized void lock() { if (done) throw new IllegalStateException("Transaction has terminated " + this); Thread currentThread = Thread.currentThread(); if (locked != null && locked != currentThread) { if (log.isDebugEnabled()) { log.debug("Lock contention, tx=" + toString() + " otherThread=" + locked); } //DEBUG Thread.currentThread().dumpStack(); while (locked != null && locked != currentThread) { try { // Wakeup happens when: // - notify() is called from unlock() // - notifyAll is called from instanceDone() wait(); } catch (InterruptedException ex) { // ignore } if (done) throw new IllegalStateException("Transaction has now terminated " + this); } } locked = currentThread; ++lockDepth; } /** * Unlock this instance. */ synchronized void unlock() { Thread currentThread = Thread.currentThread(); if (locked == null || locked != currentThread) { log.warn("Unlocking, but not locked, tx=" + toString() + " otherThread=" + locked, new Throwable("[Stack trace]")); } else { if (--lockDepth == 0) { locked = null; notify(); } } } /** * Prepare an external transaction * * @return XAResource.XA_RDONLY or XAResource.XA_OK */ int prepare() throws HeuristicMixedException, HeuristicRollbackException, RollbackException { lock(); try { if (log.isDebugEnabled()) log.debug("Preparing, tx=" + this + ", status=" + getStringStatus(status)); checkWork(); beforePrepare(); if (status == Status.STATUS_ACTIVE) { switch (getCommitStrategy()) { case 0: // Nothing to do if (log.isDebugEnabled()) { log.debug("Prepare tx=" + this + ": No resources."); } status = Status.STATUS_COMMITTED; completeTransaction(); return XAResource.XA_RDONLY; default: { // Two phase commit if (log.isDebugEnabled()) log.debug("Prepare tx=" + this + ": Many resources."); if (prepareResources()) { if (log.isDebugEnabled()) { log.debug("Prepared tx=" + this + ": All readonly."); } status = Status.STATUS_COMMITTED; completeTransaction(); return XAResource.XA_RDONLY; } else break;/* else { boolean commitDecision = status == Status.STATUS_PREPARED && (heuristicCode == HEUR_NONE || heuristicCode == XAException.XA_HEURCOM); // TODO: Save decision to stable storage for recovery after system crash. }*/ } } } if (status != Status.STATUS_PREPARED) { // save off the cause throwable as Instance done resets it to null Throwable causedByThrowable = cause; rollbackResources(); completeTransaction(); // throw jboss rollback exception with the saved off cause throw new JBossRollbackException("Unable to prepare, tx=" + toString() + " status=" + getStringStatus(status), causedByThrowable); } // We are ok to commit return XAResource.XA_OK; } finally { unlock(); } } /** * Commit an external transaction * * @param onePhase whether the commit is one or two phase */ void commit(boolean onePhase) throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException { checkWork(); // One phase commit optimization if (onePhase) { commit(); return; } // Two phase lock(); try { if (log.isDebugEnabled()) log.debug("Committing two phase, tx=" + this + ", status=" + getStringStatus(status)); switch (status) { case Status.STATUS_PREPARING: throw new IllegalStateException("Still preparing. " + this); case Status.STATUS_ROLLING_BACK: throw new IllegalStateException("Already started rolling back. " + this); case Status.STATUS_ROLLEDBACK: instanceDone(); checkHeuristics(); throw new IllegalStateException("Already rolled back. " + this); case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: instanceDone(); checkHeuristics(); throw new IllegalStateException("Already committed. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); case Status.STATUS_MARKED_ROLLBACK: endResources(); rollbackResources(); completeTransaction(); checkHeuristics(); throw new RollbackException("Already marked for rollback " + this); case Status.STATUS_PREPARED: break; default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } commitResources(false); if (status != Status.STATUS_COMMITTED) { Throwable causedByThrowable = cause; rollbackResources(); completeTransaction(); // throw jboss rollback exception with the saved off cause throw new JBossRollbackException("Unable to commit, tx=" + toString() + " status=" + getStringStatus(status), causedByThrowable); } completeTransaction(); checkHeuristics(); if (log.isDebugEnabled()) log.debug("Committed OK, tx=" + this); } finally { unlock(); } } /** * Get the work * * @return the work */ Work getWork() { return work; } /** * Set the work * * @param work the work * @throws WorkCompletedException with error code WorkException.TX_CONCURRENT_WORK_DISALLOWED * when work is already present for the xid or whose completion is in progress, only * the global part of the xid must be used for this check. Or with error code * WorkException.TX_RECREATE_FAILED if it is unable to recreate the transaction context */ void setWork(Work work) throws WorkCompletedException { lock(); try { if (work == null) { this.work = null; return; } if (status == Status.STATUS_NO_TRANSACTION || status == Status.STATUS_UNKNOWN) throw new WorkCompletedException("The transaction is not active " + this + ": " + getStringStatus(status), WorkException.TX_RECREATE_FAILED); else if (status != Status.STATUS_ACTIVE) throw new WorkCompletedException("Too late to start work " + this + ": " + getStringStatus(status), WorkException.TX_CONCURRENT_WORK_DISALLOWED); else if (this.work != null) throw new WorkCompletedException("Already have work " + this + ": " + this.work, WorkException.TX_CONCURRENT_WORK_DISALLOWED); this.work = work; } finally { unlock(); } } /** * Getter for property done. */ boolean isDone() { return done; } // Private ------------------------------------------------------- /** * Before prepare */ private void beforePrepare() throws HeuristicMixedException, HeuristicRollbackException, RollbackException { checkIntegrity(); doBeforeCompletion(); if (log.isDebugEnabled()) log.debug("Before completion done, tx=" + this + ", status=" + getStringStatus(status)); endResources(); } /** * Check the integrity of the transaction */ private void checkIntegrity() throws HeuristicMixedException, HeuristicRollbackException, RollbackException { // Spec defined checks for the transaction in a valid state checkBeforeStatus(); TransactionIntegrity integrity = TxManager.getInstance().getTransactionIntegrity(); if (integrity != null) { // Extra integrity checks unlock(); try { integrity.checkTransactionIntegrity(this); } finally { lock(); } // Recheck the transaction state checkBeforeStatus(); } } /** * Check the before status */ private void checkBeforeStatus() throws HeuristicMixedException, HeuristicRollbackException, RollbackException { switch (status) { case Status.STATUS_PREPARING: throw new IllegalStateException("Already started preparing. " + this); case Status.STATUS_PREPARED: throw new IllegalStateException("Already prepared. " + this); case Status.STATUS_ROLLING_BACK: throw new IllegalStateException("Already started rolling back. " + this); case Status.STATUS_ROLLEDBACK: instanceDone(); checkHeuristics(); throw new IllegalStateException("Already rolled back." + this); case Status.STATUS_COMMITTING: throw new IllegalStateException("Already started committing. " + this); case Status.STATUS_COMMITTED: instanceDone(); checkHeuristics(); throw new IllegalStateException("Already committed. " + this); case Status.STATUS_NO_TRANSACTION: throw new IllegalStateException("No transaction. " + this); case Status.STATUS_UNKNOWN: throw new IllegalStateException("Unknown state " + this); case Status.STATUS_MARKED_ROLLBACK: endResources(); rollbackResources(); completeTransaction(); checkHeuristics(); throw new RollbackException("Already marked for rollback " + this); case Status.STATUS_ACTIVE: break; default: throw new IllegalStateException("Illegal status: " + getStringStatus(status) + " tx=" + this); } } /** * Complete the transaction */ private void completeTransaction() { cancelTimeout(); doAfterCompletion(); instanceDone(); } /** * Interrupt all threads involved with transaction * This is called on timeout */ private void interruptThreads() { TxManager manager = TxManager.getInstance(); if (manager.isInterruptThreads()) { HashSet clone = (HashSet) threads.clone(); threads.clear(); for (Iterator i = clone.iterator(); i.hasNext();) { Thread thread = (Thread) i.next(); try { thread.interrupt(); } catch (Throwable ignored) { if (log.isDebugEnabled()) log.debug("Ignored error interrupting thread: " + thread, ignored); } } } } /** * Return a string representation of the given status code. */ private String getStringStatus(int status) { switch (status) { case Status.STATUS_PREPARING: return "STATUS_PREPARING"; case Status.STATUS_PREPARED: return "STATUS_PREPARED"; case Status.STATUS_ROLLING_BACK: return "STATUS_ROLLING_BACK"; case Status.STATUS_ROLLEDBACK: return "STATUS_ROLLEDBACK"; case Status.STATUS_COMMITTING: return "STATUS_COMMITING"; case Status.STATUS_COMMITTED: return "STATUS_COMMITED"; case Status.STATUS_NO_TRANSACTION: return "STATUS_NO_TRANSACTION"; case Status.STATUS_UNKNOWN: return "STATUS_UNKNOWN"; case Status.STATUS_MARKED_ROLLBACK: return "STATUS_MARKED_ROLLBACK"; case Status.STATUS_ACTIVE: return "STATUS_ACTIVE"; default: return "STATUS_UNKNOWN(" + status + ")"; } } /** * Return a string representation of the given XA error code. */ private String getStringXAErrorCode(int errorCode) { switch (errorCode) { case XAException.XA_HEURCOM: return "XA_HEURCOM"; case XAException.XA_HEURHAZ: return "XA_HEURHAZ"; case XAException.XA_HEURMIX: return "XA_HEURMIX"; case XAException.XA_HEURRB: return "XA_HEURRB"; case XAException.XA_NOMIGRATE: return "XA_NOMIGRATE"; case XAException.XA_RBCOMMFAIL: return "XA_RBCOMMFAIL"; case XAException.XA_RBDEADLOCK: return "XA_RBDEADLOCK"; case XAException.XA_RBINTEGRITY: return "XA_RBINTEGRITY"; case XAException.XA_RBOTHER: return "XA_RBOTHER"; case XAException.XA_RBPROTO: return "XA_RBPROTO"; case XAException.XA_RBROLLBACK: return "XA_RBROLLBACK"; case XAException.XA_RBTIMEOUT: return "XA_RBTIMEOUT"; case XAException.XA_RBTRANSIENT: return "XA_RBTRANSIENT"; case XAException.XA_RDONLY: return "XA_RDONLY"; case XAException.XA_RETRY: return "XA_RETRY"; case XAException.XAER_ASYNC: return "XAER_ASYNC"; case XAException.XAER_DUPID: return "XAER_DUPID"; case XAException.XAER_INVAL: return "XAER_INVAL"; case XAException.XAER_NOTA: return "XAER_NOTA"; case XAException.XAER_OUTSIDE: return "XAER_OUTSIDE"; case XAException.XAER_PROTO: return "XAER_PROTO"; case XAException.XAER_RMERR: return "XAER_RMERR"; case XAException.XAER_RMFAIL: return "XAER_RMFAIL"; default: return "XA_UNKNOWN(" + errorCode + ")"; } } private void logXAException(XAException xae) { log.warn("XAException: tx=" + toString() + " errorCode=" + getStringXAErrorCode(xae.errorCode), xae); // if (txManagerService != null) // txManagerService.formatXAException(xae, log); } /** * Mark this transaction as non-existing. */ private synchronized void instanceDone() { TxManager manager = TxManager.getInstance(); if (status == Status.STATUS_COMMITTED) manager.incCommitCount(); else manager.incRollbackCount(); // Clear tables refering to external objects. // Even if a client holds on to this instance forever, the objects // that we have referenced may be garbage collected. sync = null; resources = null; transactionLocalMap.clear(); threads.clear(); // Garbage collection manager.releaseTransactionImpl(this); // Set the status status = Status.STATUS_NO_TRANSACTION; // Notify all threads waiting for the lock. notifyAll(); // set the done flag done = true; } /** * Cancel the timeout. * This will release the lock while calling out. */ private void cancelTimeout() { if (timeout != null) { unlock(); try { timeout.cancel(); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("failed to cancel timeout " + this, e); } finally { lock(); } timeout = null; } } /** * Return the resource for the given XAResource */ private Resource findResource(XAResource xaRes) { // A linear search may seem slow, but please note that // the number of XA resources registered with a transaction // are usually low. // Note: This searches backwards intentionally! It ensures that // if this resource was enlisted multiple times, then the last one // will be returned. All others should be in the state RS_ENDED. // This allows ResourceManagers that always return false from isSameRM // to be enlisted and delisted multiple times. for (int idx = resources.size() - 1; idx >= 0; --idx) { Resource resource = (Resource) resources.get(idx); if (xaRes == resource.getXAResource()) return resource; } return null; } private Resource findResourceManager(XAResource xaRes) throws XAException { for (int i = 0; i < resources.size(); ++i) { Resource resource = (Resource) resources.get(i); if (resource.isResourceManager(xaRes)) return resource; } return null; } /** * Add a resource, expanding tables if needed. * * @param xaRes The new XA resource to add. It is assumed that the * resource is not already in the table of XA resources. * @param branchXid The Xid for the transaction branch that is to * be used for associating with this resource. * @param sameRMResource The resource of the first * XA resource having the same resource manager as * <code>xaRes</code>, or <code>null</code> if <code>xaRes</code> * is the first resource seen with this resource manager. * * @return the new resource */ private Resource addResource(XAResource xaRes, Xid branchXid, Resource sameRMResource) { Resource resource = new Resource(xaRes, branchXid, sameRMResource); resources.add(resource); // Remember the first resource that wants the last resource gambit if (lastResource == null && xaRes instanceof LastResource) lastResource = resource; return resource; } /** * End Tx association for all resources. */ private void endResources() { for (int idx = 0; idx < resources.size(); ++idx) { Resource resource = (Resource) resources.get(idx); try { resource.endResource(); } catch (XAException xae) { logXAException(xae); status = Status.STATUS_MARKED_ROLLBACK; cause = xae; } } resourcesEnded = true; // Too late to enlist new resources. } /** * Call synchronization <code>beforeCompletion()</code>. * This will release the lock while calling out. */ private void doBeforeCompletion() { unlock(); try { for (int i = 0; i < syncCount; i++) { try { if (log.isDebugEnabled()) log.debug("calling sync " + i + ", " + sync[i] + " tx=" + this); sync[i].beforeCompletion(); } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("failed before completion " + sync[i], t); status = Status.STATUS_MARKED_ROLLBACK; // save the cause off so the user can inspect it cause = t; break; } } } finally { lock(); } } /** * Call synchronization <code>afterCompletion()</code>. * This will release the lock while calling out. */ private void doAfterCompletion() { // Assert: Status indicates: Too late to add new synchronizations. unlock(); try { for (int i = 0; i < syncCount; i++) { try { sync[i].afterCompletion(status); } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("failed after completion " + sync[i], t); } } } finally { lock(); } } /** * We got another heuristic. * * Promote <code>heuristicCode</code> if needed and tell * the resource to forget the heuristic. * This will release the lock while calling out. * * @param resource The resource of the XA resource that got a * heurictic in our internal tables, or <code>null</code> * if the heuristic came from here. * @param code The heuristic code, one of * <code>XAException.XA_HEURxxx</code>. */ private void gotHeuristic(Resource resource, int code) { switch (code) { case XAException.XA_HEURMIX: heuristicCode = XAException.XA_HEURMIX; break; case XAException.XA_HEURRB: if (heuristicCode == HEUR_NONE) heuristicCode = XAException.XA_HEURRB; else if (heuristicCode == XAException.XA_HEURCOM || heuristicCode == XAException.XA_HEURHAZ) heuristicCode = XAException.XA_HEURMIX; break; case XAException.XA_HEURCOM: if (heuristicCode == HEUR_NONE) heuristicCode = XAException.XA_HEURCOM; else if (heuristicCode == XAException.XA_HEURRB || heuristicCode == XAException.XA_HEURHAZ) heuristicCode = XAException.XA_HEURMIX; break; case XAException.XA_HEURHAZ: if (heuristicCode == HEUR_NONE) heuristicCode = XAException.XA_HEURHAZ; else if (heuristicCode == XAException.XA_HEURCOM || heuristicCode == XAException.XA_HEURRB) heuristicCode = XAException.XA_HEURMIX; break; default: throw new IllegalArgumentException(); } if (resource != null) resource.forget(); } /** * Check for heuristics, clear and throw exception if any found. */ private void checkHeuristics() throws HeuristicMixedException, HeuristicRollbackException { switch (heuristicCode) { case XAException.XA_HEURHAZ: heuristicCode = HEUR_NONE; if (log.isDebugEnabled()) log.debug("Throwing HeuristicMixedException, tx=" + this + "status=" + getStringStatus(status)); throw new HeuristicMixedException(); case XAException.XA_HEURMIX: heuristicCode = HEUR_NONE; if (log.isDebugEnabled()) log.debug("Throwing HeuristicMixedException, tx=" + this + "status=" + getStringStatus(status)); throw new HeuristicMixedException(); case XAException.XA_HEURRB: heuristicCode = HEUR_NONE; if (log.isDebugEnabled()) log.debug("Throwing HeuristicRollbackException, tx=" + this + "status=" + getStringStatus(status)); throw new HeuristicRollbackException(); case XAException.XA_HEURCOM: heuristicCode = HEUR_NONE; // Why isn't HeuristicCommitException used in JTA ? // And why define something that is not used ? // For now we just have to ignore this failure, even if it happened // on rollback. if (log.isDebugEnabled()) log.debug("NOT Throwing HeuristicCommitException, tx=" + this + "status=" + getStringStatus(status)); return; } } /** * Prepare all enlisted resources. * If the first phase of the commit process results in a decision * to commit the <code>status</code> will be * <code>Status.STATUS_PREPARED</code> on return. * Otherwise the <code>status</code> will be * <code>Status.STATUS_MARKED_ROLLBACK</code> on return. * This will release the lock while calling out. * * @return True iff all resources voted read-only. */ private boolean prepareResources() { boolean readOnly = true; status = Status.STATUS_PREPARING; // Prepare te XAResources for (int i = 0; i < resources.size(); ++i) { // Abort prepare on state change. if (status != Status.STATUS_PREPARING) return false; Resource resource = (Resource) resources.get(i); if (resource.isResourceManager() == false) continue; // This RM already prepared. // Ignore the last resource it is done later if (resource == lastResource) continue; try { int vote = resource.prepare(); if (vote == RS_VOTE_OK) readOnly = false; else if (vote != RS_VOTE_READONLY) { // Illegal vote: rollback. if (log.isDebugEnabled()) log.debug("illegal vote in prepare resources tx=" + this + " resource=" + resource, new Exception()); status = Status.STATUS_MARKED_ROLLBACK; return false; } } catch (XAException e) { readOnly = false; logXAException(e); switch (e.errorCode) { case XAException.XA_HEURCOM: // Heuristic commit is not that bad when preparing. // But it means trouble if we have to rollback. gotHeuristic(resource, e.errorCode); break; case XAException.XA_HEURRB: case XAException.XA_HEURMIX: case XAException.XA_HEURHAZ: gotHeuristic(resource, e.errorCode); if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; break; default: cause = e; if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; break; } } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable in prepareResources " + this, t); if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; cause = t; } } // Abort prepare on state change. if (status != Status.STATUS_PREPARING) return false; // Are we doing the last resource gambit? if (lastResource != null) { try { lastResource.prepareLastResource(); lastResource.commit(false); } catch (XAException e) { logXAException(e); switch (e.errorCode) { case XAException.XA_HEURRB: case XAException.XA_HEURCOM: case XAException.XA_HEURMIX: case XAException.XA_HEURHAZ: //usually throws an exception, but not for a couple of cases. gotHeuristic(lastResource, e.errorCode); if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; break; default: cause = e; if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; break; } } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable in prepareResources " + this, t); if (status == Status.STATUS_PREPARING) status = Status.STATUS_MARKED_ROLLBACK; cause = t; } } if (status == Status.STATUS_PREPARING) status = Status.STATUS_PREPARED; else return false; return readOnly; } /** * Commit all enlisted resources. * This will release the lock while calling out. */ private void commitResources(boolean onePhase) { status = Status.STATUS_COMMITTING; for (int i = 0; i < resources.size(); ++i) { // Abort commit on state change. if (status != Status.STATUS_COMMITTING) return; Resource resource = (Resource) resources.get(i); // Ignore the last resource, it is already committed if (onePhase == false && lastResource == resource) continue; try { resource.commit(onePhase); } catch (XAException e) { logXAException(e); switch (e.errorCode) { case XAException.XA_HEURRB: case XAException.XA_HEURCOM: case XAException.XA_HEURMIX: case XAException.XA_HEURHAZ: //usually throws an exception, but not for a couple of cases. gotHeuristic(resource, e.errorCode); //May not be correct for HEURCOM //Two phase commit is committed after prepare is logged. if (onePhase) status = Status.STATUS_MARKED_ROLLBACK; break; default: cause = e; if (onePhase) { status = Status.STATUS_MARKED_ROLLBACK; break; } //Not much we can do if there is an RMERR in the //commit phase of 2pc. I guess we try the other rms. } } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable in commitResources " + this, t); } } if (status == Status.STATUS_COMMITTING) status = Status.STATUS_COMMITTED; } /** * Rollback all enlisted resources. * This will release the lock while calling out. */ private void rollbackResources() { status = Status.STATUS_ROLLING_BACK; for (int i = 0; i < resources.size(); ++i) { Resource resource = (Resource) resources.get(i); try { resource.rollback(); } catch (XAException e) { logXAException(e); switch (e.errorCode) { case XAException.XA_HEURRB: // Heuristic rollback is not that bad when rolling back. gotHeuristic(resource, e.errorCode); continue; case XAException.XA_HEURCOM: case XAException.XA_HEURMIX: case XAException.XA_HEURHAZ: gotHeuristic(resource, e.errorCode); continue; default: cause = e; break; } } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable in rollbackResources " + this, t); } } status = Status.STATUS_ROLLEDBACK; } /** * Create an Xid representing a new branch of this transaction. */ private Xid createXidBranch() { long branchId = ++lastBranchId; return xidFactory.newBranch(xid, branchId); } /** * Determine the commit strategy * * @return 0 for nothing to do, 1 for one phase and 2 for two phase */ private int getCommitStrategy() { int resourceCount = resources.size(); if (resourceCount == 0) return 0; if (resourceCount == 1) return 1; // first XAResource surely has -1, it's the first! for (int i = 1; i < resourceCount; ++i) { Resource resource = (Resource) resources.get(i); if (resource.isResourceManager()) { // this one is not the same rm as previous ones, // there must be at least 2 return 2; } } // all rms are the same one, one phase commit is ok. return 1; } public long getTimeLeftBeforeTimeout(boolean errorRollback) throws RollbackException { if (errorRollback && status != Status.STATUS_ACTIVE) throw new RollbackException("Transaction is not active: " + TxUtils.getStatusAsString(status)); return (start + timeoutPeriod) - System.currentTimeMillis(); } Object getTransactionLocalValue(TransactionLocal tlocal) { return transactionLocalMap.get(tlocal); } void putTransactionLocalValue(TransactionLocal tlocal, Object value) { transactionLocalMap.put(tlocal, value); } boolean containsTransactionLocal(TransactionLocal tlocal) { return transactionLocalMap.containsKey(tlocal); } /** * Check we have no outstanding work * * @throws IllegalStateException when there is still work */ private void checkWork() { if (work != null) throw new IllegalStateException("Work still outstanding " + work + " tx=" + this); } // Inner classes ------------------------------------------------- /** * Represents a resource enlisted in the transaction */ private class Resource { /** The XAResource */ private final XAResource xaResource; /** The state of the resources */ private int resourceState; /** The related xa resource from the same resource manager */ private final Resource resourceSameRM; /** The Xid of this resource */ private final Xid resourceXid; /** * Create a new resource */ public Resource(XAResource xaResource, Xid resourceXid, Resource resourceSameRM) { this.xaResource = xaResource; this.resourceXid = resourceXid; this.resourceSameRM = resourceSameRM; resourceState = RS_NEW; } /** * Get the XAResource for this resource */ public XAResource getXAResource() { return xaResource; } /** * Get the Xid for this resource */ public Xid getXid() { return resourceXid; } /** * Is the resource enlisted? */ public boolean isEnlisted() { return resourceState == RS_ENLISTED; } /** * Is this a resource manager */ public boolean isResourceManager() { return resourceSameRM == null; } /** * Is this the resource manager for the passed xa resource */ public boolean isResourceManager(XAResource xaRes) throws XAException { return resourceSameRM == null && xaRes.isSameRM(xaResource); } /** * Is the resource delisted and the XAResource always returns false * for isSameRM */ public boolean isDelisted(XAResource xaRes) throws XAException { return resourceState == RS_ENDED && xaResource.isSameRM(xaRes) == false; } /** * Call <code>start()</code> on a XAResource and update * internal state information. * This will release the lock while calling out. * * @return when started, false otherwise */ public boolean startResource() throws XAException { int flags = XAResource.TMJOIN; if (resourceSameRM == null) { switch (resourceState) { case RS_NEW: flags = XAResource.TMNOFLAGS; break; case RS_SUSPENDED: flags = XAResource.TMRESUME; break; default: if (log.isDebugEnabled()) log.debug("Unhandled resource state: " + resourceState + " (not RS_NEW or RS_SUSPENDED, using TMJOIN flags)"); } } if (log.isDebugEnabled()) log.debug("startResource(" + xidFactory.toString(resourceXid) + ") entered: " + xaResource.toString() + " flags=" + flags); unlock(); // OSH FIXME: resourceState could be incorrect during this callout. try { try { xaResource.start(resourceXid, flags); } catch (XAException e) { throw e; } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable error in startResource", t); status = Status.STATUS_MARKED_ROLLBACK; return false; } // Now the XA resource is associated with a transaction. resourceState = RS_ENLISTED; } finally { lock(); if (log.isDebugEnabled()) log.debug("startResource(" + xidFactory.toString(resourceXid) + ") leaving: " + xaResource.toString() + " flags=" + flags); } return true; } /** * Delist the resource unless we already did it */ public boolean delistResource(XAResource xaRes, int flag) throws XAException { if (isDelisted(xaRes)) { // This RM always returns false on isSameRM. Further, // the last resource has already been delisted. log.warn("Resource already delisted. tx=" + this.toString()); return false; } endResource(flag); return true; } /** * End the resource */ public void endResource() throws XAException { if (resourceState == RS_ENLISTED || resourceState == RS_SUSPENDED) { if (log.isDebugEnabled()) log.debug("endresources(" + xaResource + "): state=" + resourceState); endResource(XAResource.TMSUCCESS); } } /** * Call <code>end()</code> on the XAResource and update * internal state information. * This will release the lock while calling out. * * @param flag The flag argument for the end() call. */ private void endResource(int flag) throws XAException { if (log.isDebugEnabled()) log.debug("endResource(" + xidFactory.toString(resourceXid) + ") entered: " + xaResource.toString() + " flag=" + flag); unlock(); // OSH FIXME: resourceState could be incorrect during this callout. try { try { xaResource.end(resourceXid, flag); } catch (XAException e) { throw e; } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("unhandled throwable error in endResource", t); status = Status.STATUS_MARKED_ROLLBACK; // Resource may or may not be ended after illegal exception. // We just assume it ended. resourceState = RS_ENDED; return; } // Update our internal state information if (flag == XAResource.TMSUSPEND) resourceState = RS_SUSPENDED; else { if (flag == XAResource.TMFAIL) status = Status.STATUS_MARKED_ROLLBACK; resourceState = RS_ENDED; } } finally { lock(); if (log.isDebugEnabled()) log.debug("endResource(" + xidFactory.toString(resourceXid) + ") leaving: " + xaResource.toString() + " flag=" + flag); } } /** * Forget the resource */ public void forget() { unlock(); try { xaResource.forget(resourceXid); } catch (XAException xae) { logXAException(xae); cause = xae; } finally { lock(); } resourceState = RS_FORGOT; } /** * Prepare the resource */ public int prepare() throws XAException { int vote; unlock(); try { vote = xaResource.prepare(resourceXid); } finally { lock(); } if (vote == XAResource.XA_OK) resourceState = RS_VOTE_OK; else if (vote == XAResource.XA_RDONLY) resourceState = RS_VOTE_READONLY; return resourceState; } /** * Prepare the last resource */ public void prepareLastResource() throws XAException { resourceState = RS_VOTE_OK; } /** * Commit the resource */ public void commit(boolean onePhase) throws XAException { if (log.isDebugEnabled()) log.debug("Committing resource " + xaResource + " state=" + resourceState); if (!onePhase && resourceState != RS_VOTE_OK) return; // Voted read-only at prepare phase. if (resourceSameRM != null) return; // This RM already committed. unlock(); try { xaResource.commit(resourceXid, onePhase); } finally { lock(); } } /** * Rollback the resource */ public void rollback() throws XAException { if (resourceState == RS_VOTE_READONLY) return; // Already forgotten if (resourceState == RS_FORGOT) return; if (resourceSameRM != null) return; // This RM already rolled back. unlock(); try { xaResource.rollback(resourceXid); } finally { lock(); } } } }