/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.ambari.server.orm; import java.lang.reflect.Method; import java.sql.SQLException; import java.util.Iterator; import java.util.LinkedList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.PersistenceException; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.ambari.annotations.TransactionalLock; import org.apache.ambari.annotations.TransactionalLock.LockArea; import org.apache.ambari.annotations.TransactionalLock.LockType; import org.eclipse.persistence.exceptions.EclipseLinkException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.persist.Transactional; import com.google.inject.persist.UnitOfWork; import com.google.inject.persist.jpa.AmbariJpaPersistService; /** * The {@link AmbariJpaLocalTxnInterceptor} is used to intercept method calls * annotated with the {@link Transactional} annotation. If a transaction is not * already in progress, then a new transaction is automatically started. * Otherwise, the currently active transaction will be reused. * <p/> * This interceptor also works with {@link TransactionalLock}s to lock on * {@link LockArea}s. If this interceptor encounters a {@link TransactionalLock} * it will acquire the lock and then add the {@link LockArea} to a collection of * areas which need to be released when the transaction is committed or rolled * back. This ensures that transactional methods invoke from an already running * transaction can have their lock invoked for the lifespan of the outer * "parent" transaction. */ public class AmbariJpaLocalTxnInterceptor implements MethodInterceptor { private static final Logger LOG = LoggerFactory.getLogger(AmbariJpaLocalTxnInterceptor.class); /** * A list of all of the {@link TransactionalLock}s that this interceptor is * responsible for. As a thread moves through the system encountering * {@link Transactional} and {@link TransactionalLock} methods, this will keep * track of which locks the outer-most interceptor will need to release. */ private static final ThreadLocal<LinkedList<TransactionalLock>> s_transactionalLocks = new ThreadLocal<LinkedList<TransactionalLock>>() { /** * {@inheritDoc} */ @Override protected LinkedList<TransactionalLock> initialValue() { return new LinkedList<>(); } }; /** * Used to ensure that methods which rely on the completion of * {@link Transactional} can detect when they are able to run. * * @see TransactionalLock */ @Inject private final TransactionalLocks transactionLocks = null; @Inject private final AmbariJpaPersistService emProvider = null; @Inject private final UnitOfWork unitOfWork = null; // Tracks if the unit of work was begun implicitly by this transaction. private final ThreadLocal<Boolean> didWeStartWork = new ThreadLocal<>(); /** * {@inheritDoc} */ @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { // Should we start a unit of work? if (!emProvider.isWorking()) { emProvider.begin(); didWeStartWork.set(true); } Transactional transactional = readTransactionMetadata(methodInvocation); EntityManager em = emProvider.get(); // lock the transaction if needed lockTransaction(methodInvocation); // Allow 'joining' of transactions if there is an enclosing @Transactional method. if (em.getTransaction().isActive()) { return methodInvocation.proceed(); } try { // this is the outer-most transactional, begin a transaction final EntityTransaction txn = em.getTransaction(); txn.begin(); Object result; try { result = methodInvocation.proceed(); } catch (Exception e) { // commit transaction only if rollback didn't occur if (rollbackIfNecessary(transactional, e, txn)) { txn.commit(); } detailedLogForPersistenceError(e); // propagate whatever exception is thrown anyway throw e; } finally { // Close the em if necessary (guarded so this code doesn't run unless // catch fired). if (null != didWeStartWork.get() && !txn.isActive()) { didWeStartWork.remove(); unitOfWork.end(); } } // everything was normal so commit the txn (do not move into try block // above as it // interferes with the advised method's throwing semantics) try { txn.commit(); } catch (Exception e) { detailedLogForPersistenceError(e); throw e; } finally { // close the em if necessary if (null != didWeStartWork.get()) { didWeStartWork.remove(); unitOfWork.end(); } } // or return result return result; } finally { // unlock all lock areas for this transaction unlockTransaction(); } } private void detailedLogForPersistenceError(Exception e) { if (e instanceof PersistenceException) { PersistenceException rbe = (PersistenceException) e; Throwable cause = rbe.getCause(); if (cause != null && cause instanceof EclipseLinkException) { EclipseLinkException de = (EclipseLinkException) cause; LOG.error("[DETAILED ERROR] Rollback reason: ", cause); Throwable internal = de.getInternalException(); int exIndent = 1; if (internal != null && internal instanceof SQLException) { SQLException exception = (SQLException) internal; while (exception != null) { LOG.error("[DETAILED ERROR] Internal exception (" + exIndent + ") : ", exception); // Log the exception exception = exception.getNextException(); exIndent++; } } } } } // TODO Cache this method's results. private Transactional readTransactionMetadata(MethodInvocation methodInvocation) { Transactional transactional; Method method = methodInvocation.getMethod(); Class<?> targetClass = methodInvocation.getThis().getClass(); transactional = method.getAnnotation(Transactional.class); if (null == transactional) { // If none on method, try the class. transactional = targetClass.getAnnotation(Transactional.class); } if (null == transactional) { // If there is no transactional annotation present, use the default transactional = Internal.class.getAnnotation(Transactional.class); } return transactional; } /** * Returns True if rollback DID NOT HAPPEN (i.e. if commit should continue). * * @param transactional The metadata annotation of the method * @param e The exception to test for rollback * @param txn A JPA Transaction to issue rollbacks on */ static boolean rollbackIfNecessary(Transactional transactional, Exception e, EntityTransaction txn) { if (txn.getRollbackOnly()) { txn.rollback(); return false; } boolean commit = true; //check rollback clauses for (Class<? extends Exception> rollBackOn : transactional.rollbackOn()) { //if one matched, try to perform a rollback if (rollBackOn.isInstance(e)) { commit = false; //check ignore clauses (supercedes rollback clause) for (Class<? extends Exception> exceptOn : transactional.ignore()) { //An exception to the rollback clause was found, DON'T rollback // (i.e. commit and throw anyway) if (exceptOn.isInstance(e)) { commit = true; break; } } //rollback only if nothing matched the ignore check if (!commit) { txn.rollback(); } //otherwise continue to commit break; } } return commit; } /** * Locks the {@link LockArea} specified on the {@link TransactionalLock} * annotation if it exists. If the annotation does not exist, then no work is * done. * <p/> * If a lock is acquired, then {@link #s_transactionalLocks} is updated with * the lock so that the outer-most interceptor can release all locks when the * transaction has completed. * * @param methodInvocation */ private void lockTransaction(MethodInvocation methodInvocation) { TransactionalLock annotation = methodInvocation.getMethod().getAnnotation( TransactionalLock.class); // no work to do if the annotation is not present if (null == annotation) { return; } // no need to lock again if (s_transactionalLocks.get().contains(annotation)) { return; } // there is a lock area, so acquire the lock LockArea lockArea = annotation.lockArea(); LockType lockType = annotation.lockType(); ReadWriteLock rwLock = transactionLocks.getLock(lockArea); Lock lock = lockType == LockType.READ ? rwLock.readLock() : rwLock.writeLock(); lock.lock(); // ensure that we add this lock area, otherwise it will never be released // when the outer most transaction is committed s_transactionalLocks.get().add(annotation); } /** * Unlocks all {@link LockArea}s associated with this transaction or any of * the child transactions which were joined. The order that the locks are * released is inverted from the order in which they were acquired. */ private void unlockTransaction(){ LinkedList<TransactionalLock> annotations = s_transactionalLocks.get(); if (annotations.isEmpty()) { return; } // iterate through all locks which were encountered during the course of // this transaction and release them all now that the transaction is // committed; iterate reverse to unlock the most recently locked areas Iterator<TransactionalLock> iterator = annotations.descendingIterator(); while (iterator.hasNext()) { TransactionalLock annotation = iterator.next(); LockArea lockArea = annotation.lockArea(); LockType lockType = annotation.lockType(); ReadWriteLock rwLock = transactionLocks.getLock(lockArea); Lock lock = lockType == LockType.READ ? rwLock.readLock() : rwLock.writeLock(); lock.unlock(); iterator.remove(); } } @Transactional private static class Internal { } }