/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.jpa.transaction;
import static java.security.AccessController.doPrivileged;
import static org.jboss.as.jpa.messages.JpaLogger.ROOT_LOGGER;
import java.security.PrivilegedAction;
import java.util.EnumSet;
import javax.persistence.EntityManager;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionSynchronizationRegistry;
import org.jboss.as.jpa.container.ExtendedEntityManager;
import org.jboss.as.jpa.messages.JpaLogger;
import org.jboss.tm.TxUtils;
import org.jboss.tm.listener.EventType;
import org.wildfly.transaction.client.AbstractTransaction;
import org.wildfly.transaction.client.AssociationListener;
import org.wildfly.transaction.client.ContextTransactionManager;
/**
* Transaction utilities for JPA
*
* @author Scott Marlow (forked from code by Gavin King)
*/
public class TransactionUtil {
private static final EnumSet<EventType> eventTypes = EnumSet.of(EventType.ASSOCIATED, EventType.DISASSOCIATING);
public static boolean isInTx(TransactionManager transactionManager) {
Transaction tx = getTransaction(transactionManager);
if (tx == null || !TxUtils.isActive(tx))
return false;
return true;
}
/**
* Get current persistence context. Only call while a transaction is active in the current thread.
*
* @param puScopedName
* @return
*/
public static EntityManager getTransactionScopedEntityManager(String puScopedName, TransactionSynchronizationRegistry tsr) {
return getEntityManagerInTransactionRegistry(puScopedName, tsr);
}
public static void registerSynchronization(EntityManager entityManager, String puScopedName, TransactionSynchronizationRegistry tsr, TransactionManager transactionManager) {
SessionSynchronization sessionSynchronization = new SessionSynchronization(entityManager, puScopedName);
tsr.registerInterposedSynchronization(sessionSynchronization);
final AbstractTransaction transaction = ((ContextTransactionManager) transactionManager).getTransaction();
doPrivileged((PrivilegedAction<Void>) () -> {
transaction.registerAssociationListener(sessionSynchronization);
return null;
});
}
public static Transaction getTransaction(TransactionManager transactionManager) {
try {
return transactionManager.getTransaction();
} catch (SystemException e) {
throw JpaLogger.ROOT_LOGGER.errorGettingTransaction(e);
}
}
private static String currentThread() {
return Thread.currentThread().getName();
}
public static String getEntityManagerDetails(EntityManager manager, String scopedPuName) {
String result = currentThread() + ":"; // show the thread for correlation with other modules
if (manager instanceof ExtendedEntityManager) {
result += manager.toString();
}
else {
result += "transaction scoped EntityManager [" + scopedPuName + "]";
}
return result;
}
private static EntityManager getEntityManagerInTransactionRegistry(String scopedPuName, TransactionSynchronizationRegistry tsr) {
return (EntityManager)tsr.getResource(scopedPuName);
}
/**
* Save the specified EntityManager in the local threads active transaction. The TransactionSynchronizationRegistry
* will clear the reference to the EntityManager when the transaction completes.
*
* @param scopedPuName
* @param entityManager
*/
public static void putEntityManagerInTransactionRegistry(String scopedPuName, EntityManager entityManager, TransactionSynchronizationRegistry tsr) {
tsr.putResource(scopedPuName, entityManager);
}
/**
* The AssociationListener helps protect against a non-application thread closing the entity manager at the same
* time that the application thread may be using the entity manager. We only close the entity manager after the
* Synchronization.afterCompletion has been triggered and zero threads are associated with the transaction.
*
* We know when the application thread is associated with the transaction and can defer closing the EntityManager
* until both conditions are met:
*
* 1. application thread is disassociated from transaction
* 2. Synchronization.afterCompletion has been called
*
* Note that entity managers do not get propagated on remote EJB invocations.
*
* See discussions for more details about how we arrived at using the AssociationListener (TransactionListener):
* https://developer.jboss.org/message/919807
* https://developer.jboss.org/thread/252572
*/
private static class SessionSynchronization implements Synchronization, AssociationListener {
private EntityManager manager; // the underlying entity manager
private String scopedPuName;
private boolean afterCompletionCalled = false;
private int associationCounter = 1; // set to one since transaction is associated with current thread already.
// incremented when a thread is associated with transaction,
// decremented when a thread is disassociated from transaction.
// synchronization on this object protects associationCounter.
public SessionSynchronization(EntityManager session, String scopedPuName) {
this.manager = session;
this.scopedPuName = scopedPuName;
}
public void beforeCompletion() {
afterCompletionCalled = false;
}
public void afterCompletion(int status) {
/**
* Note: synchronization is to protect against two concurrent threads from closing the EntityManager (manager)
* at the same time.
*/
synchronized (this) {
afterCompletionCalled = true;
safeCloseEntityManager();
}
}
/**
* After the JTA transaction is ended (Synchronization.afterCompletion has been called) and
* the JTA transaction is no longer associated with application thread (application thread called
* transaction.rollback/commit/suspend), the entity manager can safely be closed.
*
* NOTE: caller must call with synchronized(this), where this == instance of SessionSynchronization associated with
* the JTA transaction.
*/
private void safeCloseEntityManager() {
if ( afterCompletionCalled == true && associationCounter == 0) {
if (manager != null) {
try {
if (ROOT_LOGGER.isDebugEnabled())
ROOT_LOGGER.debugf("%s: closing entity managersession", getEntityManagerDetails(manager, scopedPuName));
manager.close();
} catch (Exception ignored) {
if (ROOT_LOGGER.isDebugEnabled())
ROOT_LOGGER.debugf(ignored, "ignoring error that occurred while closing EntityManager for %s (", scopedPuName);
}
manager = null;
}
}
}
public void associationChanged(final AbstractTransaction transaction, final boolean associated) {
synchronized (this) {
// associationCounter is set to zero when application thread is no longer associated with JTA transaction.
// We are tracking when the application thread
// is no longer associated with the transaction, as that indicates that it is safe to
// close the entity manager (since the application is no longer using the entity manager).
//
// Expected values for associationCounter:
// 1 - application thread is associated with transaction
// 0 - application thread is not associated with transaction (e.g. tm.suspend called)
//
// Expected values for TM Reaper thread timing out transaction
// 1 - application thread is associated with transaction
// 2 - TM reaper thread is associated with transaction (tx timeout handling)
// 1 - either TM reaper or application thread disassociated from transaction
// 0 - both TM reaper and application thread are disassociated from transaction
//
// the safeCloseEntityManager() may close the entity manager in the (background) reaper thread or
// application thread (whichever thread reaches associationCounter == 0).
associationCounter += associated ? 1 : -1;
if (ROOT_LOGGER.isTraceEnabled()) {
ROOT_LOGGER.tracef("transaction association counter = %d for %s: ", associationCounter, getEntityManagerDetails(manager, scopedPuName));
}
safeCloseEntityManager();
}
}
}
}