HHH-10946 - Add utility to detect connection leaks during testing

This commit is contained in:
Vlad Mihalcea 2016-07-11 18:12:14 +03:00
parent f0fe332991
commit 9c74438e99
9 changed files with 406 additions and 1 deletions

View File

@ -0,0 +1,75 @@
package org.hibernate.testing.jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.jboss.logging.Logger;
/**
* @author Vlad Mihalcea
*/
public class JdbcProperties {
private static final Logger log = Logger.getLogger( JdbcProperties.class );
public static final JdbcProperties INSTANCE = new JdbcProperties();
private final String url;
private final String user;
private final String password;
private final Integer poolSize;
public JdbcProperties() {
Properties connectionProperties = new Properties();
InputStream inputStream = null;
try {
inputStream = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream( "hibernate.properties" );
try {
connectionProperties.load( inputStream );
url = connectionProperties.getProperty(
"hibernate.connection.url" );
poolSize = Integer.valueOf( connectionProperties.getProperty(
"hibernate.connection.pool_size" ) );
user = connectionProperties.getProperty(
"hibernate.connection.username" );
password = connectionProperties.getProperty(
"hibernate.connection.password" );
}
catch ( IOException e ) {
throw new IllegalArgumentException( e );
}
}
finally {
try {
if ( inputStream != null ) {
inputStream.close();
}
}
catch ( IOException ignore ) {
log.error( ignore.getMessage() );
}
}
}
public String getUrl() {
return url;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public Integer getPoolSize() {
return poolSize;
}
}

View File

@ -0,0 +1,29 @@
package org.hibernate.testing.jdbc.leak;
/**
* @author Vlad Mihalcea
*/
public class ConnectionLeakException extends RuntimeException {
public ConnectionLeakException() {
}
public ConnectionLeakException(String message) {
super( message );
}
public ConnectionLeakException(String message, Throwable cause) {
super( message, cause );
}
public ConnectionLeakException(Throwable cause) {
super( cause );
}
public ConnectionLeakException(
String message,
Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super( message, cause, enableSuppression, writableStackTrace );
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.testing.jdbc.leak;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import org.hibernate.dialect.Dialect;
import org.hibernate.testing.jdbc.JdbcProperties;
public class ConnectionLeakUtil {
private JdbcProperties jdbcProperties = JdbcProperties.INSTANCE;
private List<IdleConnectionCounter> idleConnectionCounters = Arrays.asList(
H2IdleConnectionCounter.INSTANCE,
OracleIdleConnectionCounter.INSTANCE,
PostgreSQLIdleConnectionCounter.INSTANCE,
MySQLIdleConnectionCounter.INSTANCE
);
private IdleConnectionCounter connectionCounter;
private int connectionLeakCount;
public ConnectionLeakUtil() {
for ( IdleConnectionCounter connectionCounter : idleConnectionCounters ) {
if ( connectionCounter.appliesTo( Dialect.getDialect().getClass() ) ) {
this.connectionCounter = connectionCounter;
break;
}
}
if ( connectionCounter != null ) {
connectionLeakCount = countConnectionLeaks();
}
}
public void assertNoLeaks() {
if ( connectionCounter != null ) {
int currentConnectionLeakCount = countConnectionLeaks();
int diff = currentConnectionLeakCount - connectionLeakCount;
if ( diff > 0 ) {
throw new ConnectionLeakException( String.format(
"%d connection(s) have been leaked! Previous leak count: %d, Current leak count: %d",
diff,
connectionLeakCount,
currentConnectionLeakCount
) );
}
}
}
private int countConnectionLeaks() {
try ( Connection connection = newConnection() ) {
return connectionCounter.count( connection );
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
/**
* Obtain a new JDBC Connection.
*
* @return JDBC Connection
*/
private Connection newConnection() {
try {
return DriverManager.getConnection(
jdbcProperties.getUrl(),
jdbcProperties.getUser(),
jdbcProperties.getPassword()
);
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
}

View File

@ -0,0 +1,40 @@
package org.hibernate.testing.jdbc.leak;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.H2Dialect;
/**
* @author Vlad Mihalcea
*/
public class H2IdleConnectionCounter implements IdleConnectionCounter {
public static final IdleConnectionCounter INSTANCE = new H2IdleConnectionCounter();
@Override
public boolean appliesTo(Class<? extends Dialect> dialect) {
return H2Dialect.class.isAssignableFrom( dialect );
}
@Override
public int count(Connection connection) {
try ( Statement statement = connection.createStatement() ) {
try ( ResultSet resultSet = statement.executeQuery(
"select count(*) " +
"from information_schema.sessions " +
"where statement is null" ) ) {
while ( resultSet.next() ) {
return resultSet.getInt( 1 );
}
return 0;
}
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
}

View File

@ -0,0 +1,29 @@
package org.hibernate.testing.jdbc.leak;
import java.sql.Connection;
import org.hibernate.dialect.Dialect;
/**
* @author Vlad Mihalcea
*/
public interface IdleConnectionCounter {
/**
* Specifies which Dialect the counter applies to.
*
* @param dialect dialect
*
* @return applicability.
*/
boolean appliesTo(Class<? extends Dialect> dialect);
/**
* Count the number of idle connections.
*
* @param connection current JDBC connection to be used for querying the number of idle connections.
*
* @return idle connection count.
*/
int count(Connection connection);
}

View File

@ -0,0 +1,42 @@
package org.hibernate.testing.jdbc.leak;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.MySQL5Dialect;
/**
* @author Vlad Mihalcea
*/
public class MySQLIdleConnectionCounter implements IdleConnectionCounter {
public static final IdleConnectionCounter INSTANCE = new MySQLIdleConnectionCounter();
@Override
public boolean appliesTo(Class<? extends Dialect> dialect) {
return MySQL5Dialect.class.isAssignableFrom( dialect );
}
@Override
public int count(Connection connection) {
try ( Statement statement = connection.createStatement() ) {
try ( ResultSet resultSet = statement.executeQuery(
"SHOW PROCESSLIST" ) ) {
int count = 0;
while ( resultSet.next() ) {
String state = resultSet.getString( "command" );
if ( "sleep".equalsIgnoreCase( state ) ) {
count++;
}
}
return count;
}
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
}

View File

@ -0,0 +1,40 @@
package org.hibernate.testing.jdbc.leak;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.Oracle10gDialect;
/**
* @author Vlad Mihalcea
*/
public class OracleIdleConnectionCounter implements IdleConnectionCounter {
public static final IdleConnectionCounter INSTANCE = new OracleIdleConnectionCounter();
@Override
public boolean appliesTo(Class<? extends Dialect> dialect) {
return Oracle10gDialect.class.isAssignableFrom( dialect );
}
@Override
public int count(Connection connection) {
try ( Statement statement = connection.createStatement() ) {
try ( ResultSet resultSet = statement.executeQuery(
"SELECT count(*) " +
"FROM v$session " +
"where status = 'INACTIVE'" ) ) {
while ( resultSet.next() ) {
return resultSet.getInt( 1 );
}
return 0;
}
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
}

View File

@ -0,0 +1,40 @@
package org.hibernate.testing.jdbc.leak;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.PostgreSQL91Dialect;
/**
* @author Vlad Mihalcea
*/
public class PostgreSQLIdleConnectionCounter implements IdleConnectionCounter {
public static final IdleConnectionCounter INSTANCE = new PostgreSQLIdleConnectionCounter();
@Override
public boolean appliesTo(Class<? extends Dialect> dialect) {
return PostgreSQL91Dialect.class.isAssignableFrom( dialect );
}
@Override
public int count(Connection connection) {
try ( Statement statement = connection.createStatement() ) {
try ( ResultSet resultSet = statement.executeQuery(
"select count(*) " +
"from pg_stat_activity " +
"where state ilike '%idle%'" ) ) {
while ( resultSet.next() ) {
return resultSet.getInt( 1 );
}
return 0;
}
}
catch ( SQLException e ) {
throw new IllegalStateException( e );
}
}
}

View File

@ -10,8 +10,11 @@ import javax.transaction.SystemException;
import org.hibernate.engine.transaction.internal.jta.JtaStatusHelper;
import org.hibernate.testing.jdbc.leak.ConnectionLeakUtil;
import org.hibernate.testing.jta.TestingJtaPlatformImpl;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
@ -28,8 +31,28 @@ import org.jboss.logging.Logger;
public abstract class BaseUnitTestCase {
private static final Logger log = Logger.getLogger( BaseUnitTestCase.class );
private static boolean enableConnectionLeakDetection = Boolean.TRUE.toString()
.equals( System.getenv( "HIBERNATE_CONNECTION_LEAK_DETECTION" ) );
private static ConnectionLeakUtil connectionLeakUtil;
@Rule
public TestRule globalTimeout= new Timeout(30 * 60 * 1000); // no test should run longer than 30 minutes
public TestRule globalTimeout = new Timeout( 30 * 60 * 1000 ); // no test should run longer than 30 minutes
@BeforeClass
public static void initConnectionLeakUtility() {
if ( enableConnectionLeakDetection ) {
connectionLeakUtil = new ConnectionLeakUtil();
}
}
@AfterClass
public static void assertNoLeaks() {
if ( enableConnectionLeakDetection ) {
connectionLeakUtil.assertNoLeaks();
}
}
@After
public void releaseTransactions() {
if ( JtaStatusHelper.isActive( TestingJtaPlatformImpl.INSTANCE.getTransactionManager() ) ) {