HHH-12448 - Fix potential memory leak with Envers and JTA when after-completion callbacks did not fire.

This commit is contained in:
Chris Cranford 2018-04-17 14:02:33 -04:00
parent 83cd43d26b
commit 231dd064a4
3 changed files with 314 additions and 0 deletions

View File

@ -293,6 +293,18 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
return; return;
} }
try {
delayedAfterCompletion();
}
catch ( HibernateException e ) {
if ( getFactory().getSessionFactoryOptions().isJpaBootstrap() ) {
throw this.exceptionConverter.convert( e );
}
else {
throw e;
}
}
if ( sessionEventsManager != null ) { if ( sessionEventsManager != null ) {
sessionEventsManager.end(); sessionEventsManager.end();
} }

View File

@ -0,0 +1,161 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.tm;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Transaction;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.action.spi.AfterTransactionCompletionProcess;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.jta.TestingJtaBootstrap;
import org.hibernate.testing.jta.TestingJtaPlatformImpl;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
import static org.wildfly.common.Assert.assertTrue;
/**
* @author Chris Cranford
*/
public class AfterCompletionTest extends BaseNonConfigCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { SimpleEntity.class };
}
@Override
protected void addSettings(Map settings) {
super.addSettings( settings );
TestingJtaBootstrap.prepare( settings );
settings.put( AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jta" );
settings.put( AvailableSettings.ALLOW_JTA_TRANSACTION_ACCESS, "true" );
}
@Test
@TestForIssue(jiraKey = "HHH-12448")
public void testAfterCompletionCallbackExecutedAfterTransactionTimeout() throws Exception {
// Set timeout to 5 seconds
// Allows the reaper thread to abort our running thread for us
TestingJtaPlatformImpl.INSTANCE.getTransactionManager().setTransactionTimeout( 5 );
// Begin the transaction
TestingJtaPlatformImpl.INSTANCE.getTransactionManager().begin();
Session session = null;
try {
session = openSession();
SimpleEntity entity = new SimpleEntity( "Hello World" );
session.save( entity );
// Register before and after callback handlers
// The before causes the original thread to wait until Reaper aborts the transaction
// The after tracks whether it is invoked since this test is to guarantee it is called
final SessionImplementor sessionImplementor = (SessionImplementor) session;
sessionImplementor.getActionQueue().registerProcess( new AfterCallbackCompletionHandler() );
sessionImplementor.getActionQueue().registerProcess( new BeforeCallbackCompletionHandler() );
TestingJtaPlatformImpl.transactionManager().commit();
}
catch ( Exception e ) {
// This is expected
assertTyping( RollbackException.class, e );
}
finally {
try {
if ( session != null ) {
session.close();
}
}
catch ( HibernateException e ) {
// This is expected
assertEquals( "Transaction was rolled back in a different thread!", e.getMessage() );
}
// verify that the callback was fired.
assertEquals( 1, AfterCallbackCompletionHandler.invoked );
}
}
private void registerAfterCallbackCompletionHandler(Session session) {
( (SessionImplementor) session ).getActionQueue().registerProcess( new AfterCallbackCompletionHandler() );
}
public static class BeforeCallbackCompletionHandler implements BeforeTransactionCompletionProcess {
@Override
public void doBeforeTransactionCompletion(SessionImplementor session) {
try {
// Wait for the transaction to be rolled back by the Reaper thread.
final Transaction transaction = TestingJtaPlatformImpl.transactionManager().getTransaction();
while ( transaction.getStatus() != Status.STATUS_ROLLEDBACK )
Thread.sleep( 10 );
}
catch ( Exception e ) {
// we aren't concerned with this.
}
}
}
public static class AfterCallbackCompletionHandler implements AfterTransactionCompletionProcess {
static int invoked = 0;
@Override
public void doAfterTransactionCompletion(boolean success, SharedSessionContractImplementor session) {
assertTrue( !success );
invoked++;
}
}
@Entity(name = "SimpleEntity")
public static class SimpleEntity {
@Id
@GeneratedValue
private Integer id;
private String name;
SimpleEntity() {
}
SimpleEntity(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -0,0 +1,141 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.envers.test.integration.jta;
import java.lang.reflect.Field;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Transaction;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.boot.internal.EnversService;
import org.hibernate.envers.internal.synchronization.AuditProcess;
import org.hibernate.envers.internal.synchronization.AuditProcessManager;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.Priority;
import org.hibernate.envers.test.entities.IntTestEntity;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.jta.TestingJtaBootstrap;
import org.hibernate.testing.jta.TestingJtaPlatformImpl;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* An envers specific quest that verifies the {@link AuditProcessManager} gets flushed.
*
* There is a similar test called {@link org.hibernate.test.tm.AfterCompletionTest}
* in hibernate-core which verifies that the callbacks fires.
*
* The premise behind this test is to verify that when a JTA transaction is aborted by
* Arjuna's reaper thread, the original thread will still invoke the after-completion
* callbacks making sure that the Envers {@link AuditProcessManager} gets flushed to
* avoid memory leaks.
*
* @author Chris Cranford
*/
@TestForIssue(jiraKey = "HHH-12448")
public class JtaTransactionAfterCallbackTest extends BaseEnversJPAFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { IntTestEntity.class };
}
@Override
protected void addConfigOptions(Map options) {
TestingJtaBootstrap.prepare( options );
options.put( AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jta" );
options.put( AvailableSettings.ALLOW_JTA_TRANSACTION_ACCESS, Boolean.TRUE );
}
@Test
@Priority(10)
public void testAuditProcessManagerFlushedOnTransactionTimeout() throws Exception {
// We will set the timeout to 5 seconds to allow the transaction reaper to kick in for us.
TestingJtaPlatformImpl.INSTANCE.getTransactionManager().setTransactionTimeout( 5 );
// Begin the transaction and do some extensive 10s long work
TestingJtaPlatformImpl.INSTANCE.getTransactionManager().begin();
EntityManager entityManager = null;
try {
entityManager = getEntityManager();
IntTestEntity ite = new IntTestEntity( 10 );
entityManager.persist( ite );
// Register before completion callback
// The before causes this thread to wait until the Reaper thread aborts our transaction
final SessionImplementor session = entityManager.unwrap( SessionImplementor.class );
session.getActionQueue().registerProcess( new BeforeCallbackCompletionHandler() );
TestingJtaPlatformImpl.transactionManager().commit();
}
catch ( Exception e ) {
// This is expected
assertTyping( RollbackException.class, e );
}
finally {
try {
if ( entityManager != null ) {
entityManager.close();
}
}
catch ( PersistenceException e ) {
// we expect this
assertTrue( e.getMessage().contains( "Transaction was rolled back in a different thread!" ) );
}
// test the audit process manager was flushed
assertAuditProcessManagerEmpty();
}
}
public static class BeforeCallbackCompletionHandler implements BeforeTransactionCompletionProcess {
@Override
public void doBeforeTransactionCompletion(SessionImplementor session) {
try {
// Wait for the transaction to be rolled back by the Reaper thread.
final Transaction transaction = TestingJtaPlatformImpl.transactionManager().getTransaction();
while ( transaction.getStatus() != Status.STATUS_ROLLEDBACK )
Thread.sleep( 10 );
}
catch ( Exception e ) {
// we aren't concerned with this.
}
}
}
private void assertAuditProcessManagerEmpty() throws Exception {
final SessionFactoryImplementor sf = entityManagerFactory().unwrap( SessionFactoryImplementor.class );
final EnversService enversService = sf.getServiceRegistry().getService( EnversService.class );
final AuditProcessManager auditProcessManager = enversService.getAuditProcessManager();
Map<Transaction, AuditProcess> values;
Field field = auditProcessManager.getClass().getDeclaredField( "auditProcesses" );
field.setAccessible( true );
values = (Map<Transaction, AuditProcess>) field.get( auditProcessManager );
// assert that the AuditProcess map is not null but empty (e.g. flushed).
assertNotNull( values );
assertEquals( 0, values.size() );
}
}