diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index 697d702d26..607e0a3a96 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -625,4 +625,15 @@ public interface AvailableSettings { public static final String USE_DIRECT_REFERENCE_CACHE_ENTRIES = "hibernate.cache.use_reference_entries"; public static final String USE_NATIONALIZED_CHARACTER_DATA = "hibernate.use_nationalized_character_data"; + + /** + * A transaction can be rolled back by another thread ("tracking by thread") + * -- not the original application. Examples of this include a JTA + * transaction timeout handled by a background reaper thread. The ability + * to handle this situation requires checking the Thread ID every time + * Session is called. This can certainly have performance considerations. + * + * Default is true (enabled). + */ + public static final String JTA_TRACK_BY_THREAD = "hibernate.jta.track_by_thread"; } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java b/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java index 916917ea6a..78a9f0889d 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java @@ -96,6 +96,8 @@ public final class Settings { private MultiTableBulkIdStrategy multiTableBulkIdStrategy; private BatchFetchStyle batchFetchStyle; private boolean directReferenceCacheEntriesEnabled; + + private boolean jtaTrackByThread; /** @@ -508,4 +510,12 @@ public final class Settings { void setDefaultNullPrecedence(NullPrecedence defaultNullPrecedence) { this.defaultNullPrecedence = defaultNullPrecedence; } + + public boolean isJtaTrackByThread() { + return jtaTrackByThread; + } + + public void setJtaTrackByThread(boolean jtaTrackByThread) { + this.jtaTrackByThread = jtaTrackByThread; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java b/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java index 555682b740..20eb4a3a5d 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java @@ -389,6 +389,16 @@ public class SettingsFactory implements Serializable { } settings.setInitializeLazyStateOutsideTransactions( initializeLazyStateOutsideTransactionsEnabled ); + boolean jtaTrackByThread = ConfigurationHelper.getBoolean( + AvailableSettings.JTA_TRACK_BY_THREAD, + properties, + true + ); + if ( debugEnabled ) { + LOG.debugf( "JTA Track by Thread: %s", enabledDisabled(jtaTrackByThread) ); + } + settings.setJtaTrackByThread( jtaTrackByThread ); + return settings; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/transaction/internal/TransactionCoordinatorImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/transaction/internal/TransactionCoordinatorImpl.java index 64049cf5df..c070c09263 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/transaction/internal/TransactionCoordinatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/transaction/internal/TransactionCoordinatorImpl.java @@ -262,6 +262,7 @@ public class TransactionCoordinatorImpl implements TransactionCoordinator { } public void pulse() { + getSynchronizationCallbackCoordinator().pulse(); if ( transactionFactory().compatibleWithJtaSynchronization() ) { // the configured transaction strategy says it supports callbacks via JTA synchronization, so attempt to // register JTA synchronization if possible diff --git a/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/internal/SynchronizationCallbackCoordinatorImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/internal/SynchronizationCallbackCoordinatorImpl.java index 2ff237498f..cd3e6bf663 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/internal/SynchronizationCallbackCoordinatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/internal/SynchronizationCallbackCoordinatorImpl.java @@ -25,9 +25,8 @@ package org.hibernate.engine.transaction.synchronization.internal; import javax.transaction.SystemException; -import org.jboss.logging.Logger; - import org.hibernate.TransactionException; +import org.hibernate.cfg.Settings; import org.hibernate.engine.transaction.internal.jta.JtaStatusHelper; import org.hibernate.engine.transaction.spi.TransactionContext; import org.hibernate.engine.transaction.spi.TransactionCoordinator; @@ -36,31 +35,43 @@ import org.hibernate.engine.transaction.synchronization.spi.ExceptionMapper; import org.hibernate.engine.transaction.synchronization.spi.ManagedFlushChecker; import org.hibernate.engine.transaction.synchronization.spi.SynchronizationCallbackCoordinator; import org.hibernate.internal.CoreMessageLogger; +import org.jboss.logging.Logger; /** * Manages callbacks from the {@link javax.transaction.Synchronization} registered by Hibernate. - * + * * @author Steve Ebersole + * @author Brett Meyer */ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCallbackCoordinator { - private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, SynchronizationCallbackCoordinatorImpl.class.getName() ); + private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, + SynchronizationCallbackCoordinatorImpl.class.getName() ); private final TransactionCoordinator transactionCoordinator; + private final Settings settings; private ManagedFlushChecker managedFlushChecker; private AfterCompletionAction afterCompletionAction; private ExceptionMapper exceptionMapper; + private long registrationThreadId; + private final int NO_STATUS = -1; + private int delayedCompletionHandlingStatus; + public SynchronizationCallbackCoordinatorImpl(TransactionCoordinator transactionCoordinator) { this.transactionCoordinator = transactionCoordinator; + this.settings = transactionCoordinator.getTransactionContext() + .getTransactionEnvironment().getSessionFactory().getSettings(); reset(); + pulse(); } public void reset() { managedFlushChecker = STANDARD_MANAGED_FLUSH_CHECKER; exceptionMapper = STANDARD_EXCEPTION_MAPPER; afterCompletionAction = STANDARD_AFTER_COMPLETION_ACTION; + delayedCompletionHandlingStatus = NO_STATUS; } @Override @@ -78,7 +89,6 @@ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCa this.afterCompletionAction = afterCompletionAction; } - // sync callbacks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public void beforeCompletion() { @@ -86,16 +96,14 @@ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCa boolean flush; try { - final int status = transactionCoordinator - .getTransactionContext() - .getTransactionEnvironment() - .getJtaPlatform() - .getCurrentStatus(); + final int status = transactionCoordinator.getTransactionContext().getTransactionEnvironment() + .getJtaPlatform().getCurrentStatus(); flush = managedFlushChecker.shouldDoManagedFlush( transactionCoordinator, status ); } catch ( SystemException se ) { setRollbackOnly(); - throw exceptionMapper.mapStatusCheckFailure( "could not determine transaction status in beforeCompletion()", se ); + throw exceptionMapper.mapStatusCheckFailure( + "could not determine transaction status in beforeCompletion()", se ); } try { @@ -119,6 +127,35 @@ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCa } public void afterCompletion(int status) { + if ( !settings.isJtaTrackByThread() || isRegistrationThread() ) { + doAfterCompletion( status ); + } + else if ( JtaStatusHelper.isRollback( status ) ) { + // The transaction was rolled back by another thread -- not the + // original application. Examples of this include a JTA transaction + // timeout getting cleaned up by a reaper thread. If this happens, + // afterCompletion must be handled by the original thread in order + // to prevent non-threadsafe session use. Set the flag here and + // check for it in SessionImpl. See HHH-7910. + LOG.warnv( "Transaction afterCompletion called by a background thread! Delaying action until the original thread can handle it. [status={0}]", status ); + delayedCompletionHandlingStatus = status; + } + } + + public void pulse() { + if ( settings.isJtaTrackByThread() ) { + registrationThreadId = Thread.currentThread().getId(); + } + } + + public void delayedAfterCompletion() { + if ( delayedCompletionHandlingStatus != NO_STATUS ) { + doAfterCompletion( delayedCompletionHandlingStatus ); + delayedCompletionHandlingStatus = NO_STATUS; + } + } + + private void doAfterCompletion(int status) { LOG.tracev( "Transaction after completion callback [status={0}]", status ); try { @@ -134,6 +171,10 @@ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCa } } + private boolean isRegistrationThread() { + return Thread.currentThread().getId() == registrationThreadId; + } + private TransactionContext transactionContext() { return transactionCoordinator.getTransactionContext(); } @@ -141,17 +182,18 @@ public class SynchronizationCallbackCoordinatorImpl implements SynchronizationCa private static final ManagedFlushChecker STANDARD_MANAGED_FLUSH_CHECKER = new ManagedFlushChecker() { @Override public boolean shouldDoManagedFlush(TransactionCoordinator coordinator, int jtaStatus) { - return ! coordinator.getTransactionContext().isClosed() && - ! coordinator.getTransactionContext().isFlushModeNever() && - coordinator.getTransactionContext().isFlushBeforeCompletionEnabled() && - ! JtaStatusHelper.isRollback( jtaStatus ); + return !coordinator.getTransactionContext().isClosed() + && !coordinator.getTransactionContext().isFlushModeNever() + && coordinator.getTransactionContext().isFlushBeforeCompletionEnabled() + && !JtaStatusHelper.isRollback( jtaStatus ); } }; private static final ExceptionMapper STANDARD_EXCEPTION_MAPPER = new ExceptionMapper() { public RuntimeException mapStatusCheckFailure(String message, SystemException systemException) { LOG.error( LOG.unableToDetermineTransactionStatus(), systemException ); - return new TransactionException( "could not determine transaction status in beforeCompletion()", systemException ); + return new TransactionException( "could not determine transaction status in beforeCompletion()", + systemException ); } public RuntimeException mapManagedFlushFailure(String message, RuntimeException failure) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/spi/SynchronizationCallbackCoordinator.java b/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/spi/SynchronizationCallbackCoordinator.java index d96a62b0f9..723aa9d95a 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/spi/SynchronizationCallbackCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/transaction/synchronization/spi/SynchronizationCallbackCoordinator.java @@ -31,5 +31,7 @@ import javax.transaction.Synchronization; public interface SynchronizationCallbackCoordinator extends Synchronization{ public void setManagedFlushChecker(ManagedFlushChecker managedFlushChecker); public void setAfterCompletionAction(AfterCompletionAction afterCompletionAction); + public void pulse(); + public void delayedAfterCompletion(); public void setExceptionMapper(ExceptionMapper exceptionMapper); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index f489979f45..5f30c6ade6 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -169,6 +169,7 @@ import org.jboss.logging.Logger; * * @author Gavin King * @author Steve Ebersole + * @author Brett Meyer */ public final class SessionImpl extends AbstractSessionImpl implements EventSource { @@ -320,7 +321,9 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc public void clear() { errorIfClosed(); - checkTransactionSynchStatus(); + // Do not call checkTransactionSynchStatus() here -- if a delayed + // afterCompletion exists, it can cause an infinite loop. + pulseTransactionCoordinator(); internalClear(); } @@ -706,6 +709,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc if ( persistenceContext.getCascadeLevel() == 0 ) { actionQueue.checkNoUnresolvedActionsAfterOperation(); } + delayedAfterCompletion(); + } + + private void delayedAfterCompletion() { + transactionCoordinator.getSynchronizationCallbackCoordinator().delayedAfterCompletion(); } // saveOrUpdate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -808,6 +816,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( LockEventListener listener : listeners( EventType.LOCK ) ) { listener.onLock( event ); } + delayedAfterCompletion(); } @@ -832,6 +841,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( PersistEventListener listener : listeners( EventType.PERSIST ) ) { listener.onPersist( event, copiedAlready ); } + delayedAfterCompletion(); } private void firePersist(PersistEvent event) { @@ -867,6 +877,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( PersistEventListener listener : listeners( EventType.PERSIST_ONFLUSH ) ) { listener.onPersist( event, copiedAlready ); } + delayedAfterCompletion(); } private void firePersistOnFlush(PersistEvent event) { @@ -911,6 +922,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( MergeEventListener listener : listeners( EventType.MERGE ) ) { listener.onMerge( event, copiedAlready ); } + delayedAfterCompletion(); } @@ -943,6 +955,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( DeleteEventListener listener : listeners( EventType.DELETE ) ) { listener.onDelete( event ); } + delayedAfterCompletion(); } private void fireDelete(DeleteEvent event, Set transientEntities) { @@ -951,6 +964,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( DeleteEventListener listener : listeners( EventType.DELETE ) ) { listener.onDelete( event, transientEntities ); } + delayedAfterCompletion(); } @@ -1076,6 +1090,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( LoadEventListener listener : listeners( EventType.LOAD ) ) { listener.onLoad( event, loadType ); } + delayedAfterCompletion(); } private void fireResolveNaturalId(ResolveNaturalIdEvent event) { @@ -1084,6 +1099,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( ResolveNaturalIdEventListener listener : listeners( EventType.RESOLVE_NATURAL_ID ) ) { listener.onResolveNaturalId( event ); } + delayedAfterCompletion(); } @@ -1120,6 +1136,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( RefreshEventListener listener : listeners( EventType.REFRESH ) ) { listener.onRefresh( event ); } + delayedAfterCompletion(); } private void fireRefresh(Map refreshedAlready, RefreshEvent event) { @@ -1128,6 +1145,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( RefreshEventListener listener : listeners( EventType.REFRESH ) ) { listener.onRefresh( event, refreshedAlready ); } + delayedAfterCompletion(); } @@ -1148,6 +1166,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( ReplicateEventListener listener : listeners( EventType.REPLICATE ) ) { listener.onReplicate( event ); } + delayedAfterCompletion(); } @@ -1167,6 +1186,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( EvictEventListener listener : listeners( EventType.EVICT ) ) { listener.onEvict( event ); } + delayedAfterCompletion(); } /** @@ -1198,6 +1218,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( DirtyCheckEventListener listener : listeners( EventType.DIRTY_CHECK ) ) { listener.onDirtyCheck( event ); } + delayedAfterCompletion(); return event.isDirty(); } @@ -1211,6 +1232,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( FlushEventListener listener : listeners( EventType.FLUSH ) ) { listener.onFlush( flushEvent ); } + delayedAfterCompletion(); } public void forceFlush(EntityEntry entityEntry) throws HibernateException { @@ -1249,6 +1271,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc finally { dontFlushFromFind--; afterOperation(success); + delayedAfterCompletion(); } return results; } @@ -1268,6 +1291,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc } finally { afterOperation(success); + delayedAfterCompletion(); } return result; } @@ -1289,6 +1313,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc success = true; } finally { afterOperation(success); + delayedAfterCompletion(); } return result; } @@ -1305,6 +1330,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc return plan.performIterate( queryParameters, this ); } finally { + delayedAfterCompletion(); dontFlushFromFind--; } } @@ -1319,6 +1345,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc return plan.performScroll( queryParameters, this ); } finally { + delayedAfterCompletion(); dontFlushFromFind--; } } @@ -1333,13 +1360,16 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc getFilterQueryPlan( collection, queryString, null, false ).getParameterMetadata() ); filter.setComment( queryString ); + delayedAfterCompletion(); return filter; } public Query getNamedQuery(String queryName) throws MappingException { errorIfClosed(); checkTransactionSynchStatus(); - return super.getNamedQuery( queryName ); + Query query = super.getNamedQuery( queryName ); + delayedAfterCompletion(); + return query; } public Object instantiate(String entityName, Serializable id) throws HibernateException { @@ -1356,6 +1386,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc if ( result == null ) { result = persister.instantiate( id, this ); } + delayedAfterCompletion(); return result; } @@ -1525,6 +1556,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc finally { dontFlushFromFind--; afterOperation(success); + delayedAfterCompletion(); } return results; } @@ -1534,7 +1566,9 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc errorIfClosed(); checkTransactionSynchStatus(); FilterQueryPlan plan = getFilterQueryPlan( collection, filter, queryParameters, true ); - return plan.performIterate( queryParameters, this ); + Iterator itr = plan.performIterate( queryParameters, this ); + delayedAfterCompletion(); + return itr; } public Criteria createCriteria(Class persistentClass, String alias) { @@ -1581,6 +1615,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc return loader.scroll(this, scrollMode); } finally { + delayedAfterCompletion(); dontFlushFromFind--; } } @@ -1632,6 +1667,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc finally { dontFlushFromFind--; afterOperation(success); + delayedAfterCompletion(); } return results; @@ -1731,6 +1767,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc // an entry in the session's persistence context and the entry reports // that the entity has not been removed EntityEntry entry = persistenceContext.getEntry( object ); + delayedAfterCompletion(); return entry != null && entry.getStatus() != Status.DELETED && entry.getStatus() != Status.GONE; } @@ -1764,6 +1801,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc return loader.scroll(queryParameters, this); } finally { + delayedAfterCompletion(); dontFlushFromFind--; } } @@ -1791,6 +1829,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc } finally { dontFlushFromFind--; + delayedAfterCompletion(); afterOperation(success); } } @@ -1808,6 +1847,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc for ( InitializeCollectionEventListener listener : listeners( EventType.INIT_COLLECTION ) ) { listener.onInitializeCollection( event ); } + delayedAfterCompletion(); } public String bestGuessEntityName(Object object) { @@ -2063,8 +2103,12 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc loadQueryInfluencers.disableFetchProfile( name ); } - private void checkTransactionSynchStatus() { + pulseTransactionCoordinator(); + delayedAfterCompletion(); + } + + private void pulseTransactionCoordinator() { if ( !isClosed() ) { transactionCoordinator.pulse(); } diff --git a/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/Book.java b/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/Book.java index a8be84e57e..cf3e32ea7d 100644 --- a/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/Book.java +++ b/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/Book.java @@ -18,6 +18,13 @@ public class Book { @Version public Integer version; + + public Book() {} + + public Book(String name, Integer version) { + this.name = name; + this.version = version; + } public Integer getId() { return id; diff --git a/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/TransactionJoiningTest.java b/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/TransactionJoiningTest.java index 9ba922f819..8ffb53a8b5 100644 --- a/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/TransactionJoiningTest.java +++ b/hibernate-entitymanager/src/test/java/org/hibernate/ejb/test/transaction/TransactionJoiningTest.java @@ -23,11 +23,16 @@ */ package org.hibernate.ejb.test.transaction; -import java.util.Map; -import javax.persistence.EntityManager; -import javax.transaction.Synchronization; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; -import org.junit.Test; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import javax.persistence.EntityManager; +import javax.transaction.Status; +import javax.transaction.Synchronization; import org.hibernate.Session; import org.hibernate.Transaction; @@ -36,11 +41,11 @@ import org.hibernate.ejb.test.BaseEntityManagerFunctionalTestCase; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.transaction.internal.jta.CMTTransaction; import org.hibernate.engine.transaction.internal.jta.JtaStatusHelper; +import org.hibernate.internal.SessionImpl; +import org.hibernate.testing.TestForIssue; import org.hibernate.testing.jta.TestingJtaBootstrap; import org.hibernate.testing.jta.TestingJtaPlatformImpl; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import org.junit.Test; /** * Largely a copy of {@link org.hibernate.test.jpa.txn.TransactionJoiningTest} @@ -171,5 +176,49 @@ public class TransactionJoiningTest extends BaseEntityManagerFunctionalTestCase ); TestingJtaPlatformImpl.INSTANCE.getTransactionManager().commit(); } + + /** + * In certain JTA environments (JBossTM, etc.), a background thread (reaper) + * can rollback a transaction if it times out. These timeouts are rare and + * typically come from server failures. However, we need to handle the + * multi-threaded nature of the transaction afterCompletion action. + * Emulate a timeout with a simple afterCompletion call in a thread. + * See HHH-7910 + */ + @Test + @TestForIssue(jiraKey="HHH-7910") + public void testMultiThreadTransactionTimeout() throws Exception { + TestingJtaPlatformImpl.INSTANCE.getTransactionManager().begin(); + + EntityManager em = entityManagerFactory().createEntityManager(); + final SessionImpl sImpl = em.unwrap( SessionImpl.class ); + + final CountDownLatch latch = new CountDownLatch(1); + + Thread thread = new Thread() { + public void run() { + sImpl.getTransactionCoordinator().getSynchronizationCallbackCoordinator().afterCompletion( Status.STATUS_ROLLEDBACK ); + latch.countDown(); + } + }; + thread.start(); + + latch.await(); + + em.persist( new Book( "The Book of Foo", 1 ) ); + + // Ensure that the session was cleared by the background thread. + assertEquals( "The background thread did not clear the session as expected!", + 0, em.createQuery( "from Book" ).getResultList().size() ); + TestingJtaPlatformImpl.INSTANCE.getTransactionManager().commit(); + em.close(); + } + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] { + Book.class + }; + } }