org.alfresco.util.transaction.SpringAwareUserTransaction.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.util.transaction.SpringAwareUserTransaction.java

Source

/*
 * Copyright (C) 2005-2014 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alfresco.util.transaction;

import java.lang.reflect.Method;

import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.alfresco.error.StackTraceUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * A <code>UserTransaction</code> that will allow the thread using it to participate
 * in transactions that are normally only begun and committed by the <b>SpringFramework</b>
 * transaction aware components.
 * <p>
 * Client code can use this class directly, but should be very careful to handle the exception
 * conditions with the appropriate <code>finally</code> blocks and <code>rollback</code> code.
 * It is recommended that clients use this class indirectly via an instance of the
 * {@link org.alfresco.repo.transaction.RetryingTransactionHelper}.
 * <p>
 * This class is thread-safe in that it will detect multithreaded access and throw
 * exceptions.  Therefore </b>do not use on multiple threads</b>.  Instances should be
 * used only for the duration of the required user transaction and then discarded.
 * Any attempt to reuse an instance will result in failure.
 * <p>
 * Nested user transaction are allowed.
 * <p>
 * <b>Logging:</b><br/>
 * To dump exceptions during commits, turn debugging on for this class.<br/>
 * To log leaked transactions i.e. a begin() is not matched by a commit() or rollback(),
 * add <i>.trace</i> to the usual classname-based debug category and set to WARN log
 * level. This will log the first detection of a leaked transaction and automatically enable
 * transaction call stack logging for subsequent leaked transactions.  To enforce
 * call stack logging from the start set the <i>.trace</i> log level to DEBUG. Call stack
 * logging will hamper performance but is useful when it appears that something is eating
 * connections or holding onto resources - usually a sign that client code hasn't handled all
 * possible exception conditions.
 * 
 * @see org.springframework.transaction.PlatformTransactionManager
 * @see org.springframework.transaction.support.DefaultTransactionDefinition
 * 
 * @author Derek Hulley
 */
