/* * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Florent Guillaume */ package org.nuxeo.runtime.transaction; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Supplier; import javax.naming.NamingException; 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.TransactionSynchronizationRegistry; import javax.transaction.UserTransaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.runtime.jtajca.NuxeoContainer; /** * Utilities to work with transactions. */ public class TransactionHelper { private static final Log log = LogFactory.getLog(TransactionHelper.class); private static final Field GERONIMO_TRANSACTION_TIMEOUT_FIELD; static { try { GERONIMO_TRANSACTION_TIMEOUT_FIELD = org.apache.geronimo.transaction.manager.TransactionImpl.class.getDeclaredField( "timeout"); GERONIMO_TRANSACTION_TIMEOUT_FIELD.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { throw new ExceptionInInitializerError(e); } } private TransactionHelper() { // utility class } /** * Looks up the User Transaction in JNDI. * * @return the User Transaction * @throws NamingException if not found */ public static UserTransaction lookupUserTransaction() throws NamingException { UserTransaction ut = NuxeoContainer.getUserTransaction(); if (ut == null) { throw new NamingException("tx manager not installed"); } return ut; } /** * Gets the transaction status. * * @return the transaction {@linkplain Status status}, or -1 if there is no transaction manager * @since 8.4 * @see Status */ public static int getTransactionStatus() { UserTransaction ut = NuxeoContainer.getUserTransaction(); if (ut == null) { return -1; } try { return ut.getStatus(); } catch (SystemException e) { throw new TransactionRuntimeException("Cannot get transaction status", e); } } /** * Returns the UserTransaction JNDI binding name. * <p> * Assumes {@link #lookupUserTransaction} has been called once before. */ public static String getUserTransactionJNDIName() { return NuxeoContainer.nameOf("UserTransaction"); } /** * Looks up the TransactionManager in JNDI. * * @return the TransactionManager * @throws NamingException if not found */ public static TransactionManager lookupTransactionManager() throws NamingException { TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { throw new NamingException("tx manager not installed"); } return tm; } /** * Looks up the TransactionSynchronizationRegistry in JNDI. * * @return the TransactionSynchronizationRegistry * @throws NamingException if not found */ public static TransactionSynchronizationRegistry lookupSynchronizationRegistry() throws NamingException { TransactionSynchronizationRegistry synch = NuxeoContainer.getTransactionSynchronizationRegistry(); if (synch == null) { throw new NamingException("tx manager not installed"); } return synch; } /** * Checks if there is no transaction * * @6.0 */ public static boolean isNoTransaction() { int status = getTransactionStatus(); return status == Status.STATUS_NO_TRANSACTION || status == -1; } /** * Checks if the current User Transaction is active. */ public static boolean isTransactionActive() { int status = getTransactionStatus(); return status == Status.STATUS_ACTIVE; } /** * Checks if the current User Transaction is marked rollback only. */ public static boolean isTransactionMarkedRollback() { int status = getTransactionStatus(); return status == Status.STATUS_MARKED_ROLLBACK; } /** * Checks if the current User Transaction is active or marked rollback only. */ public static boolean isTransactionActiveOrMarkedRollback() { int status = getTransactionStatus(); return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK; } /** * Checks if the current User Transaction is active or preparing. * * @since 8.4 */ public static boolean isTransactionActiveOrPreparing() { int status = getTransactionStatus(); return status == Status.STATUS_ACTIVE || status == Status.STATUS_PREPARING; } /** * Checks if the current User Transaction has already timed out, i.e., whether a commit would immediately abort with * a timeout exception. * * @return {@code true} if there is a current transaction that has timed out, {@code false} otherwise * @since 7.1 */ public static boolean isTransactionTimedOut() { TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { return false; } try { Transaction tx = tm.getTransaction(); if (tx == null || tx.getStatus() != Status.STATUS_ACTIVE) { return false; } if (tx instanceof org.apache.geronimo.transaction.manager.TransactionImpl) { // Geronimo Transaction Manager Long timeout = (Long) GERONIMO_TRANSACTION_TIMEOUT_FIELD.get(tx); return System.currentTimeMillis() > timeout.longValue(); } else { // unknown transaction manager return false; } } catch (SystemException | ReflectiveOperationException e) { throw new RuntimeException(e); } } /** * Checks if the current User Transaction has already timed out, i.e., whether a commit would immediately abort with * a timeout exception. * <p> * Throws if the transaction has timed out. * * @throws TransactionRuntimeException if the transaction has timed out * @since 8.4 */ public static void checkTransactionTimeout() throws TransactionRuntimeException { if (isTransactionTimedOut()) { throw new TransactionRuntimeException("Transaction has timed out"); } } /** * Starts a new User Transaction. * * @return {@code true} if the transaction was successfully started, {@code false} otherwise */ public static boolean startTransaction() { UserTransaction ut = NuxeoContainer.getUserTransaction(); if (ut == null) { return false; } try { if (log.isDebugEnabled()) { log.debug("Starting transaction"); } ut.begin(); return true; } catch (NotSupportedException | SystemException e) { log.error("Unable to start transaction", e); } return false; } /** * Suspend the current transaction if active and start a new transaction * * @return the suspended transaction or null * @throws TransactionRuntimeException * @since 5.6 */ public static Transaction requireNewTransaction() { TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { return null; } try { Transaction tx = tm.getTransaction(); if (tx != null) { tx = tm.suspend(); } tm.begin(); return tx; } catch (NotSupportedException | SystemException e) { throw new TransactionRuntimeException("Cannot suspend tx", e); } } public static Transaction suspendTransaction() { TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { return null; } try { Transaction tx = tm.getTransaction(); if (tx != null) { tx = tm.suspend(); } return tx; } catch (SystemException e) { throw new TransactionRuntimeException("Cannot suspend tx", e); } } /** * Commit the current transaction if active and resume the principal transaction * * @param tx */ public static void resumeTransaction(Transaction tx) { TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { return; } try { if (tm.getStatus() == Status.STATUS_ACTIVE) { tm.commit(); } if (tx != null) { tm.resume(tx); } } catch (SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException | InvalidTransactionException | IllegalStateException | SecurityException e) { throw new TransactionRuntimeException("Cannot resume tx", e); } } /** * Starts a new User Transaction with the specified timeout. * * @param timeout the timeout in seconds, <= 0 for the default * @return {@code true} if the transaction was successfully started, {@code false} otherwise * @since 5.6 */ public static boolean startTransaction(int timeout) { if (timeout < 0) { timeout = 0; } TransactionManager tm = NuxeoContainer.getTransactionManager(); if (tm == null) { return false; } try { tm.setTransactionTimeout(timeout); } catch (SystemException e) { log.error("Unable to set transaction timeout: " + timeout, e); return false; } try { return startTransaction(); } finally { try { tm.setTransactionTimeout(0); } catch (SystemException e) { log.error("Unable to reset transaction timeout", e); } } } /** * Commits or rolls back the User Transaction depending on the transaction status. */ public static void commitOrRollbackTransaction() { UserTransaction ut = NuxeoContainer.getUserTransaction(); if (ut == null) { return; } noteSuppressedExceptions(); RuntimeException thrown = null; boolean isRollbackDuringCommit = false; try { int status = ut.getStatus(); if (status == Status.STATUS_ACTIVE) { if (log.isDebugEnabled()) { log.debug("Committing transaction"); } try { ut.commit(); } catch (RollbackException | HeuristicRollbackException | HeuristicMixedException e) { String msg = "Unable to commit"; // messages from org.apache.geronimo.transaction.manager.TransactionImpl.commit switch (e.getMessage()) { case "Unable to commit: transaction marked for rollback": // don't log as error, this happens if there's a ConcurrentUpdateException // at transaction end inside VCS isRollbackDuringCommit = true; // $FALL-THROUGH$ case "Unable to commit: Transaction timeout": // don't log either log.debug(msg, e); break; default: log.error(msg, e); } throw new TransactionRuntimeException(e.getMessage(), e); } } else if (status == Status.STATUS_MARKED_ROLLBACK) { if (log.isDebugEnabled()) { log.debug("Cannot commit transaction because it is marked rollback only"); } ut.rollback(); } else { if (log.isDebugEnabled()) { log.debug("Cannot commit transaction with unknown status: " + status); } } } catch (SystemException e) { thrown = new TransactionRuntimeException(e); throw thrown; } catch (RuntimeException e) { thrown = e; throw thrown; } finally { List<Exception> suppressed = getSuppressedExceptions(); if (!suppressed.isEmpty()) { // add suppressed to thrown exception, or throw a new one RuntimeException e; if (thrown == null) { e = new TransactionRuntimeException("Exception during commit"); } else { if (isRollbackDuringCommit && suppressed.get(0) instanceof RuntimeException) { // use the suppressed one directly and throw it instead thrown = null; // force rethrow below e = (RuntimeException) suppressed.remove(0); } else { e = thrown; } } suppressed.forEach(s -> e.addSuppressed(s)); if (thrown == null) { throw e; } } } } private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>(); /** * After this, some exceptions during transaction commit may be suppressed and remembered. * * @since 5.9.4 */ protected static void noteSuppressedExceptions() { suppressedExceptions.set(new ArrayList<>()); } /** * Remembers the exception if it happens during the processing of a commit, so that it can be surfaced as a * suppressed exception at the end of the commit. * * @since 5.9.4 */ public static void noteSuppressedException(Exception e) { List<Exception> exceptions = suppressedExceptions.get(); if (exceptions != null) { exceptions.add(e); } } /** * Gets the suppressed exceptions, and stops remembering. * * @since 5.9.4 */ protected static List<Exception> getSuppressedExceptions() { List<Exception> exceptions = suppressedExceptions.get(); suppressedExceptions.remove(); return exceptions == null ? Collections.<Exception> emptyList() : exceptions; } /** * Sets the current User Transaction as rollback only. * * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise */ public static boolean setTransactionRollbackOnly() { if (log.isDebugEnabled()) { log.debug("Setting transaction as rollback only"); if (log.isTraceEnabled()) { log.trace("Rollback stack trace", new Throwable("Rollback stack trace")); } } UserTransaction ut = NuxeoContainer.getUserTransaction(); if (ut == null) { return false; } try { ut.setRollbackOnly(); return true; } catch (IllegalStateException | SystemException cause) { log.error("Could not mark transaction as rollback only", cause); } return false; } /** * Sets the current User Transaction as rollback only if it has timed out. * * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise * @since 7.1 */ public static boolean setTransactionRollbackOnlyIfTimedOut() { if (isTransactionTimedOut()) { return setTransactionRollbackOnly(); } return false; } public static void registerSynchronization(Synchronization handler) { if (!isTransactionActiveOrPreparing()) { throw new TransactionRuntimeException("Cannot register Synchronization if transaction is not active"); } try { NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler); } catch (IllegalStateException | RollbackException | SystemException cause) { throw new RuntimeException("Cannot register synch handler in current tx", cause); } } /** * Runs the given {@link Runnable} without a transactional context. Will suspend and restore the transaction if one already * exists. * * @param runnable the {@link Runnable} * @since 9.1 */ public static void runWithoutTransaction(Runnable runnable) { runWithoutTransaction(() -> { runnable.run(); return null; }); } /** * Calls the given {@link Supplier} without a transactional context. Will suspend and restore the transaction if one already * exists. * * @param supplier the {@link Supplier} * @since 9.1 */ public static <R> R runWithoutTransaction(Supplier<R> supplier) { Transaction tx = suspendTransaction(); try { return supplier.get(); } finally { resumeTransaction(tx); } } /** * Runs the given {@link Runnable} in a new transactional context. Will suspend and restore the transaction if one already * exists. * * @param runnable the {@link Runnable} * @since 9.1 */ public static void runInNewTransaction(Runnable runnable) { runInNewTransaction(() -> { runnable.run(); return null;}); } /** * Calls the given {@link Supplier} in a new transactional context. Will suspend and restore the transaction if one already * exists. * * @param supplier the {@link Supplier} * @since 9.1 */ public static <R> R runInNewTransaction(Supplier<R> supplier) { Transaction tx = suspendTransaction(); try { return runInTransaction(supplier); } finally { resumeTransaction(tx); } } /** * Runs the given {@link Runnable} in a transactional context. Will not start a new transaction if one already * exists. * * @param runnable the {@link Runnable} * @since 8.4 */ public static void runInTransaction(Runnable runnable) { runInTransaction(() -> {runnable.run(); return null;}); } /** * Calls the given {@link Supplier} in a transactional context. Will not start a new transaction if one already * exists. * * @param supplier the {@link Supplier} * @return the supplier's result * @since 8.4 */ public static <R> R runInTransaction(Supplier<R> supplier) { boolean startTransaction = !isTransactionActiveOrMarkedRollback(); if (startTransaction) { if (!startTransaction()) { throw new TransactionRuntimeException("Cannot start transaction"); } } boolean completedAbruptly = true; try { R result = supplier.get(); completedAbruptly = false; return result; } finally { try { if (completedAbruptly) { setTransactionRollbackOnly(); } } finally { if (startTransaction) { commitOrRollbackTransaction(); } } } } }