diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/JdbcProperties.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/JdbcProperties.java new file mode 100644 index 0000000000..c67d407b9e --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/JdbcProperties.java @@ -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; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakException.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakException.java new file mode 100644 index 0000000000..8a0d62012b --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakException.java @@ -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 ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakUtil.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakUtil.java new file mode 100644 index 0000000000..0983622bb4 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/ConnectionLeakUtil.java @@ -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 . + */ +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 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 ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/H2IdleConnectionCounter.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/H2IdleConnectionCounter.java new file mode 100644 index 0000000000..261c1ef23a --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/H2IdleConnectionCounter.java @@ -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 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 ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/IdleConnectionCounter.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/IdleConnectionCounter.java new file mode 100644 index 0000000000..a205b3666b --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/IdleConnectionCounter.java @@ -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 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); +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/MySQLIdleConnectionCounter.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/MySQLIdleConnectionCounter.java new file mode 100644 index 0000000000..1ce4e5d4e8 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/MySQLIdleConnectionCounter.java @@ -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 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 ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/OracleIdleConnectionCounter.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/OracleIdleConnectionCounter.java new file mode 100644 index 0000000000..2b36527cb8 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/OracleIdleConnectionCounter.java @@ -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 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 ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/PostgreSQLIdleConnectionCounter.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/PostgreSQLIdleConnectionCounter.java new file mode 100644 index 0000000000..62555a059f --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/leak/PostgreSQLIdleConnectionCounter.java @@ -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 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 ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/BaseUnitTestCase.java b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/BaseUnitTestCase.java index 8b441bc38d..4c157df7ae 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/BaseUnitTestCase.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/BaseUnitTestCase.java @@ -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() ) ) {