/*
* Bitronix Transaction Manager
*
* Copyright (c) 2010, Bitronix Software.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package bitronix.tm;
import bitronix.tm.internal.*;
import bitronix.tm.journal.Journal;
import bitronix.tm.twopc.*;
import bitronix.tm.resource.ResourceRegistrar;
import bitronix.tm.resource.common.XAResourceHolder;
import bitronix.tm.utils.*;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import javax.transaction.*;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import java.io.IOException;
import java.util.*;
/**
* Implementation of {@link Transaction}.
*
* @author lorban
*/
public class BitronixTransaction implements Transaction, BitronixTransactionMBean {
private final static Logger log = LoggerFactory.getLogger(BitronixTransaction.class);
private volatile int status = Status.STATUS_NO_TRANSACTION;
private XAResourceManager resourceManager;
private Scheduler synchronizationScheduler = new Scheduler();
private List transactionStatusListeners = new ArrayList();
private boolean timeout = false;
private Date timeoutDate;
private Preparer preparer = new Preparer(TransactionManagerServices.getExecutor());
private Committer committer = new Committer(TransactionManagerServices.getExecutor());
private Rollbacker rollbacker = new Rollbacker(TransactionManagerServices.getExecutor());
/* management */
private String threadName;
private Date startDate;
public BitronixTransaction() {
Uid gtrid = UidGenerator.generateUid();
if (log.isDebugEnabled()) log.debug("creating new transaction with GTRID [" + gtrid + "]");
this.resourceManager = new XAResourceManager(gtrid);
this.threadName = Thread.currentThread().getName();
}
public int getStatus() throws SystemException {
return status;
}
public boolean enlistResource(XAResource xaResource) throws RollbackException, IllegalStateException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (status == Status.STATUS_MARKED_ROLLBACK)
throw new BitronixRollbackException("transaction has been marked as rollback only");
if (isDone())
throw new IllegalStateException("transaction started or finished 2PC, cannot enlist any more resource");
XAResourceHolder resourceHolder = ResourceRegistrar.findXAResourceHolder(xaResource);
if (resourceHolder == null)
throw new BitronixSystemException("unknown XAResource " + xaResource + ", it does not belong to a registered resource");
XAResourceHolderState resourceHolderState = new XAResourceHolderState(resourceHolder, resourceHolder.getResourceBean());
// resource timeout must be set here so manually enlisted resources can receive it
resourceHolderState.setTransactionTimeoutDate(timeoutDate);
try {
resourceManager.enlist(resourceHolderState);
} catch (XAException ex) {
if (BitronixXAException.isUnilateralRollback(ex)) {
// if the resource unilaterally rolled back, the transaction will never be able to commit -> mark it as rollback only
setStatus(Status.STATUS_MARKED_ROLLBACK);
throw new BitronixRollbackException("resource " + resourceHolderState + " unilaterally rolled back, error=" + Decoder.decodeXAExceptionErrorCode(ex), ex);
}
throw new BitronixSystemException("cannot enlist " + resourceHolderState + ", error=" + Decoder.decodeXAExceptionErrorCode(ex), ex);
}
resourceHolder.putXAResourceHolderState(resourceHolderState.getXid(), resourceHolderState);
return true;
}
public boolean delistResource(XAResource xaResource, int flag) throws IllegalStateException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (flag != XAResource.TMSUCCESS && flag != XAResource.TMSUSPEND && flag != XAResource.TMFAIL)
throw new BitronixSystemException("can only delist with SUCCESS, SUSPEND, FAIL - was: " + Decoder.decodeXAResourceFlag(flag));
if (isWorking())
throw new IllegalStateException("transaction is being committed or rolled back, cannot delist any resource now");
XAResourceHolder resourceHolder = ResourceRegistrar.findXAResourceHolder(xaResource);
if (resourceHolder == null)
throw new BitronixSystemException("unknown XAResource " + xaResource + ", it does not belong to a registered resource");
Map statesForGtrid = resourceHolder.getXAResourceHolderStatesForGtrid(resourceManager.getGtrid());
Iterator statesForGtridIt = statesForGtrid.values().iterator();
boolean result = false;
List exceptions = new ArrayList();
List resourceStates = new ArrayList();
while (statesForGtridIt.hasNext()) {
XAResourceHolderState resourceHolderState = (XAResourceHolderState) statesForGtridIt.next();
try {
result &= delistResource(resourceHolderState, flag);
} catch (BitronixSystemException ex) {
if (log.isDebugEnabled()) log.debug("failed to delist resource state " + resourceHolderState);
exceptions.add(ex);
resourceStates.add(resourceHolderState);
}
}
if (!exceptions.isEmpty()) {
BitronixMultiSystemException multiSystemException = new BitronixMultiSystemException("error delisting resource", exceptions, resourceStates);
if (!multiSystemException.isUnilateralRollback())
throw multiSystemException;
else
if (log.isDebugEnabled()) log.debug("unilateral rollback of resource " + resourceHolder, multiSystemException);
}
return result;
}
private boolean delistResource(XAResourceHolderState resourceHolderState, int flag) throws BitronixSystemException {
try {
return resourceManager.delist(resourceHolderState, flag);
}
catch (XAException ex) {
// if the resource could not be delisted, the transaction must not commit -> mark it as rollback only
if (status != Status.STATUS_MARKED_ROLLBACK)
setStatus(Status.STATUS_MARKED_ROLLBACK);
if (BitronixXAException.isUnilateralRollback(ex)) {
// The resource unilaterally rolled back here. We have to throw an exception to indicate this but
// The signature of this method is inherited from javax.transaction.Transaction. Thereof, we have choice
// between creating a sub-exception of SystemException or using a RuntimeException. Is that the best way
// forward as this 'hidden' exception can be left throw out at unexpected locations where SystemException
// should be rethrown but the exception thrown here should be catched & handled... ?
throw new BitronixRollbackSystemException("resource " + resourceHolderState + " unilaterally rolled back, error=" + Decoder.decodeXAExceptionErrorCode(ex), ex);
}
throw new BitronixSystemException("cannot delist " + resourceHolderState + ", error=" + Decoder.decodeXAExceptionErrorCode(ex), ex);
}
}
public void registerSynchronization(Synchronization synchronization) throws RollbackException, IllegalStateException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (status == Status.STATUS_MARKED_ROLLBACK)
throw new BitronixRollbackException("transaction has been marked as rollback only");
if (isDone())
throw new IllegalStateException("transaction is done, cannot register any more synchronization");
if (log.isDebugEnabled()) log.debug("registering synchronization " + synchronization);
synchronizationScheduler.add(synchronization, Scheduler.DEFAULT_POSITION);
}
public Scheduler getSynchronizationScheduler() {
return synchronizationScheduler;
}
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (isDone())
throw new IllegalStateException("transaction is done, cannot commit it");
TransactionManagerServices.getTaskScheduler().cancelTransactionTimeout(this);
// beforeCompletion must be called before the check to STATUS_MARKED_ROLLBACK as the synchronization
// can still set the status to STATUS_MARKED_ROLLBACK.
fireBeforeCompletionEvent();
// The following if statements and try/catch block must not be included in the prepare try-catch block as
// they call rollback().
// Doing so would call fireAfterCompletionEvent() twice in case one of those conditions are true.
if (timedOut()) {
if (log.isDebugEnabled()) log.debug("transaction timed out");
rollback();
throw new BitronixRollbackException("transaction timed out and has been rolled back");
}
try {
delistUnclosedResources(XAResource.TMSUCCESS);
} catch (BitronixRollbackException ex) {
if (log.isDebugEnabled()) log.debug("delistment error causing transaction rollback", ex);
rollback();
throw new BitronixRollbackException("delistment error caused transaction rollback" + ex.getMessage());
}
if (status == Status.STATUS_MARKED_ROLLBACK) {
if (log.isDebugEnabled()) log.debug("transaction marked as rollback only");
rollback();
throw new BitronixRollbackException("transaction was marked as rollback only and has been rolled back");
}
try {
List interestedResources;
// prepare phase
try {
if (log.isDebugEnabled()) log.debug("committing, " + resourceManager.size() + " enlisted resource(s)");
interestedResources = preparer.prepare(this);
}
catch (RollbackException ex) {
if (log.isDebugEnabled()) log.debug("caught rollback exception during prepare, trying to rollback");
// rollbackPrepareFailure might throw a SystemException that will 'swallow' the RollbackException which is
// what we want in that case as the transaction has not been rolled back and some resources are now left in-doubt.
rollbackPrepareFailure(ex);
throw new BitronixRollbackException("transaction failed to prepare: " + this, ex);
}
// commit phase
if (log.isDebugEnabled()) log.debug(interestedResources.size() + " interested resource(s)");
committer.commit(this, interestedResources);
if (log.isDebugEnabled()) log.debug("successfully committed " + this);
}
finally {
fireAfterCompletionEvent();
}
}
public void rollback() throws IllegalStateException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (isDone())
throw new IllegalStateException("transaction is done, cannot roll it back");
TransactionManagerServices.getTaskScheduler().cancelTransactionTimeout(this);
try {
delistUnclosedResources(XAResource.TMSUCCESS);
} catch (BitronixRollbackException ex) {
if (log.isDebugEnabled()) log.debug("some resource(s) failed delistment", ex);
}
try {
try {
if (log.isDebugEnabled()) log.debug("rolling back, " + resourceManager.size() + " enlisted resource(s)");
List resourcesToRollback = new ArrayList();
List allResources = resourceManager.getAllResources();
for (int i = 0; i < allResources.size(); i++) {
XAResourceHolderState resourceHolderState = (XAResourceHolderState) allResources.get(i);
if (!resourceHolderState.isFailed())
resourcesToRollback.add(resourceHolderState);
}
rollbacker.rollback(this, resourcesToRollback);
if (log.isDebugEnabled()) log.debug("successfully rolled back " + this);
} catch (HeuristicMixedException ex) {
throw new BitronixSystemException("transaction partly committed and partly rolled back. Resources are now inconsistent !", ex);
} catch (HeuristicCommitException ex) {
throw new BitronixSystemException("transaction committed instead of rolled back. Resources are now inconsistent !", ex);
}
} finally {
fireAfterCompletionEvent();
}
}
public void setRollbackOnly() throws IllegalStateException, SystemException {
if (status == Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction hasn't started yet");
if (isDone())
throw new IllegalStateException("transaction is done, cannot change its status");
setStatus(Status.STATUS_MARKED_ROLLBACK);
}
public XAResourceManager getResourceManager() {
return resourceManager;
}
public void timeout() throws BitronixSystemException {
this.timeout = true;
setStatus(Status.STATUS_MARKED_ROLLBACK);
log.warn("transaction timed out: " + this);
}
public boolean timedOut() {
return timeout;
}
public void setActive(int timeout) throws IllegalStateException, SystemException {
if (status != Status.STATUS_NO_TRANSACTION)
throw new IllegalStateException("transaction has already started");
setStatus(Status.STATUS_ACTIVE);
this.startDate = new Date();
this.timeoutDate = new Date(System.currentTimeMillis() + (timeout * 1000L));
TransactionManagerServices.getTaskScheduler().scheduleTransactionTimeout(this, timeoutDate);
}
public void setStatus(int status) throws BitronixSystemException {
setStatus(status, resourceManager.collectUniqueNames());
}
public void setStatus(int status, Set uniqueNames) throws BitronixSystemException {
try {
boolean force = (resourceManager.size() > 1) && (status == Status.STATUS_COMMITTING);
if (log.isDebugEnabled()) log.debug("changing transaction status to " + Decoder.decodeStatus(status) + (force ? " (forced)" : ""));
int oldStatus = this.status;
this.status = status;
Journal journal = TransactionManagerServices.getJournal();
journal.log(status, resourceManager.getGtrid(), uniqueNames);
if (force) {
journal.force();
}
if (status == Status.STATUS_ACTIVE)
ManagementRegistrar.register("bitronix.tm:type=Transaction,Gtrid=" + resourceManager.getGtrid(), this);
fireTransactionStatusChangedEvent(oldStatus, status);
} catch (IOException ex) {
// if we cannot log, the TM must stop managing TX until the problem is fixed
throw new BitronixSystemException("error logging status", ex);
}
}
private void fireTransactionStatusChangedEvent(int oldStatus, int newStatus) {
if (log.isDebugEnabled()) log.debug("transaction status is changing from " + Decoder.decodeStatus(oldStatus) + " to " +
Decoder.decodeStatus(newStatus) + " - executing " + transactionStatusListeners.size() + " listener(s)");
for (int i = 0; i < transactionStatusListeners.size(); i++) {
TransactionStatusChangeListener listener = (TransactionStatusChangeListener) transactionStatusListeners.get(i);
if (log.isDebugEnabled()) log.debug("executing TransactionStatusChangeListener " + listener);
listener.statusChanged(oldStatus, newStatus);
if (log.isDebugEnabled()) log.debug("executed TransactionStatusChangeListener " + listener);
}
}
public void addTransactionStatusChangeListener(TransactionStatusChangeListener listener) {
transactionStatusListeners.add(listener);
}
public int hashCode() {
return resourceManager.getGtrid().hashCode();
}
public boolean equals(Object obj) {
if (obj instanceof BitronixTransaction) {
BitronixTransaction tx = (BitronixTransaction) obj;
return resourceManager.getGtrid().equals(tx.resourceManager.getGtrid());
}
return false;
}
public String toString() {
return "a Bitronix Transaction with GTRID [" + resourceManager.getGtrid() + "], status=" + Decoder.decodeStatus(status) + ", " + resourceManager.size() + " resource(s) enlisted (started " + startDate + ")";
}
/*
* Internal impl
*/
/**
* Delist all resources that have not been closed before calling tm.commit(). This basically means calling
* XAResource.end() on all resource that has not been ended yet.
* @param flag the flag to pass to XAResource.end(). Either TMSUCCESS or TMFAIL.
* @throws bitronix.tm.internal.BitronixRollbackException if some resources unilaterally rolled back before end() call.
*/
private void delistUnclosedResources(int flag) throws BitronixRollbackException {
List resources = resourceManager.getAllResources();
List rolledBackResources = new ArrayList();
List failedResources = new ArrayList();
for (int i = 0; i < resources.size(); i++) {
XAResourceHolderState resourceHolderState = (XAResourceHolderState) resources.get(i);
if (!resourceHolderState.isEnded()) {
if (log.isDebugEnabled()) log.debug("found unclosed resource to delist: " + resourceHolderState);
try {
delistResource(resourceHolderState, flag);
} catch (BitronixRollbackSystemException ex) {
rolledBackResources.add(resourceHolderState);
if (log.isDebugEnabled()) log.debug("resource unilaterally rolled back: " + resourceHolderState, ex);
} catch (SystemException ex) {
failedResources.add(resourceHolderState);
log.warn("error delisting resource, assuming unilateral rollback: " + resourceHolderState, ex);
}
}
else
if (log.isDebugEnabled()) log.debug("no need to delist already closed resource: " + resourceHolderState);
} // for
if (!rolledBackResources.isEmpty() || !failedResources.isEmpty()) {
StringBuffer sb = new StringBuffer();
if (!rolledBackResources.isEmpty()) {
sb.append(System.getProperty("line.separator"));
sb.append(" resource(s) ");
sb.append(Decoder.collectResourcesNames(rolledBackResources));
sb.append(" unilaterally rolled back");
}
if (!failedResources.isEmpty()) {
sb.append(System.getProperty("line.separator"));
sb.append(" resource(s) ");
sb.append(Decoder.collectResourcesNames(failedResources));
sb.append(" could not be delisted");
}
throw new BitronixRollbackException(sb.toString());
}
}
/**
* Rollback resources after a phase 1 prepare failure. All resources must be rolled back as prepared ones
* are in-doubt and non-prepared ones have started/ended work done that must also be cleaned.
* @param rbEx the thrown rollback exception.
* @throws BitronixSystemException when a resource could not rollback prepapared state.
*/
private void rollbackPrepareFailure(RollbackException rbEx) throws BitronixSystemException {
List interestedResources = resourceManager.getAllResources();
try {
rollbacker.rollback(this, interestedResources);
if (log.isDebugEnabled()) log.debug("rollback after prepare failure succeeded");
} catch (Exception ex) {
// let's merge both exceptions' PhaseException to report a complete error message
PhaseException preparePhaseEx = (PhaseException) rbEx.getCause();
PhaseException rollbackPhaseEx = (PhaseException) ex.getCause();
List exceptions = new ArrayList();
List resources = new ArrayList();
exceptions.addAll(preparePhaseEx.getExceptions());
exceptions.addAll(rollbackPhaseEx.getExceptions());
resources.addAll(preparePhaseEx.getResourceStates());
resources.addAll(rollbackPhaseEx.getResourceStates());
throw new BitronixSystemException("transaction partially prepared and only partially rolled back. Some resources might be left in doubt!", new PhaseException(exceptions, resources));
}
}
/**
* Run all registered Synchronizations' beforeCompletion() method. Be aware that this method can change the
* transaction status to mark it as rollback only for instance.
* @throws bitronix.tm.internal.BitronixSystemException if status changing due to a synchronization throwing an
* exception fails.
*/
private void fireBeforeCompletionEvent() throws BitronixSystemException {
if (log.isDebugEnabled()) log.debug("before completion, " + synchronizationScheduler.size() + " synchronization(s) to execute");
Iterator it = synchronizationScheduler.reverseIterator();
while (it.hasNext()) {
Synchronization synchronization = (Synchronization) it.next();
try {
if (log.isDebugEnabled()) log.debug("executing synchronization " + synchronization);
synchronization.beforeCompletion();
} catch (RuntimeException ex) {
if (log.isDebugEnabled()) log.debug("Synchronization.beforeCompletion() call failed for " + synchronization + ", marking transaction as rollback only - " + ex);
setStatus(Status.STATUS_MARKED_ROLLBACK);
throw ex;
}
}
}
private void fireAfterCompletionEvent() {
// this TX is no longer in-flight -> remove this transaction's state from all XAResourceHolders
getResourceManager().clearXAResourceHolderStates();
if (log.isDebugEnabled()) log.debug("after completion, " + synchronizationScheduler.size() + " synchronization(s) to execute");
Iterator it = synchronizationScheduler.iterator();
while (it.hasNext()) {
Synchronization synchronization = (Synchronization) it.next();
try {
if (log.isDebugEnabled()) log.debug("executing synchronization " + synchronization + " with status=" + Decoder.decodeStatus(status));
synchronization.afterCompletion(status);
} catch (Exception ex) {
log.warn("Synchronization.afterCompletion() call failed for " + synchronization, ex);
}
}
ManagementRegistrar.unregister("bitronix.tm:type=Transaction,Gtrid=" + resourceManager.getGtrid());
}
private boolean isDone() {
switch (status) {
case Status.STATUS_PREPARING:
case Status.STATUS_PREPARED:
case Status.STATUS_COMMITTING:
case Status.STATUS_COMMITTED:
case Status.STATUS_ROLLING_BACK:
case Status.STATUS_ROLLEDBACK:
return true;
}
return false;
}
private boolean isWorking() {
switch (status) {
case Status.STATUS_PREPARING:
case Status.STATUS_PREPARED:
case Status.STATUS_COMMITTING:
case Status.STATUS_ROLLING_BACK:
return true;
}
return false;
}
/* management */
public String getGtrid() {
return resourceManager.getGtrid().toString();
}
public String getStatusDescription() {
return Decoder.decodeStatus(status);
}
public Collection getEnlistedResourcesUniqueNames() {
return resourceManager.collectUniqueNames();
}
public String getThreadName() {
return threadName;
}
public Date getStartDate() {
return startDate;
}
}