All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bitronix.tm.BitronixTransactionManager Maven / Gradle / Ivy

There is a newer version: 62
Show 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.LogDebugCheck;
import bitronix.tm.internal.ThreadContext;
import bitronix.tm.internal.XAResourceManager;
import bitronix.tm.utils.*;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import jakarta.transaction.*;
import javax.transaction.xa.XAException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;

/**
 * Implementation of {@link TransactionManager} and {@link UserTransaction}.
 *
 * @author Ludovic Orban
 */
public class BitronixTransactionManager
		implements TransactionManager, UserTransaction, Referenceable, Service
{

	private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(BitronixTransactionManager.class.toString());
	private static final String NO_TRANSACTION_TEXT = "no transaction started on this thread";
	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 (LogDebugCheck.isDebugEnabled())
			{
				log.finer("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 (LogDebugCheck.isDebugEnabled())
			{
				log.finer("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);
		}
	}

	/**
	 * Output BTM version information as INFO log.
	 */
	private void logVersion()
	{
		log.info("Bitronix Transaction Manager version " + Version.getVersion());
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("JVM version " + System.getProperty("java.version"));
		}
	}

	/**
	 * Method createInFlightTransactionsMap ...
	 *
	 * @return SortedMap
	 *
	 * @throws InstantiationException
	 * 		when
	 * @throws IllegalAccessException
	 * 		when
	 * @throws InvocationTargetException
	 * 		when
	 * @throws NoSuchMethodException
	 * 		when
	 */
	private SortedMap createInFlightTransactionsMap()
			throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
	{
		boolean debug = LogDebugCheck.isDebugEnabled();
		if (debug)
		{
			log.finer("Creating sorted memory storage for inflight transactions.");
		}

		Comparator timestampSortComparator = (t1, 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.finer("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.log(Level.FINER, "Concurrent sorted map 'ConcurrentSkipListMap' is not available. Falling back to a synchronized TreeMap.", e);
			}
			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 (LogDebugCheck.isDebugEnabled())
		{
			log.finer("beginning a new transaction");
		}
		if (isShuttingDown())
		{
			throw new BitronixSystemException("cannot start a new transaction, transaction manager is shutting down");
		}

		if (LogDebugCheck.isDebugEnabled())
		{
			dumpTransactionContexts();
		}

		BitronixTransaction currentTx = getCurrentTransaction();
		if (currentTx == null)
		{
			currentTx = createTransaction();
		}

		ThreadContext threadContext = ThreadContext.getContext();
		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 (LogDebugCheck.isDebugEnabled())
			{
				log.finer("begun new transaction at " + new Date(currentTx.getResourceManager()
				                                                          .getGtrid()
				                                                          .extractTimestamp()));
			}
		}
		catch (RuntimeException | SystemException ex)
		{
			clearContextSynchronization.afterCompletion(Status.STATUS_NO_TRANSACTION);
			throw ex;
		}
	}

	/**
	 * Method commit ...
	 *
	 * @throws RollbackException
	 * 		when
	 * @throws HeuristicMixedException
	 * 		when
	 * @throws HeuristicRollbackException
	 * 		when
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException
	{
		BitronixTransaction currentTx = getCurrentTransaction();
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("committing transaction " + currentTx);
		}
		if (currentTx == null)
		{
			throw new IllegalStateException(NO_TRANSACTION_TEXT);
		}

		currentTx.commit();
	}

	/**
	 * Method getStatus returns the status of this BitronixTransactionManager object.
	 *
	 * @return the status (type int) of this BitronixTransactionManager object.
	 *
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public int getStatus() throws SystemException
	{
		BitronixTransaction currentTx = getCurrentTransaction();
		if (currentTx == null)
		{
			return Status.STATUS_NO_TRANSACTION;
		}

		return currentTx.getStatus();
	}

	/**
	 * Method getTransaction returns the transaction of this BitronixTransactionManager object.
	 *
	 * @return the transaction (type Transaction) of this BitronixTransactionManager object.
	 *
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public Transaction getTransaction() throws SystemException
	{
		return getCurrentTransaction();
	}

	/**
	 * Method resume ...
	 *
	 * @param transaction
	 * 		of type Transaction
	 *
	 * @throws InvalidTransactionException
	 * 		when
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public void resume(Transaction transaction) throws InvalidTransactionException, SystemException
	{
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("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.getContext();
			threadContext.setTransaction(tx);
			inFlightTransactions.get(tx)
			                    .setThreadContext(threadContext);
		}
		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);
		}
	}

	/**
	 * Method rollback ...
	 *
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public void rollback() throws SystemException
	{
		BitronixTransaction currentTx = getCurrentTransaction();
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("rolling back transaction " + currentTx);
		}
		if (currentTx == null)
		{
			throw new IllegalStateException(NO_TRANSACTION_TEXT);
		}

		currentTx.rollback();
	}

	/**
	 * Method setRollbackOnly ...
	 *
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public void setRollbackOnly() throws SystemException
	{
		BitronixTransaction currentTx = getCurrentTransaction();
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("marking transaction as rollback only: " + currentTx);
		}
		if (currentTx == null)
		{
			throw new IllegalStateException(NO_TRANSACTION_TEXT);
		}

		currentTx.setRollbackOnly();
	}

	/**
	 * Method setTransactionTimeout sets the transactionTimeout of this BitronixTransactionManager object.
	 *
	 * @param seconds
	 * 		the transactionTimeout of this BitronixTransactionManager object.
	 *
	 * @throws SystemException
	 * 		when
	 */
	@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.getContext()
		             .setTimeout(seconds);
	}

	/**
	 * Method suspend ...
	 *
	 * @return Transaction
	 *
	 * @throws SystemException
	 * 		when
	 */
	@Override
	public Transaction suspend() throws SystemException
	{
		BitronixTransaction currentTx = getCurrentTransaction();
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("suspending transaction " + currentTx);
		}
		if (currentTx == null)
		{
			return null;
		}

		try
		{
			currentTx.getResourceManager()
			         .suspend();
			clearCurrentContextForSuspension();
			inFlightTransactions.get(currentTx)
			                    .setThreadContext(null);
			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);
		}
	}

	/**
	 * Unlink the transaction from the current thread's context.
	 */
	private void clearCurrentContextForSuspension()
	{
		if (LogDebugCheck.isDebugEnabled())
		{
			log.finer("clearing current thread context: " + ThreadContext.getContext());
		}
		ThreadContext.getContext()
		             .clearTransaction();
	}

	/**
	 * 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 (!LogDebugCheck.isDebugEnabled())
		{
			return;
		}

		// We're using an iterator, so we must synchronize on the collection
		synchronized (inFlightTransactions)
		{
			log.finer("dumping " + inFlightTransactions.size() + " transaction context(s)");
			for (BitronixTransaction tx : inFlightTransactions.keySet())
			{
				log.finer(tx.toString());
			}
		}
	}

	/**
	 * 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.getContext()
		                    .getTransaction();
	}

	/**
	 * Create a new transaction on the current thread's context.
	 *
	 * @return the created transaction.
	 */
	private BitronixTransaction createTransaction()
	{
		BitronixTransaction transaction = new BitronixTransaction();
		ThreadContext.getContext()
		             .setTransaction(transaction);
		return transaction;
	}

	/**
	 * 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 (LogDebugCheck.isDebugEnabled())
			{
				log.finer("oldest in-flight transaction's timestamp: " + oldestTimestamp);
			}
			return oldestTimestamp;

		}
		catch (NoSuchElementException e)
		{
			if (LogDebugCheck.isDebugEnabled())
			{
				log.log(Level.FINER, "oldest in-flight transaction's timestamp: " + Long.MIN_VALUE, e);
			}
			return Long.MIN_VALUE;
		}
	}

	/*
	 * Internal impl
	 */

	/**
	 * 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 jakarta.transaction.TransactionManager#begin()}) will be rejected with a {@link SystemException}. * * @see Configuration#getGracefulShutdownInterval() */ @Override public synchronized void shutdown() { if (isShuttingDown()) { if (LogDebugCheck.isDebugEnabled()) { log.finer("Transaction Manager has already shut down"); } return; } log.info("shutting down Bitronix Transaction Manager"); internalShutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down resource loader"); } TransactionManagerServices.getResourceLoader() .shutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down executor"); } TransactionManagerServices.getExecutor() .shutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down task scheduler"); } TransactionManagerServices.getTaskScheduler() .shutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down journal"); } TransactionManagerServices.getJournal() .shutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down recoverer"); } TransactionManagerServices.getRecoverer() .shutdown(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutting down configuration"); } TransactionManagerServices.getConfiguration() .shutdown(); // clear references TransactionManagerServices.clear(); if (LogDebugCheck.isDebugEnabled()) { log.finer("shutdown ran successfully"); } } /** * Method internalShutdown ... */ private void internalShutdown() { shuttingDown = true; dumpTransactionContexts(); int seconds = TransactionManagerServices.getConfiguration() .getGracefulShutdownInterval(); int txCount = 0; try { txCount = inFlightTransactions.size(); while (seconds > 0 && txCount > 0) { if (LogDebugCheck.isDebugEnabled()) { log.finer("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.log(Level.SEVERE, "cannot get a list of in-flight transactions", ex); } if (txCount > 0) { if (LogDebugCheck.isDebugEnabled()) { log.finer("still " + txCount + " in-flight transactions, shutting down anyway"); dumpTransactionContexts(); } } else { if (LogDebugCheck.isDebugEnabled()) { log.finer("all transactions finished, resuming shutdown"); } } } /** * Method toString ... * * @return String */ @Override public String toString() { return "a BitronixTransactionManager with " + inFlightTransactions.size() + " in-flight transaction(s)"; } private final class ClearContextSynchronization implements Synchronization { private final BitronixTransaction currentTx; private final AtomicReference threadContext; /** * Constructor ClearContextSynchronization creates a new ClearContextSynchronization instance. * * @param currentTx * of type BitronixTransaction * @param threadContext * of type ThreadContext */ @SuppressWarnings("WeakerAccess") public ClearContextSynchronization(BitronixTransaction currentTx, ThreadContext threadContext) { this.currentTx = currentTx; this.threadContext = new AtomicReference<>(threadContext); } /** * Method beforeCompletion ... */ @Override public void beforeCompletion() { //Nothing to do } /** * Method afterCompletion ... * * @param status * of type int */ @Override public void afterCompletion(int status) { ThreadContext context = threadContext.get(); if (context != null) { if (LogDebugCheck.isDebugEnabled()) { log.finer("clearing transaction from thread context: " + context); } context.clearTransaction(); } else { if (LogDebugCheck.isDebugEnabled()) { log.finer("thread context was null when clear context synchronization executed"); } } if (LogDebugCheck.isDebugEnabled()) { log.finer("removing transaction from in-flight transactions: " + currentTx); } inFlightTransactions.remove(currentTx); } /** * Method setThreadContext sets the threadContext of this ClearContextSynchronization object. * * @param threadContext * the threadContext of this ClearContextSynchronization object. */ @SuppressWarnings("WeakerAccess") public void setThreadContext(ThreadContext threadContext) { this.threadContext.set(threadContext); } /** * Method toString ... * * @return String */ @Override public String toString() { return "a ClearContextSynchronization for " + currentTx; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy