package org.ovirt.engine.core.utils.transaction; import javax.ejb.TransactionRolledbackLocalException; import javax.enterprise.inject.spi.CDI; import javax.transaction.HeuristicMixedException; import javax.transaction.HeuristicRollbackException; 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 org.ovirt.engine.core.compat.TransactionScopeOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TransactionSupport { private static final Logger log = LoggerFactory.getLogger(TransactionSupport.class); /** * JBoss specific location of TransactionManager */ private static TransactionManager findTransactionManager() { return CDI.current().select(TransactionManager.class).get(); } /** * Suspends and returns current transaction */ public static Transaction suspend() { try { TransactionManager tm = findTransactionManager(); return tm.suspend(); } catch (Exception e) { throw new RuntimeException("Unable to suspend transaction", e); } } /** * Resumes given transaction */ public static void resume(Transaction transaction) { try { TransactionManager tm = findTransactionManager(); tm.resume(transaction); } catch (Exception e) { throw new RuntimeException("Unable to resume transaction", e); } } /** * Returns current transaction */ public static Transaction current() { try { TransactionManager tm = findTransactionManager(); return tm.getTransaction(); } catch (Exception e) { throw new RuntimeException("Unable to get handle to current transaction", e); } } /** * Attaches rollback handler to current transaction */ public static void registerRollbackHandler(final TransactionCompletionListener transactionCompletionListener) { try { current().registerSynchronization(new RollbackHandlerSynchronization(transactionCompletionListener)); } catch (Exception e) { throw new RuntimeException("Unable to register synchronization to current transaction", e); } } private static boolean needToRollback(int status) { return status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_ROLLEDBACK || status == Status.STATUS_ROLLING_BACK; } /** * Runs given code in a given transaction scope */ public static <T> T executeInScope(TransactionScopeOption scope, TransactionMethod<T> code) { // check if we are already in rollback TransactionManager tm; try { tm = findTransactionManager(); if (needToRollback(tm.getStatus())) { throw new TransactionRolledbackLocalException( "Current transaction is marked for rollback, no further operations are possible or desired"); } } catch (SystemException e) { throw new RuntimeException("Failed to check transaction status - this shouldn't ever happen"); } switch (scope) { case RequiresNew: return executeInNewTransaction(code); case Suppress: return executeInSuppressed(tm, code); case Required: return executeInRequired(tm, code); default: throw new RuntimeException("Undefined Scope: " + scope); } } private static boolean statusOneOf(int status, int... options) { for (int option : options) { if (status == option) { return true; } } return false; } /** * Simply executes given code in current scope It won't actually open transaction since we are assuming we always * start with transaction from JBoss That assumption should always hold - if not we need to look at specific case * where it fails */ private static <T> T executeInRequired(TransactionManager tm, TransactionMethod<T> code) { try { // verify we are not in a bad state int status = tm.getStatus(); if (statusOneOf(status, Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN)) { throw new RuntimeException( "Transaction is required to proceed but current transaction status is wrong: " + status); } if (status == Status.STATUS_NO_TRANSACTION) { return executeInNewTransaction(code); } else { return code.runInTransaction(); } } catch (RuntimeException rte) { throw rte; } catch (Exception e) { log.error("executeInRequired - Wrapping Exception: {} with RunTimeException", e.getClass().getName()); throw new RuntimeException("Failed running code", e); } } /** * Forces "SUPPRESS" and executes the code in that scope */ private static <T> T executeInSuppressed(TransactionManager tm, TransactionMethod<T> code) { T result = null; try { Transaction transaction = tm.getTransaction(); if (transaction != null) { transaction = tm.suspend(); } try { result = code.runInTransaction(); } finally { if (transaction != null) { tm.resume(transaction); } } } catch (RuntimeException rte) { throw rte; } catch (Exception e) { log.error("executeInSuppressed - Wrapping Exception: {} with RunTimeException", e.getClass().getName()); throw new RuntimeException("Failed executing code", e); } return result; } /** * Forces "REQUIRES_NEW" and executes given code in that scope */ public static <T> T executeInNewTransaction(TransactionMethod<T> code) { T result = null; Transaction transaction = null; try { TransactionManager tm = findTransactionManager(); // suspend existing if exists transaction = tm.getTransaction(); if (transaction != null) { transaction = tm.suspend(); } // start new transaction tm.begin(); Transaction newTransaction = tm.getTransaction(); // run the code try { result = code.runInTransaction(); } catch (RuntimeException rte) { tm.rollback(); log.info("transaction rolled back"); throw rte; } catch (Exception e) { // code failed need to rollback tm.rollback(); log.info("transaction rolled back"); log.error("executeInNewTransaction - Wrapping Exception: {} with RunTimeException", e.getClass().getName()); throw new RuntimeException("Failed executing code", e); } // commit or rollback according to state if (needToRollback(newTransaction.getStatus())) { tm.rollback(); } else { tm.commit(); } } catch (SystemException | NotSupportedException | HeuristicRollbackException | HeuristicMixedException | RollbackException | IllegalStateException | SecurityException e) { throw new RuntimeException("Failed managing transaction", e); } finally { // check if we need to resume previous transaction if (transaction != null) { resume(transaction); } } // and we are done... return result; } /** * Marks the transaction for forced rollback. The actual rollback will happen when the code reaches the transaction * boundary */ public static void setRollbackOnly() { try { TransactionManager tm = findTransactionManager(); Transaction transaction = tm.getTransaction(); if (transaction != null) { transaction.setRollbackOnly(); } } catch (SystemException e) { throw new RuntimeException("Failed to mark transaction for rollback", e); } } private static class RollbackHandlerSynchronization implements Synchronization { private final TransactionCompletionListener transactionCompletionListener; public RollbackHandlerSynchronization(TransactionCompletionListener transactionCompletionListener) { this.transactionCompletionListener = transactionCompletionListener; } @Override public void beforeCompletion() { } @Override public void afterCompletion(int status) { if (needToRollback(status)) { transactionCompletionListener.onRollback(); } else { transactionCompletionListener.onSuccess(); } } } }