public class SpringAwareUserTransaction extends TransactionAspectSupport
        implements UserTransaction, TransactionAttributeSource, TransactionAttribute {
    /*
     * There is some extra work in here to perform safety checks against the thread ID.
     * This is because this class doesn't operate in an environment that guarantees that the
     * thread coming into the begin() method is the same as the thread forcing commit() or
     * rollback().
     */

    private static final long serialVersionUID = 3762538897183224373L;

    private static final String NAME = "UserTransaction";

    private static final Log logger = LogFactory.getLog(SpringAwareUserTransaction.class);

    /*
     * Leaked Transaction Logging
     */
    private static final Log traceLogger = LogFactory.getLog(SpringAwareUserTransaction.class.getName() + ".trace");
    private static volatile boolean isCallStackTraced = false;

    static {
        if (traceLogger.isDebugEnabled()) {
            isCallStackTraced = true;
            traceLogger.warn("Logging of transaction call stack is enforced and will affect performance");
        }
    }

    static boolean isCallStackTraced() {
        return isCallStackTraced;
    }

    /** stores whether begin() & commit()/rollback() methods calls are balanced */
    private boolean isBeginMatched = true;
    /** stores the begin() call stack when auto tracing */
    private StackTraceElement[] beginCallStack;

    private boolean readOnly;
    private int isolationLevel;
    private int propagationBehaviour;
    private int timeout;

    /** Stores the user transaction current status as affected by explicit operations */
    private int internalStatus = Status.STATUS_NO_TRANSACTION;
    /** the transaction information used to check for mismatched begin/end */
    private TransactionInfo internalTxnInfo;
    /** keep the thread that the transaction was started on to perform thread safety checks */
    private long threadId = Long.MIN_VALUE;
    /** make sure that we clean up the thread transaction stack properly */
    private boolean finalized = false;

    /**
     * Creates a user transaction that defaults to {@link TransactionDefinition#PROPAGATION_REQUIRED}.
     * 
     * @param transactionManager the transaction manager to use
     * @param readOnly true to force a read-only transaction
     * @param isolationLevel one of the
     *      {@link TransactionDefinition#ISOLATION_DEFAULT TransactionDefinition.ISOLATION_XXX}
     *      constants
     * @param propagationBehaviour one of the
     *      {@link TransactionDefinition#PROPAGATION_MANDATORY TransactionDefinition.PROPAGATION__XXX}
     *      constants
     * @param timeout the transaction timeout in seconds.
     * 
     * @see TransactionDefinition#getTimeout()
     */
    public SpringAwareUserTransaction(PlatformTransactionManager transactionManager, boolean readOnly,
            int isolationLevel, int propagationBehaviour, int timeout) {
        super();
        setTransactionManager(transactionManager);
        setTransactionAttributeSource(this);
        this.readOnly = readOnly;
        this.isolationLevel = isolationLevel;
        this.propagationBehaviour = propagationBehaviour;
        this.timeout = timeout;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(256);
        sb.append("UserTransaction").append("[object=").append(super.toString()).append(", status=")
                .append(internalStatus).append("]");
        return sb.toString();
    }

    /**
     * This class carries all the information required to fullfil requests about the transaction
     * attributes.  It acts as a source of the transaction attributes.
     * 
     * @return Return <code>this</code> instance
     */
    public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) {
        return this;
    }

    /**
     * Return a qualifier value associated with this transaction attribute. This is not used as the transaction manager
     * has been selected for us.
     * 
     * @return null always
     */
    public String getQualifier() {
        return null;
    }

    /**
     * The {@link UserTransaction } must rollback regardless of the error.  The
     * {@link #rollback() rollback} behaviour is implemented by simulating a caught
     * exception.  As this method will always return <code>true</code>, the rollback
     * behaviour will be to rollback the transaction or mark it for rollback.
     * 
     * @return Returns true always
     */
    public boolean rollbackOn(Throwable ex) {
        return true;
    }

    /**
     * @see #NAME
     */
    public String getName() {
        return NAME;
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public int getIsolationLevel() {
        return isolationLevel;
    }

    public int getPropagationBehavior() {
        return propagationBehaviour;
    }

    public int getTimeout() {
        return timeout;
    }

    /**
     * Implementation required for {@link UserTransaction}.
     */
    public void setTransactionTimeout(int timeout) throws SystemException {
        if (internalStatus != Status.STATUS_NO_TRANSACTION) {
            throw new RuntimeException("Can only set the timeout before begin");
        }
        this.timeout = timeout;
    }

    /**
     * Gets the current transaction info, or null if none exists.
     * <p>
     * A check is done to ensure that the transaction info on the stack is exactly
     * the same instance used when this transaction was started.
     * The internal status is also checked against the transaction info.
     * These checks ensure that the transaction demarcation is done correctly and that
     * thread safety is adhered to.
     * 
     * @return Returns the current transaction
     */
    private TransactionInfo getTransactionInfo() {
        // a few quick self-checks
        if (threadId < 0 && internalStatus != Status.STATUS_NO_TRANSACTION) {
            throw new RuntimeException("Transaction has been started but there is no thread ID");
        } else if (threadId >= 0 && internalStatus == Status.STATUS_NO_TRANSACTION) {
            throw new RuntimeException("Transaction has not been started but a thread ID has been recorded");
        }

        TransactionInfo txnInfo = null;
        try {
            txnInfo = TransactionAspectSupport.currentTransactionInfo();
            // we are in a transaction
        } catch (NoTransactionException e) {
            // No transaction.  It is possible that the transaction threw an exception during commit.
        }
        // perform checks for active transactions
        if (internalStatus == Status.STATUS_ACTIVE) {
            if (Thread.currentThread().getId() != threadId) {
                // the internally stored transaction info (retrieved in begin()) should match the info
                // on the thread
                throw new RuntimeException("UserTransaction may not be accessed by multiple threads");
            } else if (txnInfo == null) {
                // internally we recorded a transaction starting, but there is nothing on the thread
                throw new RuntimeException("Transaction boundaries have been made to overlap in the stack");
            } else if (txnInfo != internalTxnInfo) {
                // the transaction info on the stack isn't the one we started with
                throw new RuntimeException("UserTransaction begin/commit mismatch");
            }
        }
        return txnInfo;
    }

    /**
     * This status is a combination of the internal status, as recorded during explicit operations,
     * and the status provided by the Spring support.
     * 
     * @see Status
     */
    public synchronized int getStatus() throws SystemException {
        TransactionInfo txnInfo = getTransactionInfo();

        // if the txn info is null, then we are outside a transaction
        if (txnInfo == null) {
            return internalStatus; // this is checked in getTransactionInfo
        }

        // normally the internal status is correct, but we only need to double check
        // for the case where the transaction was marked for rollback, or rolledback
        // in a deeper transaction
        TransactionStatus txnStatus = txnInfo.getTransactionStatus();
        if (internalStatus == Status.STATUS_ROLLEDBACK) {
            // explicitly rolled back at some point
            return internalStatus;
        } else if (txnStatus.isRollbackOnly()) {
            // marked for rollback at some point in the stack
            return Status.STATUS_MARKED_ROLLBACK;
        } else {
            // just rely on the internal status
            return internalStatus;
        }
    }

    public synchronized void setRollbackOnly() throws IllegalStateException, SystemException {
        // just a check
        TransactionInfo txnInfo = getTransactionInfo();

        int status = getStatus();
        // check the status
        if (status == Status.STATUS_MARKED_ROLLBACK) {
            // this is acceptable
        } else if (status == Status.STATUS_NO_TRANSACTION) {
            throw new IllegalStateException("The transaction has not been started yet");
        } else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) {
            throw new IllegalStateException("The transaction has already been rolled back");
        } else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) {
            throw new IllegalStateException("The transaction has already been committed");
        } else if (status != Status.STATUS_ACTIVE) {
            throw new IllegalStateException("The transaction is not active: " + status);
        }

        // mark for rollback
        txnInfo.getTransactionStatus().setRollbackOnly();
        // make sure that we record the fact that we have been marked for rollback
        internalStatus = Status.STATUS_MARKED_ROLLBACK;
        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Set transaction status to rollback only: " + this);
        }
    }

    /**
     * @throws NotSupportedException if an attempt is made to reuse this instance
     */
    public synchronized void begin() throws NotSupportedException, SystemException {
        // make sure that the status and info align - the result may or may not be null
        @SuppressWarnings("unused")
        TransactionInfo txnInfo = getTransactionInfo();
        if (internalStatus != Status.STATUS_NO_TRANSACTION) {
            throw new NotSupportedException("The UserTransaction may not be reused");
        }

        // check 

        if ((propagationBehaviour != TransactionDefinition.PROPAGATION_REQUIRES_NEW)) {
            if (!readOnly && TransactionSynchronizationManager.isSynchronizationActive()
                    && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                throw new IllegalStateException("Nested writable transaction in a read only transaction");
            }
        }

        // begin a transaction
        try {
            internalTxnInfo = createTransactionIfNecessary((Method) null, (Class<?>) null); // super class will just pass nulls back to us
        } catch (CannotCreateTransactionException e) {
            throw new ConnectionPoolException("The DB connection pool is depleted.", e);
        }

        internalStatus = Status.STATUS_ACTIVE;
        threadId = Thread.currentThread().getId();

        // Record that transaction details now that begin was successful
        isBeginMatched = false;
        if (isCallStackTraced) {
            // get the stack trace
            Exception e = new Exception();
            e.fillInStackTrace();
            beginCallStack = e.getStackTrace();
        }

        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Began user transaction: " + this);
        }
    }

    /**
     * @throws IllegalStateException if a transaction was not started
     */
    public synchronized void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
            SecurityException, IllegalStateException, SystemException {
        // perform checks
        TransactionInfo txnInfo = getTransactionInfo();

        int status = getStatus();
        // check the status
        if (status == Status.STATUS_NO_TRANSACTION) {
            throw new IllegalStateException("The transaction has not yet begun");
        } else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) {
            throw new RollbackException("The transaction has already been rolled back");
        } else if (status == Status.STATUS_MARKED_ROLLBACK) {
            throw new RollbackException("The transaction has already been marked for rollback");
        } else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) {
            throw new IllegalStateException("The transaction has already been committed");
        } else if (status != Status.STATUS_ACTIVE || txnInfo == null) {
            throw new IllegalStateException("No user transaction is active");
        }

        if (!finalized) {
            try {
                // the status seems correct - we can try a commit
                commitTransactionAfterReturning(txnInfo);
            } catch (Throwable e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Transaction didn't commit", e);
                }
                // commit failed
                internalStatus = Status.STATUS_ROLLEDBACK;
                RollbackException re = new RollbackException("Transaction didn't commit: " + e.getMessage());
                // Stick the originating reason for failure into the exception.
                re.initCause(e);
                throw re;
            } finally {
                // make sure that we clean up the stack
                cleanupTransactionInfo(txnInfo);
                finalized = true;
                // clean up leaked transaction logging
                isBeginMatched = true;
                beginCallStack = null;
            }
        }

        // regardless of whether the transaction was finally committed or not, the status
        // as far as UserTransaction is concerned should be 'committed'

        // keep track that this UserTransaction was explicitly committed
        internalStatus = Status.STATUS_COMMITTED;

        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Committed user transaction: " + this);
        }
    }

    public synchronized void rollback() throws IllegalStateException, SecurityException, SystemException {
        // perform checks
        TransactionInfo txnInfo = getTransactionInfo();

        int status = getStatus();
        // check the status
        if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) {
            throw new IllegalStateException("The transaction has already been rolled back");
        } else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) {
            throw new IllegalStateException("The transaction has already been committed");
        } else if (txnInfo == null) {
            throw new IllegalStateException("No user transaction is active");
        }

        if (!finalized) {
            try {
                // force a rollback by generating an exception that will trigger a rollback
                completeTransactionAfterThrowing(txnInfo, new Exception());
            } finally {
                // make sure that we clean up the stack
                cleanupTransactionInfo(txnInfo);
                finalized = true;
                // clean up leaked transaction logging
                isBeginMatched = true;
                beginCallStack = null;
            }
        }

        // the internal status notes that we were specifically rolled back 
        internalStatus = Status.STATUS_ROLLEDBACK;

        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Rolled back user transaction: " + this);
        }
    }

    @Override
    protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Exception attempting to pass transaction boundaries.", ex);
        }
        super.completeTransactionAfterThrowing(txInfo, ex);
    }

    @Override
    protected String methodIdentification(Method method) {
        // note: override for debugging purposes - this method called by Spring
        return NAME;
    }

    @Override
    protected void finalize() throws Throwable {
        if (!isBeginMatched) {
            if (isCallStackTraced) {
                if (beginCallStack == null) {
                    traceLogger.error("UserTransaction being garbage collected without a commit() or rollback(). "
                            + "NOTE: Prior to transaction call stack logging.");
                } else {
                    StringBuilder sb = new StringBuilder(1024);
                    StackTraceUtil.buildStackTrace(
                            "UserTransaction being garbage collected without a commit() or rollback().",
                            beginCallStack, sb, -1);
                    traceLogger.error(sb);
                }
            } else {
                traceLogger.error(
                        "Detected first UserTransaction which is being garbage collected without a commit() or rollback()");
                traceLogger.error("Logging of transaction call stack is now enabled and will affect performance");
                isCallStackTraced = true;
            }
        }
    }
}