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

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