HHH-10946 - Add utility to detect connection leaks during testing
This commit is contained in:
parent
f0fe332991
commit
9c74438e99
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() ) ) {
|
||||
|
|
Loading…
Reference in New Issue