
bitronix.tm.BitronixTransactionManager Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2006-2013 Bitronix Software (http://www.bitronix.be)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package bitronix.tm;
import bitronix.tm.internal.BitronixSystemException;
import bitronix.tm.internal.ThreadContext;
import bitronix.tm.internal.XAResourceManager;
import bitronix.tm.utils.ClassLoaderUtils;
import bitronix.tm.utils.Decoder;
import bitronix.tm.utils.InitializationException;
import bitronix.tm.utils.MonotonicClock;
import bitronix.tm.utils.Scheduler;
import bitronix.tm.utils.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import javax.transaction.xa.XAException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
/**
* Implementation of {@link TransactionManager} and {@link UserTransaction}.
*
* @author Ludovic Orban
*/
public class BitronixTransactionManager implements TransactionManager, UserTransaction, Referenceable, Service {
private final static Logger log = LoggerFactory.getLogger(BitronixTransactionManager.class);
private final static String MDC_GTRID_KEY = "btm-gtrid";
private final SortedMap inFlightTransactions;
private volatile boolean shuttingDown;
/**
* Create the {@link BitronixTransactionManager}. Open the journal, load resources and perform recovery
* synchronously. The recovery service then gets scheduled for background recovery.
*/
public BitronixTransactionManager() {
try {
shuttingDown = false;
logVersion();
Configuration configuration = TransactionManagerServices.getConfiguration();
configuration.buildServerIdArray(); // first call will initialize the ServerId
if (log.isDebugEnabled()) { log.debug("starting BitronixTransactionManager using " + configuration); }
TransactionManagerServices.getJournal().open();
TransactionManagerServices.getResourceLoader().init();
TransactionManagerServices.getRecoverer().run();
int backgroundRecoveryInterval = TransactionManagerServices.getConfiguration().getBackgroundRecoveryIntervalSeconds();
if (backgroundRecoveryInterval < 1) {
throw new InitializationException("invalid configuration value for backgroundRecoveryIntervalSeconds, found '" + backgroundRecoveryInterval + "' but it must be greater than 0");
}
inFlightTransactions = createInFlightTransactionsMap();
if (log.isDebugEnabled()) { log.debug("recovery will run in the background every " + backgroundRecoveryInterval + " second(s)"); }
Date nextExecutionDate = new Date(MonotonicClock.currentTimeMillis() + (backgroundRecoveryInterval * 1000L));
TransactionManagerServices.getTaskScheduler().scheduleRecovery(TransactionManagerServices.getRecoverer(), nextExecutionDate);
} catch (IOException ex) {
throw new InitializationException("cannot open disk journal", ex);
} catch (Exception ex) {
TransactionManagerServices.getJournal().shutdown();
TransactionManagerServices.getResourceLoader().shutdown();
throw new InitializationException("initialization failed, cannot safely start the transaction manager", ex);
}
}
private SortedMap createInFlightTransactionsMap()
throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
final boolean debug = log.isDebugEnabled();
if (debug) { log.debug("Creating sorted memory storage for inflight transactions."); }
final Comparator timestampSortComparator = new Comparator() {
@Override
public int compare(BitronixTransaction t1, BitronixTransaction t2) {
Long timestamp1 = t1.getResourceManager().getGtrid().extractTimestamp();
Long timestamp2 = t2.getResourceManager().getGtrid().extractTimestamp();
int compareTo = timestamp1.compareTo(timestamp2);
if (compareTo == 0 && !t1.getResourceManager().getGtrid().equals(t2.getResourceManager().getGtrid())) {
// if timestamps are equal, use the Uid as the tie-breaker. the !equals() check above avoids an expensive string compare() here.
return t1.getGtrid().compareTo(t2.getGtrid());
}
return compareTo;
}
};
if (debug) { log.debug("Attempting to use a concurrent sorted map of type 'ConcurrentSkipListMap' (from jre6 or custom supplied backport)"); }
try {
@SuppressWarnings("unchecked")
SortedMap mapInstance = (SortedMap)
ClassLoaderUtils.loadClass("java.util.concurrent.ConcurrentSkipListMap").
getConstructor(Comparator.class).newInstance(timestampSortComparator);
return mapInstance;
} catch (ClassNotFoundException e) {
if (debug) { log.debug("Concurrent sorted map 'ConcurrentSkipListMap' is not available. Falling back to a synchronized TreeMap."); }
return Collections.synchronizedSortedMap(
new TreeMap(timestampSortComparator));
}
}
/**
* Start a new transaction and bind the context to the calling thread.
* @throws NotSupportedException if a transaction is already bound to the calling thread.
* @throws SystemException if the transaction manager is shutting down.
*/
@Override
public void begin() throws NotSupportedException, SystemException {
if (log.isDebugEnabled()) { log.debug("beginning a new transaction"); }
if (isShuttingDown())
throw new BitronixSystemException("cannot start a new transaction, transaction manager is shutting down");
if (log.isDebugEnabled()) {
dumpTransactionContexts();
}
BitronixTransaction currentTx = getCurrentTransaction();
if (currentTx != null)
throw new NotSupportedException("nested transactions not supported");
currentTx = createTransaction();
ThreadContext threadContext = ThreadContext.getThreadContext();
ClearContextSynchronization clearContextSynchronization = new ClearContextSynchronization(currentTx, threadContext);
try {
currentTx.getSynchronizationScheduler().add(clearContextSynchronization, Scheduler.ALWAYS_LAST_POSITION -1);
currentTx.setActive(threadContext.getTimeout());
inFlightTransactions.put(currentTx, clearContextSynchronization);
if (log.isDebugEnabled()) { log.debug("begun new transaction at " + new Date(currentTx.getResourceManager().getGtrid().extractTimestamp())); }
} catch (RuntimeException ex) {
clearContextSynchronization.afterCompletion(Status.STATUS_NO_TRANSACTION);
throw ex;
} catch (SystemException ex) {
clearContextSynchronization.afterCompletion(Status.STATUS_NO_TRANSACTION);
throw ex;
}
}
@Override
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException {
BitronixTransaction currentTx = getCurrentTransaction();
if (log.isDebugEnabled()) { log.debug("committing transaction " + currentTx); }
if (currentTx == null)
throw new IllegalStateException("no transaction started on this thread");
currentTx.commit();
}
@Override
public void rollback() throws IllegalStateException, SecurityException, SystemException {
BitronixTransaction currentTx = getCurrentTransaction();
if (log.isDebugEnabled()) { log.debug("rolling back transaction " + currentTx); }
if (currentTx == null)
throw new IllegalStateException("no transaction started on this thread");
currentTx.rollback();
}
@Override
public int getStatus() throws SystemException {
BitronixTransaction currentTx = getCurrentTransaction();
if (currentTx == null)
return Status.STATUS_NO_TRANSACTION;
return currentTx.getStatus();
}
@Override
public Transaction getTransaction() throws SystemException {
return getCurrentTransaction();
}
@Override
public void setRollbackOnly() throws IllegalStateException, SystemException {
BitronixTransaction currentTx = getCurrentTransaction();
if (log.isDebugEnabled()) { log.debug("marking transaction as rollback only: " + currentTx); }
if (currentTx == null)
throw new IllegalStateException("no transaction started on this thread");
currentTx.setRollbackOnly();
}
@Override
public void setTransactionTimeout(int seconds) throws SystemException {
if (seconds < 0)
throw new BitronixSystemException("cannot set a timeout to less than 0 second (was: " + seconds + "s)");
ThreadContext.getThreadContext().setTimeout(seconds);
}
@Override
public Transaction suspend() throws SystemException {
BitronixTransaction currentTx = getCurrentTransaction();
if (log.isDebugEnabled()) { log.debug("suspending transaction " + currentTx); }
if (currentTx == null)
return null;
try {
currentTx.getResourceManager().suspend();
clearCurrentContextForSuspension();
inFlightTransactions.get(currentTx).setThreadContext(null);
MDC.remove(MDC_GTRID_KEY);
return currentTx;
} catch (XAException ex) {
String extraErrorDetails = TransactionManagerServices.getExceptionAnalyzer().extractExtraXAExceptionDetails(ex);
throw new BitronixSystemException("cannot suspend " + currentTx + ", error=" + Decoder.decodeXAExceptionErrorCode(ex) +
(extraErrorDetails == null ? "" : ", extra error=" + extraErrorDetails), ex);
}
}
@Override
public void resume(Transaction transaction) throws InvalidTransactionException, IllegalStateException, SystemException {
if (log.isDebugEnabled()) { log.debug("resuming " + transaction); }
if (transaction == null)
throw new InvalidTransactionException("resumed transaction cannot be null");
if (!(transaction instanceof BitronixTransaction))
throw new InvalidTransactionException("resumed transaction must be an instance of BitronixTransaction");
BitronixTransaction tx = (BitronixTransaction) transaction;
if (getCurrentTransaction() != null)
throw new IllegalStateException("a transaction is already running on this thread");
try {
XAResourceManager resourceManager = tx.getResourceManager();
resourceManager.resume();
ThreadContext threadContext = ThreadContext.getThreadContext();
threadContext.setTransaction(tx);
inFlightTransactions.get(tx).setThreadContext(threadContext);
MDC.put(MDC_GTRID_KEY, tx.getGtrid());
} catch (XAException ex) {
String extraErrorDetails = TransactionManagerServices.getExceptionAnalyzer().extractExtraXAExceptionDetails(ex);
throw new BitronixSystemException("cannot resume " + tx + ", error=" + Decoder.decodeXAExceptionErrorCode(ex) +
(extraErrorDetails == null ? "" : ", extra error=" + extraErrorDetails), ex);
}
}
/**
* BitronixTransactionManager can only have a single instance per JVM so this method always returns a reference
* with no special information to find back the sole instance. BitronixTransactionManagerObjectFactory will be used
* by the JNDI server to get the BitronixTransactionManager instance of the JVM.
*
* @return an empty reference to get the BitronixTransactionManager.
*/
@Override
public Reference getReference() throws NamingException {
return new Reference(
BitronixTransactionManager.class.getName(),
new StringRefAddr("TransactionManager", "BitronixTransactionManager"),
BitronixTransactionManagerObjectFactory.class.getName(),
null
);
}
/**
* Return a count of the current in-flight transactions. Currently this method is only called by unit tests.
* @return a count of in-flight transactions
*/
public int getInFlightTransactionCount() {
return inFlightTransactions.size();
}
/**
* Return the timestamp of the oldest in-flight transaction.
* @return the timestamp or Long.MIN_VALUE if there is no in-flight transaction.
*/
public long getOldestInFlightTransactionTimestamp() {
try {
// The inFlightTransactions map is sorted by timestamp, so the first transaction is always the oldest
BitronixTransaction oldestTransaction = inFlightTransactions.firstKey();
long oldestTimestamp = oldestTransaction.getResourceManager().getGtrid().extractTimestamp();
if (log.isDebugEnabled()) { log.debug("oldest in-flight transaction's timestamp: " + oldestTimestamp); }
return oldestTimestamp;
} catch (NoSuchElementException e) {
if (log.isDebugEnabled()) { log.debug("oldest in-flight transaction's timestamp: " + Long.MIN_VALUE); }
return Long.MIN_VALUE;
}
}
/**
* Get the transaction currently registered on the current thread context.
* @return the current transaction or null if no transaction has been started on the current thread.
*/
public BitronixTransaction getCurrentTransaction() {
return ThreadContext.getThreadContext().getTransaction();
}
/**
* Check if the transaction manager is in the process of shutting down.
* @return true if the transaction manager is in the process of shutting down.
*/
private boolean isShuttingDown() {
return shuttingDown;
}
/**
* Dump an overview of all running transactions as debug logs.
*/
public void dumpTransactionContexts() {
if (!log.isDebugEnabled())
return;
// We're using an iterator, so we must synchronize on the collection
synchronized (inFlightTransactions) {
log.debug("dumping " + inFlightTransactions.size() + " transaction context(s)");
for (BitronixTransaction tx : inFlightTransactions.keySet()) {
log.debug(tx.toString());
}
}
}
/**
* Shut down the transaction manager and release all resources held by it.
* This call will also close the resources pools registered by the {@link bitronix.tm.resource.ResourceLoader}
* like JMS and JDBC pools. The manually created ones are left untouched.
* The Transaction Manager will wait during a configurable graceful period before forcibly killing active
* transactions.
* After this method is called, attempts to create new transactions (via calls to
* {@link javax.transaction.TransactionManager#begin()}) will be rejected with a {@link SystemException}.
* @see Configuration#getGracefulShutdownInterval()
*/
@Override
public synchronized void shutdown() {
if (isShuttingDown()) {
if (log.isDebugEnabled()) { log.debug("Transaction Manager has already shut down"); }
return;
}
log.info("shutting down Bitronix Transaction Manager");
internalShutdown();
if (log.isDebugEnabled()) { log.debug("shutting down resource loader"); }
TransactionManagerServices.getResourceLoader().shutdown();
if (log.isDebugEnabled()) { log.debug("shutting down executor"); }
TransactionManagerServices.getExecutor().shutdown();
if (log.isDebugEnabled()) { log.debug("shutting down task scheduler"); }
TransactionManagerServices.getTaskScheduler().shutdown();
if (log.isDebugEnabled()) { log.debug("shutting down journal"); }
TransactionManagerServices.getJournal().shutdown();
if (log.isDebugEnabled()) { log.debug("shutting down recoverer"); }
TransactionManagerServices.getRecoverer().shutdown();
if (log.isDebugEnabled()) { log.debug("shutting down configuration"); }
TransactionManagerServices.getConfiguration().shutdown();
// clear references
TransactionManagerServices.clear();
if (log.isDebugEnabled()) { log.debug("shutdown ran successfully"); }
}
private void internalShutdown() {
shuttingDown = true;
dumpTransactionContexts();
int seconds = TransactionManagerServices.getConfiguration().getGracefulShutdownInterval();
int txCount = 0;
try {
txCount = inFlightTransactions.size();
while (seconds > 0 && txCount > 0) {
if (log.isDebugEnabled()) { log.debug("still " + txCount + " in-flight transactions, waiting... (" + seconds + " second(s) left)"); }
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
// ignore
}
seconds--;
txCount = inFlightTransactions.size();
}
} catch (Exception ex) {
log.error("cannot get a list of in-flight transactions", ex);
}
if (txCount > 0) {
if (log.isDebugEnabled()) {
log.debug("still " + txCount + " in-flight transactions, shutting down anyway");
dumpTransactionContexts();
}
}
else {
if (log.isDebugEnabled()) { log.debug("all transactions finished, resuming shutdown"); }
}
}
@Override
public String toString() {
return "a BitronixTransactionManager with " + inFlightTransactions.size() + " in-flight transaction(s)";
}
/*
* Internal impl
*/
/**
* Output BTM version information as INFO log.
*/
private void logVersion() {
log.info("Bitronix Transaction Manager version " + Version.getVersion());
if (log.isDebugEnabled()) { log.debug("JVM version " + System.getProperty("java.version")); }
}
/**
* Create a new transaction on the current thread's context.
* @return the created transaction.
*/
private BitronixTransaction createTransaction() {
BitronixTransaction transaction = new BitronixTransaction();
ThreadContext.getThreadContext().setTransaction(transaction);
MDC.put(MDC_GTRID_KEY, transaction.getGtrid());
return transaction;
}
/**
* Unlink the transaction from the current thread's context.
*/
private void clearCurrentContextForSuspension() {
if (log.isDebugEnabled()) { log.debug("clearing current thread context: " + ThreadContext.getThreadContext()); }
ThreadContext.getThreadContext().clearTransaction();
}
private final class ClearContextSynchronization implements Synchronization {
private final BitronixTransaction currentTx;
private final AtomicReference threadContext;
public ClearContextSynchronization(BitronixTransaction currentTx, ThreadContext threadContext) {
this.currentTx = currentTx;
this.threadContext = new AtomicReference(threadContext);
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
ThreadContext context = threadContext.get();
if (context != null) {
if (log.isDebugEnabled()) { log.debug("clearing transaction from thread context: " + context); }
context.clearTransaction();
}
else {
if (log.isDebugEnabled()) { log.debug("thread context was null when clear context synchronization executed"); }
}
if (log.isDebugEnabled()) { log.debug("removing transaction from in-flight transactions: " + currentTx); }
inFlightTransactions.remove(currentTx);
MDC.remove(MDC_GTRID_KEY);
}
public void setThreadContext(ThreadContext threadContext) {
this.threadContext.set(threadContext);
}
@Override
public String toString() {
return "a ClearContextSynchronization for " + currentTx;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy