diff --git a/gradle.properties b/gradle.properties
index 773fe64..c9c8881 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
group = org.xbib
name = database
-version = 2.3.0
+version = 2.3.1
diff --git a/jdbc-pool/src/main/java/module-info.java b/jdbc-pool/src/main/java/module-info.java
index 8fc00b1..fd9f8cc 100644
--- a/jdbc-pool/src/main/java/module-info.java
+++ b/jdbc-pool/src/main/java/module-info.java
@@ -6,6 +6,7 @@ module org.xbib.jdbc.pool {
exports org.xbib.jdbc.pool.api;
exports org.xbib.jdbc.pool.api.cache;
exports org.xbib.jdbc.pool.api.configuration;
+ exports org.xbib.jdbc.pool.api.configuration.supplier;
exports org.xbib.jdbc.pool.api.exceptionsorter;
exports org.xbib.jdbc.pool.api.security;
exports org.xbib.jdbc.pool.api.transaction;
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/AgroalTestGroup.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/AgroalTestGroup.java
new file mode 100644
index 0000000..495a288
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/AgroalTestGroup.java
@@ -0,0 +1,14 @@
+package org.xbib.jdbc.pool.test;
+
+@SuppressWarnings( "UtilityClass" )
+public final class AgroalTestGroup {
+
+ public static final String FUNCTIONAL = "functional";
+ public static final String TRANSACTION = "transaction";
+ public static final String CONCURRENCY = "concurrency";
+ public static final String OSGI = "osgi";
+ public static final String SPRING = "spring";
+
+ private AgroalTestGroup() {
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicConcurrencyTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicConcurrencyTests.java
new file mode 100644
index 0000000..4b35e99
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicConcurrencyTests.java
@@ -0,0 +1,339 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.concurrent.locks.LockSupport;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.CONCURRENCY;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.time.Duration.ofSeconds;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+@Tag( CONCURRENCY )
+public class BasicConcurrencyTests {
+
+ private static final Logger logger = getLogger( BasicConcurrencyTests.class.getName() );
+
+ @BeforeAll
+ static void setup() {
+ registerMockDriver();
+ if ( Utils.isWindowsOS() ) {
+ Utils.windowsTimerHack();
+ }
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Multiple threads" )
+ @SuppressWarnings( "ObjectAllocationInLoop" )
+ void basicConnectionAcquireTest() throws SQLException {
+ int MAX_POOL_SIZE = 10, THREAD_POOL_SIZE = 32, CALLS = 50000, SLEEP_TIME = 1, OVERHEAD = 1;
+
+ ExecutorService executor = newFixedThreadPool( THREAD_POOL_SIZE );
+ CountDownLatch latch = new CountDownLatch( CALLS );
+ BasicConcurrencyTestsListener listener = new BasicConcurrencyTestsListener();
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ for ( int i = 0; i < CALLS; i++ ) {
+ executor.submit( () -> {
+ try {
+ Connection connection = dataSource.getConnection();
+ // logger.info( format( "{0} got {1}", Thread.currentThread().getName(), connection ) );
+ LockSupport.parkNanos( ofMillis( SLEEP_TIME ).toNanos() );
+ connection.close();
+ } catch ( SQLException e ) {
+ fail( "Unexpected SQLException " + e.getMessage() );
+ } finally {
+ latch.countDown();
+ }
+ } );
+ }
+
+ try {
+ long waitTime = ( SLEEP_TIME + OVERHEAD ) * CALLS / MAX_POOL_SIZE;
+ logger.info( format( "Main thread waiting for {0}ms", waitTime ) );
+ if ( !latch.await( waitTime, MILLISECONDS ) ) {
+ fail( "Did not execute within the required amount of time" );
+ }
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ logger.info( format( "Closing DataSource" ) );
+ }
+ logger.info( format( "Main thread proceeding with assertions" ) );
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE, listener.getCreationCount().longValue() );
+ assertEquals( CALLS, listener.getAcquireCount().longValue() );
+ assertEquals( CALLS, listener.getReturnCount().longValue() );
+ } );
+ }
+
+ @Test
+ @DisplayName( "Concurrent DataSource in closed state" )
+ @SuppressWarnings( {"BusyWait", "JDBCResourceOpenedButNotSafelyClosed", "MethodCallInLoopCondition"} )
+ void concurrentDataSourceCloseTest() throws SQLException, InterruptedException {
+ int MAX_POOL_SIZE = 10, THREAD_POOL_SIZE = 2, ACQUISITION_TIMEOUT_MS = 2000;
+
+ BasicConcurrencyTestsListener listener = new BasicConcurrencyTestsListener();
+ ExecutorService executor = newFixedThreadPool( THREAD_POOL_SIZE );
+ CountDownLatch latch = new CountDownLatch( 1 );
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .acquisitionTimeout( ofMillis( ACQUISITION_TIMEOUT_MS ) )
+ );
+
+ AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener );
+
+ executor.submit( () -> {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ try {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection, "Expected non null connection" );
+ //connection.close();
+ } catch ( SQLException e ) {
+ fail( "SQLException", e );
+ }
+ }
+
+ try {
+ assertEquals( 0, dataSource.getMetrics().availableCount(), "Should not be any available connections" );
+
+ logger.info( "Blocked waiting for a connection" );
+ dataSource.getConnection();
+
+ fail( "Expected SQLException" );
+ } catch ( SQLException e ) {
+ // SQLException should not be because of acquisition timeout
+ assertTrue( e.getCause() instanceof RejectedExecutionException || e.getCause() instanceof CancellationException, "Cause for SQLException should be either RejectedExecutionException or CancellationException" );
+ latch.countDown();
+
+ logger.info( "Unblocked after datasource close" );
+ } catch ( Throwable t ) {
+ fail( "Unexpected throwable", t );
+ }
+ } );
+
+ do {
+ Thread.sleep( ACQUISITION_TIMEOUT_MS / 10 );
+ } while ( dataSource.getMetrics().awaitingCount() == 0 );
+
+ logger.info( "Closing the datasource" );
+ dataSource.close();
+
+ if ( !latch.await( ACQUISITION_TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( "Did not execute within the required amount of time" );
+ }
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, dataSource::getConnection );
+ assertFalse( listener.getWarning().get(), "Unexpected warning" );
+ assertEquals( MAX_POOL_SIZE, listener.getCreationCount().longValue() );
+ assertEquals( MAX_POOL_SIZE, listener.getAcquireCount().longValue() );
+ assertEquals( MAX_POOL_SIZE, listener.getDestroyCount().longValue() );
+ assertEquals( MAX_POOL_SIZE, dataSource.getMetrics().destroyCount(), "Destroy count" );
+ assertEquals( 0, listener.getReturnCount().longValue() );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Active connections" );
+ assertEquals( 0, dataSource.getMetrics().availableCount(), "Should not be any available connections" );
+ } );
+
+ // Subsequent calls to dataSource.close() should not throw any exception
+ dataSource.close();
+ }
+
+ @Test
+ @DisplayName( "DataSource close" )
+ void dataSourceCloseTest() throws SQLException, InterruptedException {
+ int MAX_POOL_SIZE = 10, TIMEOUT_MS = 1000;
+
+ ShutdownListener listener = new ShutdownListener( MAX_POOL_SIZE );
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ // Add periodic tasks that should be cancelled on close
+ .reapTimeout( ofSeconds( 10 ) )
+ .validationTimeout( ofSeconds( 2 ) )
+ );
+
+ AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener );
+
+ if ( !listener.getStartupLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( "Did not execute within the required amount of time" );
+ }
+
+ assertEquals( MAX_POOL_SIZE, dataSource.getMetrics().availableCount() );
+
+ Connection c = dataSource.getConnection();
+ assertEquals( 1, dataSource.getMetrics().activeCount() );
+ assertFalse( c.isClosed() );
+
+ dataSource.close();
+
+ // Connections take a while to be destroyed because the executor has to wait on the listener.
+ // We check right after close() to make sure all were destroyed when the method returns.
+ assertAll( () -> {
+ assertFalse( listener.getWarning(), "Datasource closed but there are tasks to run" );
+ assertEquals( 0, dataSource.getMetrics().availableCount() );
+ assertEquals( MAX_POOL_SIZE, dataSource.getMetrics().destroyCount() );
+ } );
+ }
+
+ // --- //
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class BasicConcurrencyTestsListener implements AgroalDataSourceListener {
+
+ private final LongAdder creationCount = new LongAdder(), acquireCount = new LongAdder(), returnCount = new LongAdder(), destroyCount = new LongAdder();
+
+ private final AtomicBoolean warning = new AtomicBoolean( false );
+
+ BasicConcurrencyTestsListener() {
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ creationCount.increment();
+ }
+
+ @Override
+ public void onConnectionAcquire(Connection connection) {
+ acquireCount.increment();
+ }
+
+ @Override
+ public void onConnectionReturn(Connection connection) {
+ returnCount.increment();
+ }
+
+ @Override
+ public void beforeConnectionDestroy(Connection connection) {
+ destroyCount.increment();
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warning.set( true );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ warning.set( true );
+ }
+
+ // --- //
+
+ LongAdder getCreationCount() {
+ return creationCount;
+ }
+
+ LongAdder getAcquireCount() {
+ return acquireCount;
+ }
+
+ LongAdder getReturnCount() {
+ return returnCount;
+ }
+
+ LongAdder getDestroyCount() {
+ return destroyCount;
+ }
+
+ AtomicBoolean getWarning() {
+ return warning;
+ }
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class ShutdownListener implements AgroalDataSourceListener {
+ private boolean warning;
+ private final CountDownLatch startupLatch;
+
+ ShutdownListener(int poolSize) {
+ startupLatch = new CountDownLatch( poolSize );
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ startupLatch.countDown();
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ try {
+ // sleep for 1 ms
+ Thread.sleep( 1 );
+ } catch ( InterruptedException e ) {
+ fail( "Interrupted" );
+ }
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warning = true;
+ }
+
+ CountDownLatch getStartupLatch() {
+ return startupLatch;
+ }
+
+ boolean getWarning() {
+ return warning;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicHikariTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicHikariTests.java
new file mode 100644
index 0000000..bfed781
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicHikariTests.java
@@ -0,0 +1,173 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockDriver;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalDataSourceConfiguration.DataSourceImplementation.HIKARI;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static java.lang.System.nanoTime;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class BasicHikariTests {
+
+ private static final Logger logger = getLogger( BasicHikariTests.class.getName() );
+
+ private static final Driver fakeDriver = new FakeDriver();
+
+ @BeforeAll
+ static void setupMockDriver() throws SQLException {
+ DriverManager.registerDriver( fakeDriver );
+ }
+
+ @AfterAll
+ static void teardown() throws SQLException {
+ DriverManager.deregisterDriver( fakeDriver );
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Mock driver providing fake connections" )
+ void basicConnectionAcquireTest() throws SQLException {
+ int VALIDATION_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( HIKARI )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .validationTimeout( ofMillis( VALIDATION_MS ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClassName( fakeDriver.getClass().getName() )
+ .jdbcUrl( "jdbc://" )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals( connection.getSchema(), FakeDriver.FakeConnection.FAKE_SCHEMA );
+ logger.info( format( "Got schema \"{0}\" from {1}", connection.getSchema(), connection ) );
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "Connection wrapper in closed state" )
+ void basicConnectionCloseTest() throws SQLException {
+ int VALIDATION_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( HIKARI )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .validationTimeout( ofMillis( VALIDATION_MS ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClassName( fakeDriver.getClass().getName() )
+ .jdbcUrl( "jdbc://" )
+ )
+ );
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ Connection connection = dataSource.getConnection();
+
+ assertAll( () -> {
+ assertFalse( connection.isClosed(), "Expected open connection, but it's closed" );
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ } );
+
+ connection.close();
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, connection::getSchema );
+ assertTrue( connection.isClosed(), "Expected closed connection, but it's open" );
+ } );
+ }
+ }
+
+ @Test
+ @DisplayName( "Acquisition timeout" )
+ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" )
+ void basicAcquisitionTimeoutTest() throws SQLException {
+ int MAX_POOL_SIZE = 100, ACQUISITION_TIMEOUT_MS = 1000, VALIDATION_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( HIKARI )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ .acquisitionTimeout( ofMillis( ACQUISITION_TIMEOUT_MS ) )
+ .validationTimeout( ofMillis( VALIDATION_MS ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClassName( fakeDriver.getClass().getName() )
+ .jdbcUrl( "jdbc://" )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ //connection.close();
+ }
+ logger.info( format( "Holding all {0} connections from the pool and requesting a new one", MAX_POOL_SIZE ) );
+
+ long start = nanoTime(), timeoutBound = (long) ( ACQUISITION_TIMEOUT_MS * 1.1 );
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting acquisition timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Acquisition timeout after {0}ms - Configuration is {1}ms", elapsed, ACQUISITION_TIMEOUT_MS ) );
+ assertTrue( elapsed > ACQUISITION_TIMEOUT_MS, "Acquisition timeout before time" );
+ }
+ }
+
+ // --- //
+
+ public static class FakeDriver implements MockDriver {
+ @Override
+ public Connection connect(String url, Properties info) {
+ return new FakeConnection();
+ }
+
+ private static class FakeConnection implements MockConnection {
+
+ private static final String FAKE_SCHEMA = "skeema";
+
+ FakeConnection() {
+ }
+
+ @Override
+ public String getSchema() {
+ return FAKE_SCHEMA;
+ }
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicNarayanaTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicNarayanaTests.java
new file mode 100644
index 0000000..c03c165
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicNarayanaTests.java
@@ -0,0 +1,235 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.narayana.NarayanaTransactionIntegration;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import jakarta.transaction.HeuristicMixedException;
+import jakarta.transaction.HeuristicRollbackException;
+import jakarta.transaction.NotSupportedException;
+import jakarta.transaction.RollbackException;
+import jakarta.transaction.SystemException;
+import jakarta.transaction.TransactionManager;
+import jakarta.transaction.TransactionSynchronizationRegistry;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalConnectionPoolConfiguration.TransactionRequirement.STRICT;
+import static io.agroal.api.configuration.AgroalConnectionPoolConfiguration.TransactionRequirement.WARN;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.AgroalTestGroup.TRANSACTION;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+@Tag( TRANSACTION )
+public class BasicNarayanaTests {
+
+ static final Logger logger = getLogger( BasicNarayanaTests.class.getName() );
+
+ @BeforeAll
+ static void setup() {
+ registerMockDriver();
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Connection acquire test" )
+ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" )
+ void basicConnectionAcquireTest() throws SQLException {
+ TransactionManager txManager = com.arjuna.ats.jta.TransactionManager.transactionManager();
+ TransactionSynchronizationRegistry txSyncRegistry = new com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple();
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ .connectionFactoryConfiguration( cf -> cf.autoCommit( true ) )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ txManager.begin();
+
+ Connection connection = dataSource.getConnection();
+ logger.info( format( "Got connection {0}", connection ) );
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, () -> connection.setAutoCommit( true ) );
+ assertFalse( connection.getAutoCommit(), "Expect connection to have autocommit not set" );
+ // TODO: comparing toString is brittle. Find a better way to make sure the underlying physical connection is the same.
+ assertEquals( connection.toString(), dataSource.getConnection().toString(), "Expect the same connection under the same transaction" );
+ } );
+
+ txManager.commit();
+
+ assertTrue( connection.isClosed() );
+ } catch ( NotSupportedException | SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException e ) {
+ fail( "Exception: " + e.getMessage() );
+ }
+ }
+
+ @Test
+ @DisplayName( "Basic rollback test" )
+ void basicRollbackTest() throws SQLException {
+ TransactionManager txManager = com.arjuna.ats.jta.TransactionManager.transactionManager();
+ TransactionSynchronizationRegistry txSyncRegistry = new com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple();
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ txManager.begin();
+
+ Connection connection = dataSource.getConnection();
+ logger.info( format( "Got connection {0}", connection ) );
+
+ txManager.rollback();
+
+ assertTrue( connection.isClosed() );
+ } catch ( NotSupportedException | SystemException e ) {
+ fail( "Exception: " + e.getMessage() );
+ }
+ }
+
+ @Test
+ @DisplayName( "Multiple close test" )
+ void multipleCloseTest() throws SQLException {
+ TransactionManager txManager = com.arjuna.ats.jta.TransactionManager.transactionManager();
+ TransactionSynchronizationRegistry txSyncRegistry = new com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple();
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ // there is a call to connection#close in the try-with-resources block and another on the callback from the transaction#commit()
+ try ( Connection connection = dataSource.getConnection() ) {
+ logger.info( format( "Got connection {0}", connection ) );
+ try {
+ txManager.begin();
+ txManager.commit();
+ } catch ( NotSupportedException | SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException e ) {
+ fail( "Exception: " + e.getMessage() );
+ }
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Transaction required tests" )
+ void transactionRequiredTests() throws SQLException {
+ TransactionManager txManager = com.arjuna.ats.jta.TransactionManager.transactionManager();
+ TransactionSynchronizationRegistry txSyncRegistry = new com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple();
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ ), new NoWarningsListener() ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ logger.info( "Got connection " + c );
+ }
+ }
+
+ WarningListener warningListener = new WarningListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ .transactionRequirement( WARN )
+ ), warningListener ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ assertEquals( 1, warningListener.getWarnings(), "Expected a warning message" );
+ logger.info( "Got connection with warning :" + c );
+ }
+ }
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .transactionIntegration( new NarayanaTransactionIntegration( txManager, txSyncRegistry ) )
+ .transactionRequirement( STRICT )
+ ) ) ) {
+ assertThrows( SQLException.class, dataSource::getConnection );
+
+ // Make sure connection is available after getConnection() throws
+ txManager.begin();
+ try ( Connection c = dataSource.getConnection() ) {
+ logger.info( "Got connection with tx :" + c );
+ }
+ txManager.rollback();
+ } catch ( SystemException | NotSupportedException e ) {
+ fail( "Exception: " + e.getMessage() );
+ }
+ }
+
+ // --- //
+ private static class NoWarningsListener implements AgroalDataSourceListener {
+
+ @SuppressWarnings( "WeakerAccess" )
+ NoWarningsListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ fail( "Got warning: " + message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ fail( "Got warning: " + throwable.getMessage() );
+ }
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class WarningListener implements AgroalDataSourceListener {
+
+ private int warnings;
+
+ WarningListener() {
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ logger.warning( throwable.getMessage() );
+ warnings++;
+ }
+
+ int getWarnings() {
+ return warnings;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicTests.java
new file mode 100644
index 0000000..038b70e
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/BasicTests.java
@@ -0,0 +1,567 @@
+package org.xbib.jdbc.pool.test;
+
+import java.io.IOException;
+import org.xbib.jdbc.pool.api.XbibDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.logging.Logger;
+import org.xbib.jdbc.pool.api.XbibDataSourceListener;
+import org.xbib.jdbc.pool.api.configuration.supplier.XbibDataSourceConfigurationSupplier;
+
+import static java.lang.Thread.currentThread;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction.STRICT;
+import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction.WARN;
+import static org.xbib.jdbc.pool.test.MockDriver.deregisterMockDriver;
+import static org.xbib.jdbc.pool.test.MockDriver.registerMockDriver;
+
+public class BasicTests {
+
+ private static final Logger logger = getLogger( BasicTests.class.getName() );
+
+ private static final String FAKE_SCHEMA = "skeema";
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeSchemaConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ @Test
+ @DisplayName( "Mock driver providing fake connections" )
+ void basicConnectionAcquireTest() throws SQLException, IOException {
+ try (XbibDataSource dataSource = XbibDataSource.from( new XbibDataSourceConfigurationSupplier().connectionPoolConfiguration(cp -> cp.maxSize( 1 ) ) ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals(FAKE_SCHEMA, connection.getSchema());
+ logger.info( format( "Got schema \"{0}\" from {1}", connection.getSchema(), connection ) );
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "DataSource in closed state" )
+ @SuppressWarnings( "AnonymousInnerClassMayBeStatic" )
+ void basicDataSourceCloseTest() throws SQLException, IOException {
+ AtomicBoolean warning = new AtomicBoolean( false );
+ XbibDataSource dataSource = XbibDataSource.from( new XbibDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 2 ) ), new XbibDataSourceListener() {
+ @Override
+ public void onWarning(String message) {
+ warning.set( true );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ warning.set( true );
+ }
+ } );
+
+ Connection connection = dataSource.getConnection();
+ Connection leaked = dataSource.getConnection();
+ assertAll( () -> {
+ assertFalse( connection.isClosed(), "Expected open connection, but it's closed" );
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ } );
+ connection.close();
+
+ dataSource.close();
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, dataSource::getConnection );
+ assertTrue( leaked.isClosed(), "Expected closed connection, but it's open" );
+ assertFalse( warning.get(), "Unexpected warning" );
+ } );
+ }
+
+ @Test
+ @DisplayName( "Leak detection" )
+ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" )
+ void basicLeakDetectionTest() throws SQLException, IOException {
+ int MAX_POOL_SIZE = 100, LEAK_DETECTION_MS = 1000;
+ Thread leakingThread = currentThread();
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .leakTimeout( ofMillis( LEAK_DETECTION_MS ) )
+ .acquisitionTimeout( ofMillis( LEAK_DETECTION_MS ) )
+ );
+ CountDownLatch latch = new CountDownLatch( MAX_POOL_SIZE );
+
+ XbibDataSourceListener listener = new LeakDetectionListener( leakingThread, latch );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ //connection.close();
+ }
+ try {
+ logger.info( format( "Holding all {0} connections from the pool and waiting for leak notifications", MAX_POOL_SIZE ) );
+ if ( !latch.await( 3L * LEAK_DETECTION_MS, MILLISECONDS ) ) {
+ fail( format( "Missed detection of {0} leaks", latch.getCount() ) );
+ }
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Borrow Connection Validation" )
+ void basicBorrowValidationTest() throws SQLException, IOException {
+ int CALLS = 10;
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 2 )
+ .validateOnBorrow(true)
+ );
+ ValidationCountListener listener = new ValidationCountListener();
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ for ( int i = 0; i < CALLS; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ connection.close();
+ }
+ }
+
+ assertEquals( CALLS, listener.getValidationCount() );
+ }
+
+ @Test
+ @DisplayName( "Connection Validation" )
+ void basicValidationTest() throws SQLException, IOException {
+ int MAX_POOL_SIZE = 100, CALLS = 1000, VALIDATION_MS = 1000;
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .validationTimeout( ofMillis( VALIDATION_MS ) )
+ );
+
+ CountDownLatch latch = new CountDownLatch( MAX_POOL_SIZE );
+
+ XbibDataSourceListener listener = new ValidationListener( latch );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ for ( int i = 0; i < CALLS; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ connection.close();
+ }
+ try {
+ logger.info( format( "Awaiting for validation of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ if ( !latch.await( 3L * VALIDATION_MS, MILLISECONDS ) ) {
+ fail( format( "Validation of {0} connections", latch.getCount() ) );
+ }
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Idle Connection Validation" )
+ void basicIdleValidationTest() throws SQLException, IOException {
+ int CALLS = 10, IDLE_VALIDATION_MS = 1000;
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 2 )
+ .idleValidationTimeout( ofMillis( IDLE_VALIDATION_MS ) )
+ );
+
+ CountDownLatch latch = new CountDownLatch( 1 );
+
+ XbibDataSourceListener listener = new ValidationListener( latch );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ for ( int i = 0; i < CALLS; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ connection.close();
+ }
+ assertEquals( 1, latch.getCount(), "Not expected validation to occur before " + IDLE_VALIDATION_MS );
+ Executors.newSingleThreadScheduledExecutor().schedule( () -> {
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ } catch ( SQLException e ) {
+ fail( e );
+ }
+ }, IDLE_VALIDATION_MS, MILLISECONDS );
+
+ try {
+ logger.info( format( "Awaiting validation of idle connection" ) );
+ if ( !latch.await( 3L * IDLE_VALIDATION_MS, MILLISECONDS ) ) {
+ fail( format( "Did not validate idle connection", latch.getCount() ) );
+ }
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Connection Reap" )
+ void basicReapTest() throws SQLException, IOException {
+ int MIN_POOL_SIZE = 40, MAX_POOL_SIZE = 100, CALLS = 1000, REAP_TIMEOUT_MS = 1000;
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .reapTimeout( ofMillis( REAP_TIMEOUT_MS ) )
+ );
+
+ CountDownLatch allLatch = new CountDownLatch( MAX_POOL_SIZE );
+ CountDownLatch destroyLatch = new CountDownLatch( MAX_POOL_SIZE - MIN_POOL_SIZE );
+ LongAdder reapCount = new LongAdder();
+
+ XbibDataSourceListener listener = new ReapListener( allLatch, reapCount, destroyLatch );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ for ( int i = 0; i < CALLS; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ connection.close();
+ }
+ try {
+ logger.info( format( "Awaiting test of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ if ( !allLatch.await( 3L * REAP_TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not tested for reap", allLatch.getCount() ) );
+ }
+ logger.info( format( "Waiting for reaping of {0} connections ", MAX_POOL_SIZE - MIN_POOL_SIZE ) );
+ if ( !destroyLatch.await( 2L * REAP_TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} idle connections not sent for destruction", destroyLatch.getCount() ) );
+ }
+ assertEquals( MAX_POOL_SIZE - MIN_POOL_SIZE, reapCount.longValue(), "Unexpected number of idle connections " );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Enhanced leak report" )
+ void enhancedLeakReportTest() throws SQLException, IOException {
+ int LEAK_DETECTION_MS = 1000;
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 10 )
+ .leakTimeout( ofMillis( LEAK_DETECTION_MS ) )
+ .acquisitionTimeout( ofMillis( LEAK_DETECTION_MS ) )
+ .enhancedLeakReport()
+ );
+ CountDownLatch latch = new CountDownLatch( 1 );
+
+ LeakDetectionListener listener = new LeakDetectionListener( currentThread(), latch );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier, listener ) ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ connection.unwrap( Connection.class ).close();
+
+ try {
+ logger.info( format( "Holding connection from the pool and waiting for leak notification" ) );
+ if ( !latch.await( 3L * LEAK_DETECTION_MS, MILLISECONDS ) ) {
+ fail( format( "Missed detection of {0} leaks", latch.getCount() ) );
+ }
+ Thread.sleep( 100 ); // hold for a bit to allow for enhanced info
+ assertEquals( 3 + 1, listener.getInfoCount(), "Not enough info on extended leak report" );
+ assertEquals( 1, listener.getWarningCount(), "Not enough info on extended leak report" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" )
+ @DisplayName( "Initial SQL test" )
+ void initialSQLTest() throws SQLException, IOException {
+ int MAX_POOL_SIZE = 10;
+
+ XbibDataSourceConfigurationSupplier configurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ .connectionFactoryConfiguration( cf -> cf.initialSql( "Initial SQL" ) )
+ );
+ try ( XbibDataSource dataSource = XbibDataSource.from( configurationSupplier ) ) {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ assertEquals( "Initial SQL", dataSource.getConnection().unwrap( FakeSchemaConnection.class ).initialSQL(), "Connection not initialized" );
+ //connection.close();
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Single acquisition" )
+ @SuppressWarnings( {"JDBCResourceOpenedButNotSafelyClosed", "ObjectAllocationInLoop"} )
+ void basicSingleAcquisitionTest() throws SQLException, IOException {
+ int MAX_POOL_SIZE = 10;
+
+ XbibDataSourceConfigurationSupplier offConfigurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ );
+ XbibDataSourceConfigurationSupplier warnConfigurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ .multipleAcquisition( WARN ) );
+ XbibDataSourceConfigurationSupplier strictConfigurationSupplier = new XbibDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ .multipleAcquisition( STRICT ) );
+
+ try ( XbibDataSource dataSource = XbibDataSource.from( offConfigurationSupplier, new NoWarningsAgroalListener() ) ) {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ //connection.close();
+ }
+ }
+ WarningsAgroalListener warningsAgroalListener = new WarningsAgroalListener();
+ try ( XbibDataSource dataSource = XbibDataSource.from( warnConfigurationSupplier, warningsAgroalListener ) ) {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ //connection.close();
+ }
+ assertEquals( MAX_POOL_SIZE - 1, warningsAgroalListener.getWarningCount(), "Wrong number of warning messages" );
+ }
+ try ( XbibDataSource dataSource = XbibDataSource.from( strictConfigurationSupplier, new NoWarningsAgroalListener() ) ) {
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ if ( i == 0 ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ //connection.close();
+ } else {
+ assertThrows( SQLException.class, dataSource::getConnection, "Expected exception on multiple acquisition" );
+ assertEquals( 1, dataSource.getMetrics().acquireCount() );
+ assertEquals( 1, dataSource.getMetrics().activeCount() );
+ }
+ }
+ }
+ }
+
+ // --- //
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class LeakDetectionListener implements XbibDataSourceListener {
+ private final Thread leakingThread;
+ private final CountDownLatch latch;
+ private int infoCount, warningCount;
+
+ LeakDetectionListener(Thread leakingThread, CountDownLatch latch) {
+ this.leakingThread = leakingThread;
+ this.latch = latch;
+ }
+
+ @Override
+ public void onConnectionLeak(Connection connection, Thread thread) {
+ assertEquals( leakingThread, thread, "Wrong thread reported" );
+ latch.countDown();
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warningCount++;
+ logger.warning( message );
+ }
+
+ @Override
+ public void onInfo(String message) {
+ infoCount++;
+ logger.info( message );
+ }
+
+ int getInfoCount() {
+ return infoCount;
+ }
+
+ int getWarningCount() {
+ return warningCount;
+ }
+ }
+
+ private static class ValidationCountListener implements XbibDataSourceListener {
+ private int validationCount;
+
+ @Override
+ public void beforeConnectionValidation(Connection connection) {
+ validationCount++;
+ }
+
+ public int getValidationCount() {
+ return validationCount;
+ }
+ }
+
+ private static class ValidationListener implements XbibDataSourceListener {
+ private final CountDownLatch latch;
+
+ @SuppressWarnings( "WeakerAccess" )
+ ValidationListener(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ @Override
+ public void beforeConnectionValidation(Connection connection) {
+ latch.countDown();
+ }
+ }
+
+ private static class ReapListener implements XbibDataSourceListener {
+
+ private final CountDownLatch allLatch;
+ private final LongAdder reapCount;
+ private final CountDownLatch destroyLatch;
+
+ @SuppressWarnings( "WeakerAccess" )
+ ReapListener(CountDownLatch allLatch, LongAdder reapCount, CountDownLatch destroyLatch) {
+ this.allLatch = allLatch;
+ this.reapCount = reapCount;
+ this.destroyLatch = destroyLatch;
+ }
+
+ @Override
+ public void beforeConnectionReap(Connection connection) {
+ allLatch.countDown();
+ }
+
+ @Override
+ public void onConnectionReap(Connection connection) {
+ reapCount.increment();
+ }
+
+ @Override
+ public void beforeConnectionDestroy(Connection connection) {
+ destroyLatch.countDown();
+ }
+ }
+
+ private static class NoWarningsAgroalListener implements XbibDataSourceListener {
+
+ @SuppressWarnings( "WeakerAccess" )
+ NoWarningsAgroalListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ fail( "Unexpected warn message: " + message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ fail( "Unexpected warn throwable: " + throwable.getMessage() );
+ }
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class WarningsAgroalListener implements XbibDataSourceListener {
+ private int warningCount;
+
+ WarningsAgroalListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warningCount++;
+ logger.warning( message );
+ }
+
+ @Override
+ public void onWarning(Throwable t) {
+ warningCount++;
+ logger.warning( t.getMessage() );
+ }
+
+ int getWarningCount() {
+ return warningCount;
+ }
+ }
+
+ public static class FakeSchemaConnection implements MockConnection {
+
+ private boolean closed;
+ private final RecordingStatement statement = new RecordingStatement();
+
+ @Override
+ public String getSchema() throws SQLException {
+ return FAKE_SCHEMA;
+ }
+
+ @Override
+ public void close() throws SQLException {
+ closed = true;
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return closed;
+ }
+
+ @Override
+ public Statement createStatement() throws SQLException {
+ return statement;
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ String initialSQL() {
+ return statement.getInitialSQL();
+ }
+
+ @Override
+ @SuppressWarnings( "unchecked" )
+ public T unwrap(Class target) throws SQLException {
+ return (T) this;
+ }
+
+ private static class RecordingStatement implements MockStatement {
+
+ private String initialSQL;
+
+ @SuppressWarnings( "WeakerAccess" )
+ RecordingStatement() {
+ }
+
+ @Override
+ public boolean execute(String sql) throws SQLException {
+ initialSQL = sql;
+ return true;
+ }
+
+ String getInitialSQL() {
+ return initialSQL;
+ }
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionCloseTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionCloseTests.java
new file mode 100644
index 0000000..cee76c8
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionCloseTests.java
@@ -0,0 +1,301 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.pool.wrapper.StatementWrapper;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockStatement;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class ConnectionCloseTests {
+
+ private static final Logger logger = getLogger( ConnectionCloseTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeSchemaConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Connection wrapper in closed state" )
+ void basicConnectionCloseTest() throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ) ) ) ) {
+ Connection connection = dataSource.getConnection();
+
+ assertAll( () -> {
+ assertFalse( connection.isClosed(), "Expected open connection, but it's closed" );
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ } );
+
+ connection.close();
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, connection::getSchema );
+ assertTrue( connection.isClosed(), "Expected closed connection, but it's open" );
+ } );
+
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "Connection closes Statements and ResultSets" )
+ void statementCloseTest() throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ) ) ) ) {
+ Connection connection = dataSource.getConnection();
+ logger.info( format( "Creating 2 Statements on Connection {0}", connection ) );
+
+ Statement statementOne = connection.createStatement();
+ Statement statementTwo = connection.createStatement();
+ ResultSet setOne = statementOne.getResultSet();
+ ResultSet setTwo = statementTwo.getResultSet();
+ statementTwo.close();
+
+ assertAll( () -> {
+ assertNotNull( setOne, "Expected non null value" );
+ assertFalse( statementOne.isClosed(), "Expected open Statement, but it's closed" );
+ assertThrows( SQLException.class, statementTwo::getResultSet, "Expected SQLException on closed Connection" );
+ assertTrue( statementTwo.isClosed(), "Expected closed Statement, but it's open" );
+ assertTrue( setTwo.isClosed(), "Expected closed ResultSet, but it's open" );
+ } );
+
+ statementTwo.close();
+ connection.close();
+
+ assertAll( () -> {
+ assertThrows( SQLException.class, statementOne::getResultSet, "Expected SQLException on closed Connection" );
+ assertTrue( statementOne.isClosed(), "Expected closed Statement, but it's open" );
+ assertTrue( setOne.isClosed(), "Expected closed ResultSet, but it's open" );
+ } );
+
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "Connection closed multiple times" )
+ @SuppressWarnings( "RedundantExplicitClose" )
+ void multipleCloseTest() throws SQLException {
+ ReturnListener returnListener = new ReturnListener();
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().metricsEnabled().connectionPoolConfiguration( cp -> cp.maxSize( 1 ) ), returnListener ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ // Explicit close. This is a try-with-resources so there is another call to connection.close() after this one.
+ connection.close();
+ }
+
+ assertAll( () -> {
+ assertEquals( 1, returnListener.getReturnCount().longValue(), "Expecting connection to be returned once to the pool" );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Expecting 0 active connections" );
+ } );
+ }
+ }
+
+ @Test
+ @DisplayName( "Flush on close" )
+ void flushOnCloseTest() throws Exception {
+ OnWarningListener warningListener = new OnWarningListener();
+ OnDestroyListener destroyListener = new OnDestroyListener( 1 );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().metricsEnabled().connectionPoolConfiguration( cp -> cp.minSize( 1 ).maxSize( 1 ).flushOnClose() ), warningListener, destroyListener ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertFalse( connection.isClosed() );
+ }
+
+ assertTrue( destroyListener.awaitSeconds( 1 ) );
+
+ assertAll( () -> {
+ assertFalse( warningListener.getWarning().get(), "Unexpected warning on close connection" );
+ assertEquals( 1, dataSource.getMetrics().destroyCount(), "Expecting 1 destroyed connection" );
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expecting 1 available connection" );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Expecting 0 active connections" );
+ } );
+ }
+ }
+
+ @Test
+ @DisplayName( "Statement close" )
+ void statementClose() throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ) ) ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ try ( Statement statement = connection.createStatement() ) {
+ Statement underlyingStatement = statement.unwrap( ClosableStatement.class );
+ statement.close();
+ assertTrue( underlyingStatement.isClosed() );
+ }
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "ResultSet leak" )
+ void resultSetLeak() throws SQLException {
+ ResultSet resultSet;
+ OnWarningListener listener = new OnWarningListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ) ), listener ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ Statement statement = connection.createStatement();
+ resultSet = statement.getResultSet();
+ statement.close();
+ }
+ }
+ assertTrue( resultSet.isClosed(), "Leaked ResultSet not closed" );
+ assertTrue( listener.getWarning().get(), "No warning message on ResultSet leak" );
+ resultSet.close();
+ }
+
+ @Test
+ @DisplayName( "JDBC resources tracking disabled" )
+ @SuppressWarnings( "InstanceofConcreteClass" )
+ void jdbcResourcesTrackingDisabled() throws SQLException {
+ Statement statement;
+ OnWarningListener listener = new OnWarningListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().metricsEnabled().connectionPoolConfiguration( cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.trackJdbcResources( false ) ) ), listener ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ statement = connection.createStatement();
+ assertTrue( statement instanceof ClosableStatement, "Wrapped Statement when tracking is disabled" );
+ assertThrows( ClassCastException.class, () -> statement.unwrap( StatementWrapper.class ), "Wrapped Statement when tracking is disabled" );
+ }
+ }
+ assertFalse( listener.getWarning().get(), "Leak warning when tracking is disabled " );
+ assertFalse( statement.isClosed(), "Tracking is disabled, but acted to clean leak!" );
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class ReturnListener implements AgroalDataSourceListener {
+
+ private final LongAdder returnCount = new LongAdder();
+
+ ReturnListener() {
+ }
+
+ @Override
+ public void beforeConnectionReturn(Connection connection) {
+ returnCount.increment();
+ }
+
+ LongAdder getReturnCount() {
+ return returnCount;
+ }
+ }
+
+ // --- //
+
+ public static class FakeSchemaConnection implements MockConnection {
+
+ private static final String FAKE_SCHEMA = "skeema";
+
+ @Override
+ public String getSchema() {
+ return FAKE_SCHEMA;
+ }
+
+ @Override
+ public Statement createStatement() throws SQLException {
+ return new ClosableStatement();
+ }
+ }
+
+ public static class ClosableStatement implements MockStatement {
+
+ private boolean closed;
+
+ @Override
+ public void close() throws SQLException {
+ closed = true;
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return closed;
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ return iface.cast( this );
+ }
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class OnWarningListener implements AgroalDataSourceListener {
+
+ private final AtomicBoolean warning = new AtomicBoolean( false );
+
+ OnWarningListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warning.set( true );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ warning.set( true );
+ }
+
+ AtomicBoolean getWarning() {
+ return warning;
+ }
+ }
+
+ @SuppressWarnings( {"WeakerAccess", "SameParameterValue"} )
+ private static class OnDestroyListener implements AgroalDataSourceListener {
+
+ private final CountDownLatch latch;
+
+ OnDestroyListener(int count) {
+ latch = new CountDownLatch( count );
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ latch.countDown();
+ }
+
+ boolean awaitSeconds(int timeout) throws InterruptedException {
+ return latch.await( timeout, TimeUnit.SECONDS );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionResetTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionResetTests.java
new file mode 100644
index 0000000..d64aa29
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ConnectionResetTests.java
@@ -0,0 +1,239 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.NONE;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.READ_COMMITTED;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.READ_UNCOMMITTED;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.REPEATABLE_READ;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.SERIALIZABLE;
+import static io.agroal.api.configuration.AgroalConnectionPoolConfiguration.ExceptionSorter.emptyExceptionSorter;
+import static io.agroal.api.configuration.AgroalConnectionPoolConfiguration.ExceptionSorter.fatalExceptionSorter;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class ConnectionResetTests {
+
+ private static final Logger logger = getLogger( ConnectionResetTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection isolation remains the same after being changed" )
+ void isolationTest() throws SQLException {
+ isolation( NONE, Connection.TRANSACTION_NONE );
+ isolation( READ_UNCOMMITTED, Connection.TRANSACTION_READ_UNCOMMITTED );
+ isolation( READ_COMMITTED, Connection.TRANSACTION_READ_COMMITTED );
+ isolation( REPEATABLE_READ, Connection.TRANSACTION_REPEATABLE_READ );
+ isolation( SERIALIZABLE, Connection.TRANSACTION_SERIALIZABLE );
+ }
+
+ private static void isolation(AgroalConnectionFactoryConfiguration.TransactionIsolation isolation, int level) throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.jdbcTransactionIsolation( isolation ) )
+ ) ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals( connection.getTransactionIsolation(), level );
+ connection.setTransactionIsolation( Connection.TRANSACTION_NONE );
+ connection.close();
+
+ connection = dataSource.getConnection();
+ assertEquals( connection.getTransactionIsolation(), level );
+ logger.info( format( "Got isolation \"{0}\" from {1}", connection.getTransactionIsolation(), connection ) );
+ connection.close();
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection with custom transaction isolation level" )
+ void customIsolationTest() throws SQLException {
+ int level = 42;
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.jdbcTransactionIsolation( level ) )
+ ) ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertEquals( connection.getTransactionIsolation(), level );
+ }
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection reset with default (driver) transaction isolation level" )
+ void defaultIsolationResetTest() throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 )
+ ) ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertEquals( connection.getTransactionIsolation(), FakeConnection.DEFAULT_ISOLATION );
+ connection.setTransactionIsolation( Connection.TRANSACTION_SERIALIZABLE );
+ assertEquals( connection.getTransactionIsolation(), Connection.TRANSACTION_SERIALIZABLE );
+ }
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertEquals( connection.getTransactionIsolation(), FakeConnection.DEFAULT_ISOLATION );
+ }
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection autoCommit status remains the same after being changed" )
+ void autoCommitTest() throws SQLException {
+ autocommit( false );
+ autocommit( true );
+ }
+
+ private static void autocommit(boolean autoCommit) throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.autoCommit( autoCommit ) )
+ ) ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals( connection.getAutoCommit(), autoCommit );
+ connection.setAutoCommit( !autoCommit );
+ connection.close();
+
+ connection = dataSource.getConnection();
+ assertEquals( connection.getAutoCommit(), autoCommit );
+ logger.info( format( "Got autoCommit \"{0}\" from {1}", connection.getAutoCommit(), connection ) );
+ connection.close();
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection with warnings" )
+ void warningsTest() throws SQLException {
+ warnings( false );
+ warnings( true );
+ }
+
+ private static void warnings(boolean fatal) throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp.maxSize( 1 ).exceptionSorter( fatal ? fatalExceptionSorter() : emptyExceptionSorter() ) )
+ ) ) {
+ Connection connection = dataSource.getConnection();
+ assertNotNull( connection.getWarnings() );
+ connection.close();
+
+ connection = dataSource.getConnection();
+ assertEquals( fatal ? 2 : 1, dataSource.getMetrics().creationCount() );
+ assertEquals( fatal, connection.getWarnings() != null ); // checks if warnings were cleared
+ connection.close();
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test exception during reset" )
+ void resetExceptionTest() throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().metricsEnabled().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.connectionProviderClass( SneakyDataSource.class ) ) ) ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ assertThrows( SQLException.class, c::getWarnings );
+ }
+ }
+ }
+
+ // --- //
+
+ public static class FakeConnection implements MockConnection {
+
+ private static final int DEFAULT_ISOLATION = 99;
+
+ private int isolation = DEFAULT_ISOLATION;
+ private boolean autoCommit;
+ private boolean warnings = true;
+
+ @Override
+ @SuppressWarnings( "MagicConstant" )
+ public int getTransactionIsolation() {
+ return isolation;
+ }
+
+ @Override
+ public void setTransactionIsolation(int level) {
+ isolation = level;
+ }
+
+ @Override
+ public boolean getAutoCommit() {
+ return autoCommit;
+ }
+
+ @Override
+ public void setAutoCommit(boolean autoCommit) {
+ this.autoCommit = autoCommit;
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException {
+ return warnings ? new SQLWarning( "SQL Warning" ) : null;
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException {
+ warnings = false;
+ }
+ }
+
+ public static class SneakyDataSource implements MockDataSource {
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ return new SneakyConnection();
+ }
+
+ }
+
+ public static class SneakyConnection implements MockConnection {
+ @Override
+ public SQLWarning getWarnings() throws SQLException {
+ // getWarnings method is called on connection return. Need to make sure the pool is usable in that scenario.
+ throw new SQLException("This one is sneaky!");
+ }
+ }
+
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/DriverTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/DriverTests.java
new file mode 100644
index 0000000..3a2f9ce
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/DriverTests.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockDriver;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class DriverTests {
+
+ static final Logger logger = getLogger( DriverTests.class.getName() );
+
+ // --- //
+
+ @Test
+ @DisplayName( "Driver does not accept the provided URL" )
+ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" )
+ void basicUnacceptableURL() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configuration = new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration(
+ cp -> cp.maxSize( 1 ).connectionFactoryConfiguration(
+ cf -> cf.connectionProviderClass( UnacceptableURLDriver.class ).jdbcUrl( "jdbc:unacceptableURL" )
+ ) );
+
+ DriverAgroalDataSourceListener listener = new DriverAgroalDataSourceListener();
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configuration, listener ) ) {
+ dataSource.getConnection();
+ fail( "Should thrown SQLException" );
+ } catch ( SQLException e ) {
+ logger.info( "Expected SQLException: " + e.getMessage() );
+ }
+ assertTrue( listener.hasWarning(), "An warning message should be issued" );
+ }
+
+ // --- //
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class DriverAgroalDataSourceListener implements AgroalDataSourceListener {
+
+ private boolean warning;
+
+ DriverAgroalDataSourceListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ logger.info( "EXPECTED WARNING: " + message );
+ warning = true;
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ logger.info( "EXPECTED WARNING: " + throwable.getMessage() );
+ warning = true;
+ }
+
+ boolean hasWarning() {
+ return warning;
+ }
+ }
+
+ public static class UnacceptableURLDriver implements MockDriver {
+ @Override
+ public boolean acceptsURL(String url) throws SQLException {
+ return false;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/FlushTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/FlushTests.java
new file mode 100644
index 0000000..7185586
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/FlushTests.java
@@ -0,0 +1,481 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.AgroalConnectionPoolConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.lang.Integer.max;
+import static java.lang.Integer.min;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class FlushTests {
+
+ private static final Logger logger = getLogger( FlushTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "FlushMode.ALL" )
+ void modeAll() throws SQLException {
+ int MIN_POOL_SIZE = 40, MAX_POOL_SIZE = 100, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ );
+
+ FlushListener listener = new FlushListener( new CountDownLatch( MAX_POOL_SIZE ), new CountDownLatch( MAX_POOL_SIZE ), new CountDownLatch( MAX_POOL_SIZE ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ logger.info( format( "Awaiting fill of all the {0} initial connections on the pool", MAX_POOL_SIZE ) );
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", listener.getCreationLatch().getCount() ) );
+ }
+ listener.resetCreationLatch( MIN_POOL_SIZE );
+
+ Connection connection = dataSource.getConnection();
+ assertFalse( connection.isClosed() );
+
+ dataSource.flush( AgroalDataSource.FlushMode.ALL );
+
+ logger.info( format( "Awaiting flush of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ if ( !listener.getFlushLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not flush", listener.getFlushLatch().getCount() ) );
+ }
+ logger.info( format( "Waiting for destruction of {0} connections ", MAX_POOL_SIZE - MIN_POOL_SIZE ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} flushed connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+ logger.info( format( "Awaiting fill of all the {0} min connections on the pool", MIN_POOL_SIZE ) );
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", listener.getCreationLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertTrue( connection.isClosed(), "Expecting connection closed after forced flush" );
+
+ assertEquals( MAX_POOL_SIZE, listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE, listener.getDestroyCount().longValue(), "Unexpected number of destroyed connections" );
+ assertEquals( MAX_POOL_SIZE + MIN_POOL_SIZE, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ @Test
+ @DisplayName( "FlushMode.GRACEFUL" )
+ void modeGraceful() throws SQLException {
+ int MIN_POOL_SIZE = 10, MAX_POOL_SIZE = 30, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ );
+
+ FlushListener listener = new FlushListener( new CountDownLatch( MAX_POOL_SIZE ), new CountDownLatch( MAX_POOL_SIZE - 1 ), new CountDownLatch( MAX_POOL_SIZE - 1 ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ logger.info( format( "Awaiting fill of all the {0} initial connections on the pool", MAX_POOL_SIZE ) );
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", listener.getCreationLatch().getCount() ) );
+ }
+
+ listener.resetCreationLatch( MIN_POOL_SIZE - 1 );
+
+ Connection connection = dataSource.getConnection();
+ assertFalse( connection.isClosed() );
+
+ dataSource.flush( AgroalDataSource.FlushMode.GRACEFUL );
+
+ logger.info( format( "Awaiting flush of the {0} connections on the pool", MAX_POOL_SIZE - 1 ) );
+ if ( !listener.getFlushLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not flush", listener.getFlushLatch().getCount() ) );
+ }
+ logger.info( format( "Waiting for destruction of {0} connections ", MAX_POOL_SIZE - 1 ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} flushed connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created after flush", listener.getCreationLatch().getCount() ) );
+ }
+
+ listener.resetCreationLatch( 1 );
+ listener.resetFlushLatch( 1 );
+ listener.resetDestroyLatch( 1 );
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE, listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE - 1, listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+ assertEquals( MAX_POOL_SIZE + MIN_POOL_SIZE - 1, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+
+ assertEquals( MIN_POOL_SIZE - 1, dataSource.getMetrics().availableCount(), "Pool not fill to min" );
+ assertEquals( 1, dataSource.getMetrics().activeCount(), "Incorrect active count" );
+
+ assertFalse( connection.isClosed(), "Expecting connection open after graceful flush" );
+ } );
+
+ connection.close();
+
+ logger.info( format( "Awaiting flush of one remaining connection" ) );
+ if ( !listener.getFlushLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not flush", listener.getFlushLatch().getCount() ) );
+ }
+ logger.info( format( "Waiting for destruction of one remaining connection" ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} flushed connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+ logger.info( format( "Awaiting creation of one additional connections" ) );
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", listener.getCreationLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE, listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+ assertEquals( MAX_POOL_SIZE + MIN_POOL_SIZE, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Incorrect active count" );
+ assertEquals( MIN_POOL_SIZE, dataSource.getMetrics().availableCount(), "Pool not fill to min" );
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ @Test
+ @DisplayName( "FlushMode.INVALID" )
+ void modeValid() throws SQLException {
+ int MIN_POOL_SIZE = 10, MAX_POOL_SIZE = 30, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .connectionValidator( AgroalConnectionPoolConfiguration.ConnectionValidator.defaultValidator() )
+ );
+
+ FlushListener listener = new FlushListener( new CountDownLatch( MAX_POOL_SIZE ), new CountDownLatch( MAX_POOL_SIZE ), new CountDownLatch( MAX_POOL_SIZE ) );
+ listener.resetValidationLatch( MAX_POOL_SIZE );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ logger.info( format( "Awaiting fill of all the {0} initial connections on the pool", MAX_POOL_SIZE ) );
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", listener.getCreationLatch().getCount() ) );
+ }
+
+ listener.resetCreationLatch( MIN_POOL_SIZE );
+
+ dataSource.flush( AgroalDataSource.FlushMode.INVALID );
+
+ logger.info( format( "Awaiting for validation of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ if ( !listener.getValidationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "Missed validation of {0} connections", listener.getValidationLatch().getCount() ) );
+ }
+ logger.info( format( "Waiting for destruction of {0} connections ", MAX_POOL_SIZE - 1 ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} invalid connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created after flush", listener.getCreationLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE, listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE, listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+ assertEquals( MAX_POOL_SIZE + MIN_POOL_SIZE, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ @Test
+ @DisplayName( "FlushMode.IDLE" )
+ @SuppressWarnings( "ConstantConditions" )
+ void modeIdle() throws SQLException {
+ int MIN_POOL_SIZE = 25, MAX_POOL_SIZE = 50, CALLS = 30, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .connectionValidator( AgroalConnectionPoolConfiguration.ConnectionValidator.defaultValidator() )
+ );
+
+ FlushListener listener = new FlushListener(
+ new CountDownLatch( MAX_POOL_SIZE ),
+ new CountDownLatch( MAX_POOL_SIZE ),
+ new CountDownLatch( MAX_POOL_SIZE - max( MIN_POOL_SIZE, CALLS ) ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ Collection connections = new ArrayList<>();
+ for ( int i = 0; i < CALLS; i++ ) {
+ connections.add( dataSource.getConnection() );
+ }
+
+ // Flush to CALLS
+ dataSource.flush( AgroalDataSource.FlushMode.IDLE );
+
+ logger.info( format( "Waiting for destruction of {0} connections ", MAX_POOL_SIZE - max( MIN_POOL_SIZE, CALLS ) ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} invalid connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE - max( MIN_POOL_SIZE, CALLS ), listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+
+ assertEquals( MAX_POOL_SIZE, listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+ } );
+
+ for ( Connection connection : connections ) {
+ connection.close();
+ }
+
+ int remaining = max( MIN_POOL_SIZE, CALLS ) - min( MIN_POOL_SIZE, CALLS );
+ listener.resetDestroyLatch( remaining );
+
+ // Flush to MIN_SIZE
+ dataSource.flush( AgroalDataSource.FlushMode.IDLE );
+
+ logger.info( format( "Waiting for destruction of {0} remaining connection", remaining ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} flushed connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertEquals( MAX_POOL_SIZE - MIN_POOL_SIZE, listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+
+ assertEquals( MAX_POOL_SIZE + max( MIN_POOL_SIZE, CALLS ), listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE, listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ @Test
+ @DisplayName( "FlushMode.LEAK" )
+ @SuppressWarnings( "ConstantConditions" )
+ void modeLeak() throws SQLException {
+ int MIN_POOL_SIZE = 25, MAX_POOL_SIZE = 50, CALLS = 30, LEAK_MS = 100, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .leakTimeout( ofMillis( LEAK_MS ) )
+ );
+
+ FlushListener listener = new FlushListener(
+ new CountDownLatch( MIN_POOL_SIZE + CALLS ),
+ new CountDownLatch( MAX_POOL_SIZE ),
+ new CountDownLatch( min( MAX_POOL_SIZE, CALLS ) ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ Collection connections = new ArrayList<>();
+ for ( int i = 0; i < CALLS; i++ ) {
+ connections.add( dataSource.getConnection() );
+ }
+
+ Thread.sleep( LEAK_MS << 1 ); // 2 * LEAK_MS
+
+ // Flush to CALLS
+ dataSource.flush( AgroalDataSource.FlushMode.LEAK );
+
+ logger.info( format( "Waiting for destruction of {0} connections ", MAX_POOL_SIZE ) );
+ if ( !listener.getDestroyLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} leak connections not sent for destruction", listener.getDestroyLatch().getCount() ) );
+ }
+ if ( !listener.getCreationLatch().await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created after flush", listener.getCreationLatch().getCount() ) );
+ }
+
+ assertAll( () -> {
+ assertEquals( CALLS, listener.getDestroyCount().longValue(), "Unexpected number of destroy connections" );
+
+ assertEquals( MAX_POOL_SIZE, listener.getFlushCount().longValue(), "Unexpected number of beforeFlushConnection" );
+ assertEquals( MAX_POOL_SIZE + ( CALLS - MIN_POOL_SIZE ), listener.getCreationCount().longValue(), "Unexpected number of created connections" );
+
+ assertTrue( connections.stream().allMatch( c -> {
+ try {
+ return c.isClosed();
+ } catch ( SQLException ignore ) {
+ return false;
+ }
+ } ) );
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ // --- //
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class FlushListener implements AgroalDataSourceListener {
+
+ private final LongAdder creationCount = new LongAdder(), flushCount = new LongAdder(), destroyCount = new LongAdder();
+ private CountDownLatch creationLatch, validationLatch, flushLatch, destroyLatch;
+
+ FlushListener(CountDownLatch creationLatch, CountDownLatch flushLatch, CountDownLatch destroyLatch) {
+ this.creationLatch = creationLatch;
+ this.flushLatch = flushLatch;
+ this.destroyLatch = destroyLatch;
+ }
+
+ @Override
+ public void beforeConnectionCreation() {
+ creationCount.increment();
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ creationLatch.countDown();
+ }
+
+ @Override
+ public void beforeConnectionValidation(Connection connection) {
+ validationLatch.countDown();
+ }
+
+ @Override
+ public void beforeConnectionFlush(Connection connection) {
+ flushCount.increment();
+ }
+
+ @Override
+ public void onConnectionFlush(Connection connection) {
+ flushLatch.countDown();
+ }
+
+ @Override
+ public void beforeConnectionDestroy(Connection connection) {
+ destroyCount.increment();
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ destroyLatch.countDown();
+ }
+
+ // --- //
+
+ void resetCreationLatch(int count) {
+ creationLatch = new CountDownLatch( count );
+ }
+
+ void resetValidationLatch(int count) {
+ validationLatch = new CountDownLatch( count );
+ }
+
+ @SuppressWarnings( "SameParameterValue" )
+ void resetFlushLatch(int count) {
+ flushLatch = new CountDownLatch( count );
+ }
+
+ void resetDestroyLatch(int count) {
+ destroyLatch = new CountDownLatch( count );
+ }
+
+ // --- //
+
+ LongAdder getCreationCount() {
+ return creationCount;
+ }
+
+ LongAdder getFlushCount() {
+ return flushCount;
+ }
+
+ LongAdder getDestroyCount() {
+ return destroyCount;
+ }
+
+ CountDownLatch getCreationLatch() {
+ return creationLatch;
+ }
+
+ CountDownLatch getValidationLatch() {
+ return validationLatch;
+ }
+
+ CountDownLatch getFlushLatch() {
+ return flushLatch;
+ }
+
+ CountDownLatch getDestroyLatch() {
+ return destroyLatch;
+ }
+ }
+
+ // --- //
+
+ public static class FakeConnection implements MockConnection {
+
+ private boolean closed;
+
+ @Override
+ public void close() throws SQLException {
+ closed = true;
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return closed;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/HealthTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/HealthTests.java
new file mode 100644
index 0000000..c1cbe51
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/HealthTests.java
@@ -0,0 +1,232 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.AgroalDataSourceConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.time.Duration.ofMillis;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class HealthTests {
+
+ private static final Logger logger = getLogger( HealthTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver();
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Perform health checks" )
+ void healthChecksTest() throws SQLException, InterruptedException {
+ ValidationCountListener listener = new ValidationCountListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .acquisitionTimeout( ofMillis( 100 ) )
+ ), listener ) ) {
+
+ assertEquals( 0, dataSource.getMetrics().creationCount(), "Expected empty pool" );
+
+ logger.info( "Performing health check on empty pool" );
+ assertTrue( dataSource.isHealthy( false ) );
+
+ assertEquals( 1, dataSource.getMetrics().creationCount(), "Expected connection in pool" );
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expected connection in pool" );
+ assertEquals( 1, listener.beforeCount(), "Expected validation to have been performed" );
+ assertEquals( 1, listener.validCount(), "Expected validation to have been successful" );
+ assertEquals( 0, listener.invalidCount(), "Expected validation to have been successful" );
+
+ logger.info( "Performing health check on non-empty pool" );
+ assertTrue( dataSource.isHealthy( false ) );
+
+ assertEquals( 1, dataSource.getMetrics().creationCount(), "Expected connection to be re-used" );
+ assertEquals( 2, listener.beforeCount(), "Expected validation to have been performed" );
+
+ logger.info( "Performing health check on non-empty pool, on a new connection" );
+ assertTrue( dataSource.isHealthy( true ) );
+
+ assertEquals( 2, dataSource.getMetrics().creationCount(), "Expected connection to be created" );
+ assertEquals( 2, dataSource.getMetrics().availableCount(), "Expected extra connection in pool" );
+ assertEquals( 3, listener.beforeCount(), "Expected validation to have been performed" );
+
+ dataSource.flush( AgroalDataSource.FlushMode.ALL );
+ Thread.sleep( 100 );
+ assertEquals( 0, dataSource.getMetrics().availableCount(), "Expected empty pool" );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertEquals( 3, dataSource.getMetrics().creationCount(), "Expected connection to be created" );
+
+ logger.info( "Performing health check on exhausted pool" );
+ assertThrows( SQLException.class, () -> dataSource.isHealthy( false ), "Expected acquisition timeout" );
+
+ logger.info( "Performing health check on exhausted pool, on a new connection" );
+ assertTrue( dataSource.isHealthy( true ) );
+
+ assertEquals( 4, dataSource.getMetrics().creationCount(), "Expected connection to be re-used" );
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expected one connection in pool" );
+ assertEquals( 1, dataSource.getMetrics().activeCount(), "Expect single connection in use" );
+ assertEquals( 1, dataSource.getMetrics().acquireCount(), "Expect acquisition metric to report a single acquisition" );
+ assertEquals( 4, listener.beforeCount(), "Expected validation to have been performed" );
+
+ // use the connection
+ c.getSchema();
+ }
+
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expected connection flush on close" );
+ }
+ }
+
+ @Test
+ @DisplayName( "Perform health checks on poolless data source" )
+ void polllessTest() throws SQLException, InterruptedException {
+ ValidationCountListener listener = new ValidationCountListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL_POOLLESS )
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionValidator( c -> false ) // connections are always invalid
+ .acquisitionTimeout( ofMillis( 100 ) )
+ ), listener ) ) {
+
+ assertEquals( 0, dataSource.getMetrics().creationCount(), "Expected empty pool" );
+
+ logger.info( "Performing health check on pool(less)" );
+ assertFalse( dataSource.isHealthy( false ) );
+
+ assertEquals( 1, dataSource.getMetrics().creationCount(), "Expected connection in pool" );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Expected no active connection" );
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expected no connection in pool" );
+ assertEquals( 1, listener.beforeCount(), "Expected validation to have been performed" );
+ assertEquals( 0, listener.validCount(), "Expected validation to have been successful" );
+ assertEquals( 1, listener.invalidCount(), "Expected validation to have been successful" );
+
+ logger.info( "Performing health check on pool(less), on a new connection" );
+ assertFalse( dataSource.isHealthy( true ) );
+
+ assertEquals( 2, dataSource.getMetrics().creationCount(), "Expected connection to be created" );
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Expected no active connection" );
+ assertEquals( 1, dataSource.getMetrics().availableCount(), "Expected no connection in pool" );
+ assertEquals( 2, listener.beforeCount(), "Expected validation to have been performed" );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertEquals( 3, dataSource.getMetrics().creationCount(), "Expected connection to be created" );
+
+ logger.info( "Performing health check on exhausted pool(less)" );
+ assertThrows( SQLException.class, () -> dataSource.isHealthy( false ), "Expected acquisition timeout" );
+
+ logger.info( "Performing health check on exhausted pool(less), on a new connection" );
+ assertFalse( dataSource.isHealthy( true ) );
+
+ assertEquals( 4, dataSource.getMetrics().creationCount(), "Expected connection to be re-used" );
+ assertEquals( 0, dataSource.getMetrics().availableCount(), "Expected no connection in pool" );
+ assertEquals( 1, dataSource.getMetrics().activeCount(), "Expect single connection in use" );
+ assertEquals( 1, dataSource.getMetrics().acquireCount(), "Expect acquisition metric to report a single acquisition" );
+ assertEquals( 3, listener.beforeCount(), "Expected validation to have been performed" );
+
+ // use the connection
+ c.getSchema();
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Perform health checks when connection provider throws" )
+ void bogusFactoryTest() throws SQLException, InterruptedException {
+ ValidationCountListener listener = new ValidationCountListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf.connectionProviderClass( UnhealthyDataSource.class ) )
+ ), listener ) ) {
+
+ assertThrows( SQLException.class, () -> dataSource.isHealthy( true ), "Expected exception from connection provider" );
+
+ assertEquals( 0, dataSource.getMetrics().creationCount(), "Expected no connection to be created" );
+ assertEquals( 0, listener.beforeCount(), "Expected no validation to have been performed" );
+ }
+ }
+
+ // --- //
+
+ private static class ValidationCountListener implements AgroalDataSourceListener {
+
+ private final AtomicInteger before = new AtomicInteger(), valid = new AtomicInteger(), invalid = new AtomicInteger();
+
+ ValidationCountListener() {
+ }
+
+ @Override
+ public void beforeConnectionValidation(Connection connection) {
+ before.incrementAndGet();
+ }
+
+ @Override
+ public void onConnectionValid(Connection connection) {
+ valid.incrementAndGet();
+ }
+
+ @Override
+ public void onConnectionInvalid(Connection connection) {
+ invalid.incrementAndGet();
+ }
+
+ public int beforeCount() {
+ return before.get();
+ }
+
+ public int validCount() {
+ return valid.get();
+ }
+
+ public int invalidCount() {
+ return invalid.get();
+ }
+ }
+
+ // --- //
+
+ public static class UnhealthyDataSource implements MockDataSource {
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ throw new SQLException( "Unobtainable" );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/InterceptorTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/InterceptorTests.java
new file mode 100644
index 0000000..d7d67d1
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/InterceptorTests.java
@@ -0,0 +1,275 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.AgroalPoolInterceptor;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL_POOLLESS;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.util.Arrays.asList;
+import static java.util.List.of;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class InterceptorTests {
+
+ static final Logger logger = getLogger( InterceptorTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeSchemaConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ protected static void setSchema(String value, Connection connection) {
+ try {
+ connection.setSchema( value );
+ } catch ( SQLException e ) {
+ fail();
+ }
+ }
+
+ protected static void assertSchema(String expected, Connection connection) {
+ try {
+ assertEquals( expected, connection.getSchema() );
+ } catch ( SQLException e ) {
+ fail();
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Interceptor basic test" )
+ void basicInterceptorTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new InterceptorListener() ) ) {
+ dataSource.setPoolInterceptors( asList( new LowPriorityInterceptor(), new MainInterceptor() ) );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertSchema( "during", c );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Negative priority test" )
+ void negativePriorityTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new InterceptorListener() ) ) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> dataSource.setPoolInterceptors( asList( new LowPriorityInterceptor(), new MainInterceptor(), new NegativePriorityInterceptor() ) ),
+ "Interceptors with negative priority throw an IllegalArgumentException as negative priority values are reserved." );
+ }
+ }
+
+ @Test
+ @DisplayName( "Pool interceptor test" )
+ void poolInterceptorTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 ) );
+
+ InvocationCountInterceptor countInterceptor = new InvocationCountInterceptor();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ dataSource.setPoolInterceptors( of( countInterceptor ) );
+
+ assertEquals( 0, countInterceptor.created, "Expected connection not created" );
+ assertEquals( 0, countInterceptor.acquired, "Expected connection not acquired" );
+
+ try ( Connection ignored = dataSource.getConnection() ) {
+ assertEquals( 1, countInterceptor.created, "Expected one connection created" );
+ assertEquals( 1, countInterceptor.acquired, "Expected one connection acquired" );
+ }
+ assertEquals( 1, countInterceptor.returned, "Expected one connection returned" );
+
+ try ( Connection ignored = dataSource.getConnection() ) {
+ assertEquals( 1, countInterceptor.created, "Expected one connection created" );
+ assertEquals( 2, countInterceptor.acquired, "Expected two connection acquired" );
+ }
+ assertEquals( 2, countInterceptor.returned, "Expected two connection returned" );
+ }
+ assertEquals( 1, countInterceptor.destroy, "Expected one connection destroyed" );
+ }
+
+ @Test
+ @DisplayName( "Pooless interceptor test" )
+ void poolessInterceptorTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( AGROAL_POOLLESS )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 ) );
+
+ InvocationCountInterceptor countInterceptor = new InvocationCountInterceptor();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ dataSource.setPoolInterceptors( of( countInterceptor ) );
+
+ assertEquals( 0, countInterceptor.created, "Expected connection not created" );
+ assertEquals( 0, countInterceptor.acquired, "Expected connection not acquired" );
+
+ try ( Connection ignored = dataSource.getConnection() ) {
+ assertEquals( 1, countInterceptor.created, "Expected one connection created" );
+ assertEquals( 1, countInterceptor.acquired, "Expected one connection acquired" );
+ }
+ assertEquals( 1, countInterceptor.returned, "Expected one connection returned" );
+ assertEquals( 1, countInterceptor.destroy, "Expected one connection destroyed" );
+ }
+ }
+
+ // --- //
+
+ private static class InterceptorListener implements AgroalDataSourceListener {
+
+ @SuppressWarnings( "WeakerAccess" )
+ InterceptorListener() {
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ assertSchema( "before", connection );
+ }
+
+ @Override
+ public void beforeConnectionDestroy(Connection connection) {
+ assertSchema( "after", connection );
+ }
+
+ @Override
+ public void onInfo(String message) {
+ logger.info( message );
+ }
+ }
+
+ private static class MainInterceptor implements AgroalPoolInterceptor {
+
+ @SuppressWarnings( "WeakerAccess" )
+ MainInterceptor() {
+ }
+
+ @Override
+ public void onConnectionAcquire(Connection connection) {
+ setSchema( "during", connection );
+ }
+
+ @Override
+ public void onConnectionReturn(Connection connection) {
+ setSchema( "after", connection );
+ }
+ }
+
+ // This interceptor should be "inner" of the main one because has lower priority.
+ private static class LowPriorityInterceptor implements AgroalPoolInterceptor {
+
+ @SuppressWarnings( "WeakerAccess" )
+ LowPriorityInterceptor() {
+ }
+
+ @Override
+ public void onConnectionAcquire(Connection connection) {
+ assertSchema( "during", connection );
+ }
+
+ @Override
+ public void onConnectionReturn(Connection connection) {
+ assertSchema( "during", connection );
+ }
+
+ @Override
+ public int getPriority() {
+ return 1;
+ }
+ }
+
+ // This interceptor is invalid because priority is negative.
+ private static class NegativePriorityInterceptor implements AgroalPoolInterceptor {
+
+ @SuppressWarnings( "WeakerAccess" )
+ NegativePriorityInterceptor() {
+ }
+
+ @Override
+ public int getPriority() {
+ return -1;
+ }
+ }
+
+ // --- //
+
+ private static class InvocationCountInterceptor implements AgroalPoolInterceptor {
+
+ private int created, acquired, returned, destroy;
+
+ @Override
+ public void onConnectionCreate(Connection connection) {
+ created++;
+ }
+
+ @Override
+ public void onConnectionAcquire(Connection connection) {
+ acquired++;
+ }
+
+ @Override
+ public void onConnectionReturn(Connection connection) {
+ returned++;
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ destroy++;
+ }
+ }
+
+ // --- //
+
+
+ public static class FakeSchemaConnection implements MockConnection {
+
+ private String schema = "before";
+
+ @Override
+ public String getSchema() throws SQLException {
+ return schema;
+ }
+
+ @Override
+ public void setSchema(String schema) {
+ this.schema = schema;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/IsolatingClassloaderLauncherInterceptor.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/IsolatingClassloaderLauncherInterceptor.java
new file mode 100644
index 0000000..cb55ba0
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/IsolatingClassloaderLauncherInterceptor.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2023 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import org.junit.platform.launcher.LauncherInterceptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.logging.Logger;
+
+import static java.lang.Thread.currentThread;
+import static java.util.logging.Logger.getLogger;
+
+/**
+ * {@link LauncherInterceptor} that loads test classes, and classes the test relies on from designated packages, in new {@link ClassLoader}.
+ *
+ * @author Luis Barreiro
+ */
+@SuppressWarnings( "unused" )
+public class IsolatingClassloaderLauncherInterceptor implements LauncherInterceptor {
+
+ private static final String TRANSACTION_TEST_PACKAGE_PREFIX = "io.agroal.test.narayana", NARAYANA_PACKAGE_PREFIX = "com.arjuna";
+
+ private final ClassLoader parentClassloader;
+
+ public IsolatingClassloaderLauncherInterceptor() {
+ parentClassloader = currentThread().getContextClassLoader();
+ }
+
+ @Override
+ public T intercept(Invocation invocation) {
+ currentThread().setContextClassLoader( new IsolatingClassLoader( parentClassloader ) );
+ return invocation.proceed();
+ }
+
+ @Override
+ public void close() {
+ currentThread().setContextClassLoader( parentClassloader );
+ }
+
+ // --- //
+
+ // This classloader intercepts calls to loadClass() and redefines the test class in a new classloader
+ private static class IsolatingClassLoader extends ClassLoader {
+
+ private static final Logger logger = getLogger( IsolatingClassLoader.class.getName() );
+
+ public IsolatingClassLoader(ClassLoader parent) {
+ super( parent );
+ }
+
+ @Override
+ @SuppressWarnings( {"StringConcatenation", "HardcodedFileSeparator"} )
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if ( name.startsWith( TRANSACTION_TEST_PACKAGE_PREFIX ) && !name.contains( "$" ) ) {
+ Class> existingClass = findLoadedClass( name );
+ URL resourceURL = getResource( existingClass != null ? existingClass.getSimpleName() : name.replace( ".", "/" ) + ".class" );
+ if ( resourceURL != null ) {
+ try ( InputStream in = resourceURL.openStream() ) {
+ logger.info( "New NarayanaRedefiningClassloader for test class " + name );
+ return new NarayanaRedefiningClassloader( getParent() ).defineClass( name, in.readAllBytes() );
+ } catch ( IOException e ) {
+ throw new RuntimeException( e );
+ }
+ }
+ }
+ return super.loadClass( name, resolve );
+ }
+ }
+
+ // This classloader re-defines the test case (including inner classes) and all the Narayana classes
+ // Loading of other classes is delegated to the parent classloader
+ private static class NarayanaRedefiningClassloader extends ClassLoader {
+
+ public NarayanaRedefiningClassloader(ClassLoader parent) {
+ super( parent );
+ }
+
+ @Override
+ @SuppressWarnings( "HardcodedFileSeparator" )
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if ( name.startsWith( TRANSACTION_TEST_PACKAGE_PREFIX ) || name.startsWith( NARAYANA_PACKAGE_PREFIX ) ) {
+ Class> existingClass = findLoadedClass( name );
+ URL resourceURL = getResource( ( existingClass != null ) ? existingClass.getSimpleName() : name.replace( ".", "/" ) + ".class" );
+ if ( resourceURL != null ) {
+ try ( InputStream in = resourceURL.openStream() ) {
+ defineClass( name, in.readAllBytes() );
+ } catch ( IOException e ) {
+ throw new RuntimeException( e );
+ }
+ }
+ }
+ return super.loadClass( name, resolve );
+ }
+
+ public Class> defineClass(String name, byte[] data) {
+ return defineClass( name, data, 0, data.length );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/LifetimeTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/LifetimeTests.java
new file mode 100644
index 0000000..e988615
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/LifetimeTests.java
@@ -0,0 +1,237 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.concurrent.locks.LockSupport;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class LifetimeTests {
+
+ private static final Logger logger = getLogger( LifetimeTests.class.getName() );
+
+ private static final String FAKE_SCHEMA = "skeema";
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeSchemaConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Lifetime Test" )
+ void basicLifetimeTest() throws SQLException {
+ int MIN_POOL_SIZE = 40, MAX_POOL_SIZE = 100, MAX_LIFETIME_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .minSize( MIN_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .maxLifetime( ofMillis( MAX_LIFETIME_MS ) )
+ );
+
+ CountDownLatch allLatch = new CountDownLatch( MAX_POOL_SIZE );
+ CountDownLatch destroyLatch = new CountDownLatch( MAX_POOL_SIZE );
+ LongAdder flushCount = new LongAdder();
+
+ AgroalDataSourceListener listener = new MaxLifetimeListener( allLatch, flushCount, destroyLatch );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ try {
+ logger.info( format( "Awaiting creation of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ if ( !allLatch.await( MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created for maxLifetime", allLatch.getCount() ) );
+ }
+ assertEquals( MAX_POOL_SIZE, dataSource.getMetrics().creationCount(), "Unexpected number of connections on the pool" );
+
+ logger.info( format( "Waiting for removal of {0} connections ", MAX_POOL_SIZE ) );
+ if ( !destroyLatch.await( 2L * MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} old connections not sent for destruction", destroyLatch.getCount() ) );
+ }
+ assertEquals( MAX_POOL_SIZE, flushCount.longValue(), "Unexpected number of old connections" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Connection in use reaches maxLifetime" )
+ void inUseLifetimeTest() throws SQLException {
+ int MAX_LIFETIME_MS = 200;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .maxLifetime( ofMillis( MAX_LIFETIME_MS ) )
+ );
+
+ CountDownLatch allLatch = new CountDownLatch( 1 );
+ CountDownLatch destroyLatch = new CountDownLatch( 1 );
+ LongAdder flushCount = new LongAdder();
+
+ AgroalDataSourceListener listener = new MaxLifetimeListener( allLatch, flushCount, destroyLatch );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ try ( Connection connection = dataSource.getConnection() ) {
+ connection.getSchema();
+
+ logger.info( format( "Waiting for {0}ms (twice the maxLifetime)", 2 * MAX_LIFETIME_MS ) );
+ LockSupport.parkNanos( ofMillis( 2 * MAX_LIFETIME_MS ).toNanos() );
+
+ assertFalse( connection.isClosed() );
+ assertEquals( 1, dataSource.getMetrics().creationCount(), "Unexpected number of connections on the pool" );
+ assertEquals( 0, flushCount.longValue(), "Unexpected number of flushed connections" );
+ }
+
+ try {
+ logger.info( format( "Waiting for removal of {0} connections", 1 ) );
+ if ( !destroyLatch.await( MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} old connections not sent for destruction", destroyLatch.getCount() ) );
+ }
+ assertEquals( 1, flushCount.longValue(), "Unexpected number of old connections" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Min-size after maxLifetime" )
+ void minSizeLifetimeTest() throws SQLException {
+ int MIN_SIZE = 5, MAX_LIFETIME_MS = 200;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( 2 * MIN_SIZE )
+ .minSize( MIN_SIZE )
+ .maxSize( 10 * MIN_SIZE )
+ .maxLifetime( ofMillis( MAX_LIFETIME_MS ) )
+ );
+
+ CountDownLatch initialLatch = new CountDownLatch( 2 * MIN_SIZE );
+ CountDownLatch allLatch = new CountDownLatch( 3 * MIN_SIZE );
+ CountDownLatch destroyLatch = new CountDownLatch( 2 * MIN_SIZE );
+
+ MaxLifetimeListener initialListener = new MaxLifetimeListener( initialLatch, new LongAdder(), destroyLatch);
+ MaxLifetimeListener finalListener = new MaxLifetimeListener( allLatch, new LongAdder(), new CountDownLatch( 0 ) );
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, initialListener, finalListener ) ) {
+ logger.info( format( "Waiting at most {0}ms for the creating of {1} connections", MAX_LIFETIME_MS, MIN_SIZE ) );
+ if ( !initialLatch.await( MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created in time", destroyLatch.getCount() ) );
+ }
+ assertEquals( 2 * MIN_SIZE, dataSource.getMetrics().creationCount(), "Unexpected number of initial connections" );
+
+ logger.info( format( "Waiting for {0}ms (twice the maxLifetime)", 2 * MAX_LIFETIME_MS ) );
+ if ( !destroyLatch.await( 2 * MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not destroyed in time", destroyLatch.getCount() ) );
+ }
+ assertEquals( 2 * MIN_SIZE, dataSource.getMetrics().destroyCount(), "Unexpected number of flush connections" );
+
+ logger.info( format( "Waiting for {0}ms", MAX_LIFETIME_MS ) );
+ if ( !allLatch.await( MAX_LIFETIME_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created in time", allLatch.getCount() ) );
+ }
+ assertEquals( 3 * MIN_SIZE, dataSource.getMetrics().creationCount(), "Unexpected number of created connections" );
+ assertEquals( MIN_SIZE, dataSource.getMetrics().availableCount(), "Min-size not maintained" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ // --- //
+
+ private static class MaxLifetimeListener implements AgroalDataSourceListener {
+
+ private final CountDownLatch allLatch;
+ private final LongAdder flushCount;
+ private final CountDownLatch destroyLatch;
+
+ @SuppressWarnings( "WeakerAccess" )
+ MaxLifetimeListener(CountDownLatch allLatch, LongAdder flushCount, CountDownLatch destroyLatch) {
+ this.allLatch = allLatch;
+ this.flushCount = flushCount;
+ this.destroyLatch = destroyLatch;
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ allLatch.countDown();
+ }
+
+ @Override
+ public void onConnectionFlush(Connection connection) {
+ flushCount.increment();
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ destroyLatch.countDown();
+ }
+ }
+
+ // --- //
+
+ public static class FakeSchemaConnection implements MockConnection {
+
+ private boolean closed;
+
+ @Override
+ public String getSchema() throws SQLException {
+ return FAKE_SCHEMA;
+ }
+
+ @Override
+ public void close() throws SQLException {
+ if ( closed ) {
+ fail( "Double close on connection" );
+ } else {
+ closed = true;
+ }
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return closed;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockConnection.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockConnection.java
new file mode 100644
index 0000000..2d1f2fb
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockConnection.java
@@ -0,0 +1,294 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.SQLClientInfoException;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockConnection extends Connection {
+
+ @Override
+ default Statement createStatement() throws SQLException {
+ return new MockStatement.Empty();
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default CallableStatement prepareCall(String sql) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String nativeSQL(String sql) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean getAutoCommit() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void setAutoCommit(boolean autoCommit) throws SQLException {
+ }
+
+ @Override
+ default void commit() throws SQLException {
+ }
+
+ @Override
+ default void rollback() throws SQLException {
+ }
+
+ @Override
+ default void close() throws SQLException {
+ }
+
+ @Override
+ default boolean isClosed() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default DatabaseMetaData getMetaData() throws SQLException {
+ return new MockDatabaseMetaData.Empty();
+ }
+
+ @Override
+ default boolean isReadOnly() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void setReadOnly(boolean readOnly) throws SQLException {
+ }
+
+ @Override
+ default String getCatalog() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setCatalog(String catalog) throws SQLException {
+ }
+
+ @Override
+ default int getTransactionIsolation() throws SQLException {
+ return TRANSACTION_NONE;
+ }
+
+ @Override
+ default void setTransactionIsolation(int level) throws SQLException {
+ }
+
+ @Override
+ default SQLWarning getWarnings() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void clearWarnings() throws SQLException {
+ }
+
+ @Override
+ default Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
+ return new MockStatement.Empty();
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Map> getTypeMap() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setTypeMap(Map> map) throws SQLException {
+ }
+
+ @Override
+ default int getHoldability() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setHoldability(int holdability) throws SQLException {
+ }
+
+ @Override
+ default Savepoint setSavepoint() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Savepoint setSavepoint(String name) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void rollback(Savepoint savepoint) throws SQLException {
+ }
+
+ @Override
+ default void releaseSavepoint(Savepoint savepoint) throws SQLException {
+ }
+
+ @Override
+ default Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+ return new MockStatement.Empty();
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
+ return new MockPreparedStatement.Empty();
+ }
+
+ @Override
+ default Clob createClob() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Blob createBlob() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default NClob createNClob() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default SQLXML createSQLXML() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isValid(int timeout) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void setClientInfo(String name, String value) throws SQLClientInfoException {
+ }
+
+ @Override
+ default String getClientInfo(String name) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Properties getClientInfo() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setClientInfo(Properties properties) throws SQLClientInfoException {
+ }
+
+ @Override
+ default Array createArrayOf(String typeName, Object[] elements) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Struct createStruct(String typeName, Object[] attributes) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getSchema() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setSchema(String schema) throws SQLException {
+ }
+
+ @Override
+ default void abort(Executor executor) throws SQLException {
+ }
+
+ @Override
+ default void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
+ }
+
+ @Override
+ default int getNetworkTimeout() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default T unwrap(Class target) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isWrapperFor(Class> target) throws SQLException {
+ return false;
+ }
+
+ // --- //
+
+ class Empty implements MockConnection {
+
+ @Override
+ public String toString() {
+ return "MockConnection@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDataSource.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDataSource.java
new file mode 100644
index 0000000..100bce7
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDataSource.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import javax.sql.DataSource;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.logging.Logger;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockDataSource extends DataSource {
+
+ @Override
+ default Connection getConnection() throws SQLException {
+ return new MockConnection.Empty();
+ }
+
+ @Override
+ default Connection getConnection(String username, String password) throws SQLException {
+ return new MockConnection.Empty();
+ }
+
+ @Override
+ default T unwrap(Class iface) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default PrintWriter getLogWriter() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setLogWriter(PrintWriter out) throws SQLException {
+ }
+
+ @Override
+ default int getLoginTimeout() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setLoginTimeout(int seconds) throws SQLException {
+ }
+
+ @Override
+ default Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ return null;
+ }
+
+ // --- //
+
+ class Empty implements MockDataSource {
+
+ @Override
+ public String toString() {
+ return "MockDataSource@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDatabaseMetaData.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDatabaseMetaData.java
new file mode 100644
index 0000000..179a625
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDatabaseMetaData.java
@@ -0,0 +1,923 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.RowIdLifetime;
+import java.sql.SQLException;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockDatabaseMetaData extends DatabaseMetaData {
+
+ @Override
+ default boolean allProceduresAreCallable() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean allTablesAreSelectable() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getURL() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getUserName() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isReadOnly() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean nullsAreSortedHigh() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean nullsAreSortedLow() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean nullsAreSortedAtStart() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean nullsAreSortedAtEnd() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getDatabaseProductName() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getDatabaseProductVersion() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getDriverName() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getDriverVersion() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default int getDriverMajorVersion() {
+ return 0;
+ }
+
+ @Override
+ default int getDriverMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ default boolean usesLocalFiles() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean usesLocalFilePerTable() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMixedCaseIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesUpperCaseIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesLowerCaseIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesMixedCaseIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMixedCaseQuotedIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesUpperCaseQuotedIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesLowerCaseQuotedIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean storesMixedCaseQuotedIdentifiers() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getIdentifierQuoteString() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getSQLKeywords() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getNumericFunctions() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getStringFunctions() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getSystemFunctions() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getTimeDateFunctions() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getSearchStringEscape() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getExtraNameCharacters() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean supportsAlterTableWithAddColumn() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsAlterTableWithDropColumn() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsColumnAliasing() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean nullPlusNonNullIsNull() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsConvert() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsConvert(int fromType, int toType) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsTableCorrelationNames() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsDifferentTableCorrelationNames() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsExpressionsInOrderBy() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOrderByUnrelated() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsGroupBy() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsGroupByUnrelated() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsGroupByBeyondSelect() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsLikeEscapeClause() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMultipleResultSets() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMultipleTransactions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsNonNullableColumns() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMinimumSQLGrammar() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCoreSQLGrammar() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsExtendedSQLGrammar() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsANSI92EntryLevelSQL() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsANSI92IntermediateSQL() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsANSI92FullSQL() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsIntegrityEnhancementFacility() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOuterJoins() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsFullOuterJoins() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsLimitedOuterJoins() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getSchemaTerm() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getProcedureTerm() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getCatalogTerm() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isCatalogAtStart() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getCatalogSeparator() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean supportsSchemasInDataManipulation() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSchemasInProcedureCalls() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSchemasInTableDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSchemasInIndexDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSchemasInPrivilegeDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCatalogsInDataManipulation() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCatalogsInProcedureCalls() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCatalogsInTableDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCatalogsInIndexDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsPositionedDelete() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsPositionedUpdate() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSelectForUpdate() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsStoredProcedures() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSubqueriesInComparisons() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSubqueriesInExists() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSubqueriesInIns() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsSubqueriesInQuantifieds() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsCorrelatedSubqueries() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsUnion() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsUnionAll() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOpenCursorsAcrossCommit() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOpenCursorsAcrossRollback() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOpenStatementsAcrossCommit() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsOpenStatementsAcrossRollback() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getMaxBinaryLiteralLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxCharLiteralLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnsInGroupBy() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnsInIndex() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnsInOrderBy() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnsInSelect() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxColumnsInTable() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxConnections() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxCursorNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxIndexLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxSchemaNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxProcedureNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxCatalogNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxRowSize() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean doesMaxRowSizeIncludeBlobs() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getMaxStatementLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxStatements() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxTableNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxTablesInSelect() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getMaxUserNameLength() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getDefaultTransactionIsolation() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean supportsTransactions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsTransactionIsolationLevel(int level) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsDataManipulationTransactionsOnly() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean dataDefinitionCausesTransactionCommit() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean dataDefinitionIgnoredInTransactions() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getSchemas() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getCatalogs() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getTableTypes() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getTypeInfo() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default boolean supportsResultSetType(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean ownUpdatesAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean ownDeletesAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean ownInsertsAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean othersUpdatesAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean othersDeletesAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean othersInsertsAreVisible(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean updatesAreDetected(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean deletesAreDetected(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean insertsAreDetected(int type) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsBatchUpdates() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default Connection getConnection() throws SQLException {
+ return new MockConnection.Empty();
+ }
+
+ @Override
+ default boolean supportsSavepoints() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsNamedParameters() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsMultipleOpenResults() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsGetGeneratedKeys() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default boolean supportsResultSetHoldability(int holdability) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getResultSetHoldability() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getDatabaseMajorVersion() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getDatabaseMinorVersion() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getJDBCMajorVersion() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getJDBCMinorVersion() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int getSQLStateType() throws SQLException {
+ return sqlStateSQL;
+ }
+
+ @Override
+ default boolean locatorsUpdateCopy() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean supportsStatementPooling() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default RowIdLifetime getRowIdLifetime() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean autoCommitFailureClosesAllResultSets() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getClientInfoProperties() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default boolean generatedKeyAlwaysReturned() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default long getMaxLogicalLobSize() throws SQLException {
+ return DatabaseMetaData.super.getMaxLogicalLobSize();
+ }
+
+ @Override
+ default boolean supportsRefCursors() throws SQLException {
+ return DatabaseMetaData.super.supportsRefCursors();
+ }
+
+ @Override
+ default boolean supportsSharding() throws SQLException {
+ return DatabaseMetaData.super.supportsSharding();
+ }
+
+ @Override
+ default T unwrap(Class iface) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+
+ // --- //
+
+ class Empty implements MockDatabaseMetaData {
+
+ @Override
+ public String toString() {
+ return "MockDatabaseMetaDatat@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDriver.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDriver.java
new file mode 100644
index 0000000..68f9547
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockDriver.java
@@ -0,0 +1,104 @@
+package org.xbib.jdbc.pool.test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverPropertyInfo;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import static java.lang.System.identityHashCode;
+import static java.sql.DriverManager.deregisterDriver;
+import static java.sql.DriverManager.getDriver;
+import static java.sql.DriverManager.registerDriver;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+
+public interface MockDriver extends Driver {
+
+ DriverPropertyInfo[] EMPTY_PROPERTY_INFO = new DriverPropertyInfo[0];
+
+ static void registerMockDriver(Class extends Connection> connectionType) {
+ try {
+ registerDriver( new Empty( connectionType ) );
+ } catch ( SQLException e ) {
+ getLogger( MockDriver.class.getName() ).log( WARNING, "Unable to register MockDriver into Driver Manager", e );
+ }
+ }
+
+ static void registerMockDriver() {
+ registerMockDriver( MockConnection.Empty.class );
+ }
+
+ static void deregisterMockDriver() {
+ try {
+ deregisterDriver( getDriver( "" ) );
+ } catch ( SQLException e ) {
+ getLogger( MockDriver.class.getName() ).log( WARNING, "Unable to deregister MockDriver from Driver Manager", e );
+ }
+ }
+
+ @Override
+ default Connection connect(String url, Properties info) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean acceptsURL(String url) throws SQLException {
+ return true;
+ }
+
+ @Override
+ default DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
+ return EMPTY_PROPERTY_INFO;
+ }
+
+ @Override
+ default int getMajorVersion() {
+ return 0;
+ }
+
+ @Override
+ default int getMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ default boolean jdbcCompliant() {
+ return false;
+ }
+
+ @Override
+ default Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ return null;
+ }
+
+ class Empty implements MockDriver {
+
+ Class extends Connection> connectionType;
+
+ public Empty() {
+ this( MockConnection.Empty.class );
+ }
+
+ public Empty( Class extends Connection> connectionType ) {
+ this.connectionType = connectionType;
+ }
+
+ @Override
+ public Connection connect( String url, Properties info ) throws SQLException {
+ try {
+ return connectionType.getDeclaredConstructor().newInstance();
+ } catch ( InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e ) {
+ throw new SQLException( "Cannot create mock connection", e );
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MockDriver@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockPreparedStatement.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockPreparedStatement.java
new file mode 100644
index 0000000..0d9552b
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockPreparedStatement.java
@@ -0,0 +1,284 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLType;
+import java.sql.SQLXML;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockPreparedStatement extends MockStatement, PreparedStatement {
+
+ @Override
+ default ResultSet executeQuery() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default int executeUpdate() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setNull(int parameterIndex, int sqlType) throws SQLException {
+ }
+
+ @Override
+ default void setBoolean(int parameterIndex, boolean x) throws SQLException {
+ }
+
+ @Override
+ default void setByte(int parameterIndex, byte x) throws SQLException {
+ }
+
+ @Override
+ default void setShort(int parameterIndex, short x) throws SQLException {
+ }
+
+ @Override
+ default void setInt(int parameterIndex, int x) throws SQLException {
+ }
+
+ @Override
+ default void setLong(int parameterIndex, long x) throws SQLException {
+ }
+
+ @Override
+ default void setFloat(int parameterIndex, float x) throws SQLException {
+ }
+
+ @Override
+ default void setDouble(int parameterIndex, double x) throws SQLException {
+ }
+
+ @Override
+ default void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
+ }
+
+ @Override
+ default void setString(int parameterIndex, String x) throws SQLException {
+ }
+
+ @Override
+ default void setBytes(int parameterIndex, byte[] x) throws SQLException {
+ }
+
+ @Override
+ default void setDate(int parameterIndex, Date x) throws SQLException {
+ }
+
+ @Override
+ default void setTime(int parameterIndex, Time x) throws SQLException {
+ }
+
+ @Override
+ default void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
+ }
+
+ @Override
+ default void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void clearParameters() throws SQLException {
+ }
+
+ @Override
+ default void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
+ }
+
+ @Override
+ default void setObject(int parameterIndex, Object x) throws SQLException {
+ }
+
+ @Override
+ default boolean execute() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void addBatch() throws SQLException {
+ }
+
+ @Override
+ default void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
+ }
+
+ @Override
+ default void setRef(int parameterIndex, Ref x) throws SQLException {
+ }
+
+ @Override
+ default void setBlob(int parameterIndex, Blob x) throws SQLException {
+ }
+
+ @Override
+ default void setClob(int parameterIndex, Clob x) throws SQLException {
+ }
+
+ @Override
+ default void setArray(int parameterIndex, Array x) throws SQLException {
+ }
+
+ @Override
+ default ResultSetMetaData getMetaData() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
+ }
+
+ @Override
+ default void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
+ }
+
+ @Override
+ default void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
+ }
+
+ @Override
+ default void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
+ }
+
+ @Override
+ default void setURL(int parameterIndex, URL x) throws SQLException {
+ }
+
+ @Override
+ default ParameterMetaData getParameterMetaData() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setRowId(int parameterIndex, RowId x) throws SQLException {
+ }
+
+ @Override
+ default void setNString(int parameterIndex, String value) throws SQLException {
+ }
+
+ @Override
+ default void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
+ }
+
+ @Override
+ default void setNClob(int parameterIndex, NClob value) throws SQLException {
+ }
+
+ @Override
+ default void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
+ }
+
+ @Override
+ default void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
+ }
+
+ @Override
+ default void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {
+ }
+
+ @Override
+ default void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
+ }
+
+ @Override
+ default void setClob(int parameterIndex, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
+ }
+
+ @Override
+ default void setNClob(int parameterIndex, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException {
+ PreparedStatement.super.setObject( parameterIndex, x, targetSqlType, scaleOrLength );
+ }
+
+ @Override
+ default void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException {
+ PreparedStatement.super.setObject( parameterIndex, x, targetSqlType );
+ }
+
+ @Override
+ default long executeLargeUpdate() throws SQLException {
+ return PreparedStatement.super.executeLargeUpdate();
+ }
+
+// --- //
+
+ class Empty implements MockPreparedStatement {
+
+ @Override
+ public String toString() {
+ return "MockPreparedStatement@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockResultSet.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockResultSet.java
new file mode 100644
index 0000000..c541222
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockResultSet.java
@@ -0,0 +1,919 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+@SuppressWarnings( "InterfaceWithOnlyOneDirectInheritor" )
+public interface MockResultSet extends ResultSet {
+
+ byte[] BYTES = new byte[0];
+
+ // --- //
+
+ @Override
+ default boolean next() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void close() throws SQLException {
+ }
+
+ @Override
+ default boolean wasNull() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default String getString(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean getBoolean(int columnIndex) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default byte getByte(int columnIndex) throws SQLException {
+ return (byte) 0;
+ }
+
+ @Override
+ default short getShort(int columnIndex) throws SQLException {
+ return (short) 0;
+ }
+
+ @Override
+ default int getInt(int columnIndex) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long getLong(int columnIndex) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default float getFloat(int columnIndex) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default double getDouble(int columnIndex) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ @Deprecated
+ default BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default byte[] getBytes(int columnIndex) throws SQLException {
+ return BYTES;
+ }
+
+ @Override
+ default Date getDate(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Time getTime(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Timestamp getTimestamp(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default InputStream getAsciiStream(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ @Deprecated
+ default InputStream getUnicodeStream(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default InputStream getBinaryStream(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getString(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean getBoolean(String columnLabel) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default byte getByte(String columnLabel) throws SQLException {
+ return (byte) 0;
+ }
+
+ @Override
+ default short getShort(String columnLabel) throws SQLException {
+ return (short) 0;
+ }
+
+ @Override
+ default int getInt(String columnLabel) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long getLong(String columnLabel) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default float getFloat(String columnLabel) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default double getDouble(String columnLabel) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ @Deprecated
+ default BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default byte[] getBytes(String columnLabel) throws SQLException {
+ return BYTES;
+ }
+
+ @Override
+ default Date getDate(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Time getTime(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Timestamp getTimestamp(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default InputStream getAsciiStream(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ @Deprecated
+ default InputStream getUnicodeStream(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default InputStream getBinaryStream(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default SQLWarning getWarnings() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void clearWarnings() throws SQLException {
+
+ }
+
+ @Override
+ default String getCursorName() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default ResultSetMetaData getMetaData() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Object getObject(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Object getObject(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default int findColumn(String columnLabel) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default Reader getCharacterStream(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Reader getCharacterStream(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default BigDecimal getBigDecimal(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default BigDecimal getBigDecimal(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isBeforeFirst() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean isAfterLast() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean isFirst() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean isLast() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void beforeFirst() throws SQLException {
+
+ }
+
+ @Override
+ default void afterLast() throws SQLException {
+
+ }
+
+ @Override
+ default boolean first() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean last() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getRow() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean absolute(int row) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean relative(int rows) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean previous() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getFetchDirection() throws SQLException {
+ return FETCH_FORWARD;
+ }
+
+ @Override
+ default void setFetchDirection(int direction) throws SQLException {
+
+ }
+
+ @Override
+ default int getFetchSize() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setFetchSize(int rows) throws SQLException {
+
+ }
+
+ @Override
+ default int getType() throws SQLException {
+ return TYPE_FORWARD_ONLY;
+ }
+
+ @Override
+ default int getConcurrency() throws SQLException {
+ return CONCUR_READ_ONLY;
+ }
+
+ @Override
+ default boolean rowUpdated() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean rowInserted() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean rowDeleted() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void updateNull(int columnIndex) throws SQLException {
+ }
+
+ @Override
+ default void updateBoolean(int columnIndex, boolean x) throws SQLException {
+ }
+
+ @Override
+ default void updateByte(int columnIndex, byte x) throws SQLException {
+ }
+
+ @Override
+ default void updateShort(int columnIndex, short x) throws SQLException {
+ }
+
+ @Override
+ default void updateInt(int columnIndex, int x) throws SQLException {
+ }
+
+ @Override
+ default void updateLong(int columnIndex, long x) throws SQLException {
+ }
+
+ @Override
+ default void updateFloat(int columnIndex, float x) throws SQLException {
+ }
+
+ @Override
+ default void updateDouble(int columnIndex, double x) throws SQLException {
+ }
+
+ @Override
+ default void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException {
+ }
+
+ @Override
+ default void updateString(int columnIndex, String x) throws SQLException {
+ }
+
+ @Override
+ default void updateBytes(int columnIndex, byte[] x) throws SQLException {
+ }
+
+ @Override
+ default void updateDate(int columnIndex, Date x) throws SQLException {
+ }
+
+ @Override
+ default void updateTime(int columnIndex, Time x) throws SQLException {
+ }
+
+ @Override
+ default void updateTimestamp(int columnIndex, Timestamp x) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException {
+ }
+
+ @Override
+ default void updateObject(int columnIndex, Object x) throws SQLException {
+ }
+
+ @Override
+ default void updateNull(String columnLabel) throws SQLException {
+ }
+
+ @Override
+ default void updateBoolean(String columnLabel, boolean x) throws SQLException {
+ }
+
+ @Override
+ default void updateByte(String columnLabel, byte x) throws SQLException {
+ }
+
+ @Override
+ default void updateShort(String columnLabel, short x) throws SQLException {
+ }
+
+ @Override
+ default void updateInt(String columnLabel, int x) throws SQLException {
+
+ }
+
+ @Override
+ default void updateLong(String columnLabel, long x) throws SQLException {
+ }
+
+ @Override
+ default void updateFloat(String columnLabel, float x) throws SQLException {
+ }
+
+ @Override
+ default void updateDouble(String columnLabel, double x) throws SQLException {
+ }
+
+ @Override
+ default void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException {
+ }
+
+ @Override
+ default void updateString(String columnLabel, String x) throws SQLException {
+ }
+
+ @Override
+ default void updateBytes(String columnLabel, byte[] x) throws SQLException {
+ }
+
+ @Override
+ default void updateDate(String columnLabel, Date x) throws SQLException {
+ }
+
+ @Override
+ default void updateTime(String columnLabel, Time x) throws SQLException {
+ }
+
+ @Override
+ default void updateTimestamp(String columnLabel, Timestamp x) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException {
+ }
+
+ @Override
+ default void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException {
+ }
+
+ @Override
+ default void updateObject(String columnLabel, Object x) throws SQLException {
+ }
+
+ @Override
+ default void insertRow() throws SQLException {
+ }
+
+ @Override
+ default void updateRow() throws SQLException {
+ }
+
+ @Override
+ default void deleteRow() throws SQLException {
+ }
+
+ @Override
+ default void refreshRow() throws SQLException {
+ }
+
+ @Override
+ default void cancelRowUpdates() throws SQLException {
+ }
+
+ @Override
+ default void moveToInsertRow() throws SQLException {
+ }
+
+ @Override
+ default void moveToCurrentRow() throws SQLException {
+ }
+
+ @Override
+ default Statement getStatement() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Object getObject(int columnIndex, Map> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Ref getRef(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Blob getBlob(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Clob getClob(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Array getArray(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Object getObject(String columnLabel, Map> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Ref getRef(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Blob getBlob(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Clob getClob(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Array getArray(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Date getDate(int columnIndex, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Date getDate(String columnLabel, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Time getTime(int columnIndex, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Time getTime(String columnLabel, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default URL getURL(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default URL getURL(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void updateRef(int columnIndex, Ref x) throws SQLException {
+ }
+
+ @Override
+ default void updateRef(String columnLabel, Ref x) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(int columnIndex, Blob x) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(String columnLabel, Blob x) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(int columnIndex, Clob x) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(String columnLabel, Clob x) throws SQLException {
+ }
+
+ @Override
+ default void updateArray(int columnIndex, Array x) throws SQLException {
+ }
+
+ @Override
+ default void updateArray(String columnLabel, Array x) throws SQLException {
+ }
+
+ @Override
+ default RowId getRowId(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default RowId getRowId(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void updateRowId(int columnIndex, RowId x) throws SQLException {
+ }
+
+ @Override
+ default void updateRowId(String columnLabel, RowId x) throws SQLException {
+ }
+
+ @Override
+ default int getHoldability() throws SQLException {
+ return CLOSE_CURSORS_AT_COMMIT;
+ }
+
+ @Override
+ default boolean isClosed() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void updateNString(int columnIndex, String nString) throws SQLException {
+ }
+
+ @Override
+ default void updateNString(String columnLabel, String nString) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(int columnIndex, NClob nClob) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(String columnLabel, NClob nClob) throws SQLException {
+ }
+
+ @Override
+ default NClob getNClob(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default NClob getNClob(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default SQLXML getSQLXML(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default SQLXML getSQLXML(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException {
+ }
+
+ @Override
+ default void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException {
+ }
+
+ @Override
+ default String getNString(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default String getNString(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Reader getNCharacterStream(int columnIndex) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default Reader getNCharacterStream(String columnLabel) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(int columnIndex, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(String columnLabel, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(int columnIndex, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(String columnLabel, Reader reader, long length) throws SQLException {
+ }
+
+ @Override
+ default void updateNCharacterStream(int columnIndex, Reader x) throws SQLException {
+ }
+
+ @Override
+ default void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(int columnIndex, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(int columnIndex, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(int columnIndex, Reader x) throws SQLException {
+ }
+
+ @Override
+ default void updateAsciiStream(String columnLabel, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void updateBinaryStream(String columnLabel, InputStream x) throws SQLException {
+ }
+
+ @Override
+ default void updateCharacterStream(String columnLabel, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(int columnIndex, InputStream inputStream) throws SQLException {
+ }
+
+ @Override
+ default void updateBlob(String columnLabel, InputStream inputStream) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(int columnIndex, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void updateClob(String columnLabel, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(int columnIndex, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default void updateNClob(String columnLabel, Reader reader) throws SQLException {
+ }
+
+ @Override
+ default T getObject(int columnIndex, Class type) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default T getObject(String columnLabel, Class type) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default T unwrap(Class iface) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+
+ // --- //
+
+ class Empty implements MockResultSet {
+
+ @Override
+ public String toString() {
+ return "MockResultSet@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockStatement.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockStatement.java
new file mode 100644
index 0000000..56fb5c2
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockStatement.java
@@ -0,0 +1,281 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockStatement extends Statement {
+
+ long[] LARGE_BATCH = new long[0];
+ int[] BATCH = new int[0];
+
+ // --- //
+
+ @Override
+ default long getLargeUpdateCount() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long getLargeMaxRows() throws SQLException {
+ return Statement.super.getLargeMaxRows();
+ }
+
+ @Override
+ default void setLargeMaxRows(long max) throws SQLException {
+ }
+
+ @Override
+ default long[] executeLargeBatch() throws SQLException {
+ return LARGE_BATCH;
+ }
+
+ @Override
+ default long executeLargeUpdate(String sql) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default ResultSet executeQuery(String sql) throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default int executeUpdate(String sql) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void close() throws SQLException {
+
+ }
+
+ @Override
+ default int getMaxFieldSize() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setMaxFieldSize(int max) throws SQLException {
+ }
+
+ @Override
+ default int getMaxRows() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setMaxRows(int max) throws SQLException {
+ }
+
+ @Override
+ default void setEscapeProcessing(boolean enable) throws SQLException {
+ }
+
+ @Override
+ default int getQueryTimeout() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setQueryTimeout(int seconds) throws SQLException {
+ }
+
+ @Override
+ default void cancel() throws SQLException {
+ }
+
+ @Override
+ default SQLWarning getWarnings() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void clearWarnings() throws SQLException {
+ }
+
+ @Override
+ default void setCursorName(String name) throws SQLException {
+ }
+
+ @Override
+ default boolean execute(String sql) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getResultSet() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default int getUpdateCount() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean getMoreResults() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getFetchDirection() throws SQLException {
+ return ResultSet.FETCH_FORWARD;
+ }
+
+ @Override
+ default void setFetchDirection(int direction) throws SQLException {
+
+ }
+
+ @Override
+ default int getFetchSize() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setFetchSize(int rows) throws SQLException {
+
+ }
+
+ @Override
+ default int getResultSetConcurrency() throws SQLException {
+ return ResultSet.CONCUR_READ_ONLY;
+ }
+
+ @Override
+ default int getResultSetType() throws SQLException {
+ return ResultSet.TYPE_FORWARD_ONLY;
+ }
+
+ @Override
+ default void addBatch(String sql) throws SQLException {
+ }
+
+ @Override
+ default void clearBatch() throws SQLException {
+ }
+
+ @Override
+ default int[] executeBatch() throws SQLException {
+ return BATCH;
+ }
+
+ @Override
+ default Connection getConnection() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean getMoreResults(int current) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default ResultSet getGeneratedKeys() throws SQLException {
+ return new MockResultSet.Empty();
+ }
+
+ @Override
+ default int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default int executeUpdate(String sql, String[] columnNames) throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean execute(String sql, int[] columnIndexes) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean execute(String sql, String[] columnNames) throws SQLException {
+ return false;
+ }
+
+ @Override
+ default int getResultSetHoldability() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default boolean isClosed() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default boolean isPoolable() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default void setPoolable(boolean poolable) throws SQLException {
+ }
+
+ @Override
+ default void closeOnCompletion() throws SQLException {
+ }
+
+ @Override
+ default boolean isCloseOnCompletion() throws SQLException {
+ return false;
+ }
+
+ @Override
+ default T unwrap(Class iface) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+
+ // --- //
+
+ class Empty implements MockStatement {
+
+ @Override
+ public String toString() {
+ return "MockStatement@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAConnection.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAConnection.java
new file mode 100644
index 0000000..fb21ff1
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAConnection.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import javax.sql.ConnectionEventListener;
+import javax.sql.StatementEventListener;
+import javax.sql.XAConnection;
+import javax.transaction.xa.XAResource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockXAConnection extends XAConnection {
+
+ @Override
+ default XAResource getXAResource() throws SQLException {
+ return new MockXAResource.Empty();
+ }
+
+ @Override
+ default Connection getConnection() throws SQLException {
+ return new MockConnection.Empty();
+ }
+
+ @Override
+ default void close() throws SQLException {
+ }
+
+ @Override
+ default void addConnectionEventListener(ConnectionEventListener listener) {
+ }
+
+ @Override
+ default void removeConnectionEventListener(ConnectionEventListener listener) {
+ }
+
+ @Override
+ default void addStatementEventListener(StatementEventListener listener) {
+ }
+
+ @Override
+ default void removeStatementEventListener(StatementEventListener listener) {
+ }
+
+ // --- //
+
+ class Empty implements MockXAConnection {
+
+ @Override
+ public String toString() {
+ return "MockXAConnection@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXADataSource.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXADataSource.java
new file mode 100644
index 0000000..218e5b9
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXADataSource.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import javax.sql.XAConnection;
+import javax.sql.XADataSource;
+import java.io.PrintWriter;
+import java.sql.SQLException;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+public interface MockXADataSource extends MockDataSource, XADataSource {
+
+ @Override
+ default XAConnection getXAConnection() throws SQLException {
+ return new MockXAConnection.Empty();
+ }
+
+ @Override
+ default XAConnection getXAConnection(String user, String password) throws SQLException {
+ return null;
+ }
+
+ @Override
+ default PrintWriter getLogWriter() throws SQLException {
+ return null;
+ }
+
+ @Override
+ default void setLogWriter(PrintWriter out) throws SQLException {
+ }
+
+ @Override
+ default int getLoginTimeout() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ default void setLoginTimeout(int seconds) throws SQLException {
+ }
+
+ // --- //
+
+ class Empty implements MockXADataSource {
+
+ @Override
+ public String toString() {
+ return "MockXADataSource@" + identityHashCode( this );
+ }
+ }
+
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAResource.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAResource.java
new file mode 100644
index 0000000..a9e9937
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/MockXAResource.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+import static java.lang.System.identityHashCode;
+
+/**
+ * @author Luis Barreiro
+ */
+@SuppressWarnings( "InterfaceWithOnlyOneDirectInheritor" )
+public interface MockXAResource extends XAResource {
+
+ @Override
+ default void commit(Xid xid, boolean b) throws XAException {
+ }
+
+ @Override
+ default void end(Xid xid, int i) throws XAException {
+ }
+
+ @Override
+ default void forget(Xid xid) throws XAException {
+ }
+
+ @Override
+ default int getTransactionTimeout() throws XAException {
+ return 0;
+ }
+
+ @Override
+ default boolean isSameRM(XAResource xaResource) throws XAException {
+ return false;
+ }
+
+ @Override
+ default int prepare(Xid xid) throws XAException {
+ return 0;
+ }
+
+ @Override
+ default Xid[] recover(int i) throws XAException {
+ return null;
+ }
+
+ @Override
+ default void rollback(Xid xid) throws XAException {
+ }
+
+ @Override
+ default boolean setTransactionTimeout(int i) throws XAException {
+ return false;
+ }
+
+ @Override
+ default void start(Xid xid, int i) throws XAException {
+ }
+
+ // --- //
+
+ class Empty implements MockXAResource {
+
+ @Override
+ public String toString() {
+ return "MockXAResource@" + identityHashCode( this );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/NewConnectionTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/NewConnectionTests.java
new file mode 100644
index 0000000..687c357
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/NewConnectionTests.java
@@ -0,0 +1,302 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.NONE;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.READ_COMMITTED;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.READ_UNCOMMITTED;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.REPEATABLE_READ;
+import static io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation.SERIALIZABLE;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class NewConnectionTests {
+
+ static final Logger logger = getLogger( NewConnectionTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( FakeConnection.class );
+ if ( Utils.isWindowsOS() ) {
+ Utils.windowsTimerHack();
+ }
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test connection isolation" )
+ void isolationTest() throws SQLException {
+ isolation( NONE, Connection.TRANSACTION_NONE );
+ isolation( READ_UNCOMMITTED, Connection.TRANSACTION_READ_UNCOMMITTED );
+ isolation( READ_COMMITTED, Connection.TRANSACTION_READ_COMMITTED );
+ isolation( REPEATABLE_READ, Connection.TRANSACTION_REPEATABLE_READ );
+ isolation( SERIALIZABLE, Connection.TRANSACTION_SERIALIZABLE );
+ }
+
+ private static void isolation(AgroalConnectionFactoryConfiguration.TransactionIsolation isolation, int level) throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.jdbcTransactionIsolation( isolation ) ) ) ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals( connection.getTransactionIsolation(), level );
+ logger.info( format( "Got isolation \"{0}\" from {1}", connection.getTransactionIsolation(), connection ) );
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "Test connection autoCommit status" )
+ void autoCommitTest() throws SQLException {
+ autocommit( false );
+ autocommit( true );
+ }
+
+ private static void autocommit(boolean autoCommit) throws SQLException {
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( new AgroalDataSourceConfigurationSupplier().connectionPoolConfiguration( cp -> cp.maxSize( 1 ).connectionFactoryConfiguration( cf -> cf.autoCommit( autoCommit ) ) ) ) ) {
+ Connection connection = dataSource.getConnection();
+ assertEquals( connection.getAutoCommit(), autoCommit );
+ logger.info( format( "Got autoCommit \"{0}\" from {1}", connection.getAutoCommit(), connection ) );
+ connection.close();
+ }
+ }
+
+ @Test
+ @DisplayName( "Test faulty URL setter" )
+ void faultyUrlTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( FaultyUrlDataSource.class )
+ .jdbcUrl( "the_url" )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new NoWarningsAgroalListener() ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ logger.info( "Got connection " + c + " with some URL" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Properties injection test" )
+ void propertiesInjectionTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( PropertiesDataSource.class )
+ .jdbcProperty( "connectionProperties", "url=some_url;custom=some_custom_prop" )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new NoWarningsAgroalListener() ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ logger.info( "Got connection " + c + " with some URL" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Multiple methods injection test" )
+ void multipleMethodsInjectionTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( MultipleSettersDataSource.class )
+ .jdbcProperty( "someString", "some_value" )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new NoWarningsAgroalListener() ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ logger.info( "Got connection " + c + " with some someString set" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Exception on new connection" )
+ void newConnectionExceptionTest() throws SQLException {
+ int INITIAL_SIZE = 3, INITIAL_TIMEOUT_MS = 100 * INITIAL_SIZE;
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .initialSize( INITIAL_SIZE )
+ .connectionFactoryConfiguration( cf -> cf
+ .credential( new Object() )
+ .addSecurityProvider( new ExceptionSecurityProvider() )
+ )
+ );
+
+ WarningsAgroalListener warningsListener = new WarningsAgroalListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, warningsListener ) ) {
+ Thread.sleep( INITIAL_TIMEOUT_MS );
+
+ assertEquals( 0, dataSource.getMetrics().creationCount() );
+ assertEquals( INITIAL_SIZE, warningsListener.warningCount(), "Expected warning(s)" );
+ } catch ( InterruptedException e ) {
+ fail( "Interrupt " + e );
+ }
+ }
+
+ // --- //
+
+ public static class WarningsAgroalListener implements AgroalDataSourceListener {
+
+ private final AtomicInteger warnings = new AtomicInteger();
+
+ @Override
+ public void onWarning(String message) {
+ warnings.getAndIncrement();
+ logger.info( "Expected WARN: " + message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ warnings.getAndIncrement();
+ logger.info( "Expected WARN" + throwable.getMessage() );
+ }
+
+ public int warningCount() {
+ return warnings.get();
+ }
+ }
+
+ public static class NoWarningsAgroalListener implements AgroalDataSourceListener {
+
+ @Override
+ public void onWarning(String message) {
+ fail( "Unexpected warning " + message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ fail( "Unexpected warning " + throwable.getMessage() );
+ }
+ }
+
+ public static class FaultyUrlDataSource implements MockDataSource {
+
+ private String url;
+
+ public void setURL(String url) {
+ this.url = url;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ assertNotNull( url, "Expected URL to be set before getConnection()" );
+ return new MockConnection.Empty();
+ }
+ }
+
+ public static class PropertiesDataSource implements MockDataSource {
+
+ private Properties connectionProperties;
+
+ public void setConnectionProperties(Properties properties) {
+ connectionProperties = properties;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ assertEquals( "some_url", connectionProperties.getProperty( "url" ), "Expected URL property to be set before getConnection()" );
+ assertEquals( "some_custom_prop", connectionProperties.getProperty( "custom" ), "Expected Custom property to be set before getConnection()" );
+ assertNull( connectionProperties.getProperty( "connectionProperties" ), "Not expecting property to be set before getConnection()" );
+ return new MockConnection.Empty();
+ }
+ }
+
+ public static class MultipleSettersDataSource implements MockDataSource {
+
+ private String some = "default";
+
+ @Deprecated
+ public void setSomeString(String ignore) {
+ some = "string_method";
+ }
+
+ public void setSomeString(char[] chars) {
+ some = new String( chars );
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ assertEquals( "some_value", some, "Expected property to be set before getConnection()" );
+ return new MockConnection.Empty();
+ }
+ }
+
+ public static class ExceptionSecurityProvider implements AgroalSecurityProvider {
+
+ @Override
+ public Properties getSecurityProperties(Object securityObject) {
+ throw new RuntimeException( "SecurityProvider throws!" );
+ }
+ }
+
+ public static class FakeConnection implements MockConnection {
+
+ private int isolation;
+ private boolean autoCommit;
+
+ @Override
+ public int getTransactionIsolation() {
+ return isolation;
+ }
+
+ @Override
+ public void setTransactionIsolation(int level) {
+ isolation = level;
+ }
+
+ @Override
+ public boolean getAutoCommit() {
+ return autoCommit;
+ }
+
+ @Override
+ public void setAutoCommit(boolean autoCommit) {
+ this.autoCommit = autoCommit;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PoollessTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PoollessTests.java
new file mode 100644
index 0000000..f616407
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PoollessTests.java
@@ -0,0 +1,211 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Logger;
+
+import static io.agroal.api.configuration.AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL_POOLLESS;
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.lang.Thread.currentThread;
+import static java.text.MessageFormat.format;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+class PoollessTests {
+
+ static final Logger logger = getLogger( PoollessTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver();
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Pool-less Test" )
+ @SuppressWarnings( {"AnonymousInnerClassMayBeStatic", "ObjectAllocationInLoop", "JDBCResourceOpenedButNotSafelyClosed"} )
+ void poollessTest() throws SQLException {
+ int TIMEOUT_MS = 100, NUM_THREADS = 4;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( AGROAL_POOLLESS )
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( 1 ) // ignored
+ .minSize( 1 ) // ignored
+ .maxSize( 2 )
+ .acquisitionTimeout( Duration.ofMillis( 5 * TIMEOUT_MS ) )
+ );
+
+ CountDownLatch destroyLatch = new CountDownLatch( 1 );
+
+ AgroalDataSourceListener listener = new AgroalDataSourceListener() {
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ destroyLatch.countDown();
+ }
+
+ @Override
+ public void onWarning(String message) {
+ logger.info( message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ fail( throwable );
+ }
+ };
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ assertEquals( 0, dataSource.getMetrics().creationCount() );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertFalse( c.isClosed() );
+
+ try ( Connection testSubject = dataSource.getConnection() ) {
+ assertFalse( testSubject.isClosed() );
+
+ assertEquals( 2, dataSource.getMetrics().creationCount() );
+ assertThrows( SQLException.class, dataSource::getConnection, "Expected exception due to pool being full" );
+ assertEquals( 2, dataSource.getMetrics().creationCount() );
+ }
+
+ logger.info( format( "Waiting for destruction of connection" ) );
+ if ( !destroyLatch.await( 2 * TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "Flushed connections not sent for destruction" ) );
+ }
+
+ // One connection flushed and another in use
+ assertEquals( 1, dataSource.getMetrics().flushCount() );
+ assertEquals( 1, dataSource.getMetrics().activeCount() );
+ assertEquals( 1, dataSource.getMetrics().availableCount() );
+ }
+
+ // Assert min-size is zero
+ assertEquals( 0, dataSource.getMetrics().activeCount() );
+ assertEquals( 2, dataSource.getMetrics().availableCount() );
+ assertEquals( 2, dataSource.getMetrics().flushCount() );
+
+ // Assert that closing a connection unblocks one waiting thread
+ assertDoesNotThrow( () -> {
+ Connection c = dataSource.getConnection();
+ dataSource.getConnection();
+
+ // just one of this threads will unblock
+ Collection threads = new ArrayList<>( NUM_THREADS );
+ for ( int i = 0; i < NUM_THREADS; i++ ) {
+ threads.add( newConnectionThread( dataSource ) );
+ }
+ threads.forEach( Thread::start );
+
+ try {
+ Thread.sleep( TIMEOUT_MS );
+ assertEquals( 4, dataSource.getMetrics().awaitingCount(), "Insufficient number of blocked threads" );
+ assertEquals( 4, dataSource.getMetrics().creationCount() );
+
+ logger.info( "Closing connection to unblock one waiting thread" );
+ c.close();
+
+ Thread.sleep( TIMEOUT_MS );
+
+ assertEquals( 3, dataSource.getMetrics().awaitingCount(), "Insufficient number of blocked threads" );
+ assertEquals( 5, dataSource.getMetrics().creationCount() );
+
+ for ( Thread thread : threads ) {
+ thread.join( TIMEOUT_MS );
+ }
+ } catch ( InterruptedException e ) {
+ fail( e );
+ }
+ } );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+
+ try {
+ Thread.sleep( TIMEOUT_MS );
+ } catch ( InterruptedException e ) {
+ //
+ }
+ }
+
+ private static Thread newConnectionThread(DataSource dataSource) {
+ return new Thread( () -> {
+ logger.info( currentThread().getName() + " is on the race for a connection" );
+ try {
+ Connection c = dataSource.getConnection();
+ assertFalse( c.isClosed() );
+ logger.info( currentThread().getName() + " got one connection !!!" );
+ } catch ( SQLException e ) {
+ logger.info( currentThread().getName() + " got none" );
+ }
+ } );
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Exception on create connection" )
+ void createExceptionTest() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( AGROAL_POOLLESS )
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( ExceptionalDataSource.class )
+ ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ fail( "Got connection " + c );
+ } catch ( SQLException e ) {
+ // test for AG-194 --- active count was incremented incorrectly
+ assertEquals( 0, dataSource.getMetrics().activeCount(), "Active count incremented after exception" );
+ }
+ }
+ }
+
+ public static class ExceptionalDataSource extends MockDataSource.Empty {
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ throw new SQLException( "Exceptional condition" );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PropertiesReaderTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PropertiesReaderTests.java
new file mode 100644
index 0000000..9f72fa4
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/PropertiesReaderTests.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration;
+import io.agroal.api.configuration.AgroalConnectionPoolConfiguration;
+import io.agroal.api.configuration.AgroalDataSourceConfiguration;
+import io.agroal.api.configuration.supplier.AgroalPropertiesReader;
+import io.agroal.api.exceptionsorter.PostgreSQLExceptionSorter;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Connection;
+import java.time.LocalTime;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static java.util.logging.Logger.getLogger;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+class PropertiesReaderTests {
+
+ private static final Logger logger = getLogger( PropertiesReaderTests.class.getName() );
+
+ private static final Path basePath = Paths.get( "src", "test", "resources", "PropertiesReaderTests" );
+
+ // --- //
+
+ @Test
+ @DisplayName( "Properties File" )
+ void basicPropertiesReaderTest() throws IOException {
+ AgroalDataSourceConfiguration configuration = new AgroalPropertiesReader().readProperties( basePath.resolve( "agroal.properties" ) ).get();
+
+ logger.info( configuration.toString() );
+
+ // Not an exhaustive test, just a couple properties
+ Assertions.assertEquals( 1, configuration.connectionPoolConfiguration().acquisitionTimeout().getSeconds() );
+ Assertions.assertEquals( 60, configuration.connectionPoolConfiguration().validationTimeout().getSeconds() );
+ Assertions.assertEquals( AgroalConnectionFactoryConfiguration.TransactionIsolation.SERIALIZABLE, configuration.connectionPoolConfiguration().connectionFactoryConfiguration().jdbcTransactionIsolation() );
+ Assertions.assertInstanceOf( OddHoursConnectionValidator.class, configuration.connectionPoolConfiguration().connectionValidator() );
+ Assertions.assertInstanceOf( PostgreSQLExceptionSorter.class, configuration.connectionPoolConfiguration().exceptionSorter() );
+ }
+
+ // --- //
+
+ /**
+ * Silly validator that drop connections on odd hours
+ */
+ public static class OddHoursConnectionValidator implements AgroalConnectionPoolConfiguration.ConnectionValidator {
+
+ @Override
+ public boolean isValid(Connection connection) {
+ return LocalTime.now().getHour() % 2 == 0;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ResizeTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ResizeTests.java
new file mode 100644
index 0000000..5b6b400
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ResizeTests.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.cache.LocalConnectionCache;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.lang.Integer.max;
+import static java.text.MessageFormat.format;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class ResizeTests {
+
+ private static final Logger logger = getLogger( ResizeTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver();
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @SuppressWarnings( "AnonymousInnerClassMayBeStatic" )
+ @Test
+ @DisplayName( "resize Max" )
+ void resizeMax() throws SQLException {
+ int INITIAL_SIZE = 10, MAX_SIZE = 6, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( INITIAL_SIZE )
+ .maxSize( MAX_SIZE )
+ );
+
+ CountDownLatch creationLatch = new CountDownLatch( INITIAL_SIZE );
+ CountDownLatch destroyLatch = new CountDownLatch( INITIAL_SIZE - MAX_SIZE );
+ AgroalDataSourceListener listener = new AgroalDataSourceListener() {
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ creationLatch.countDown();
+ }
+
+ @Override
+ public void onConnectionDestroy(Connection connection) {
+ destroyLatch.countDown();
+ }
+ };
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ logger.info( format( "Awaiting fill of all the {0} initial connections on the pool", INITIAL_SIZE ) );
+ if ( !creationLatch.await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", creationLatch.getCount() ) );
+ }
+ assertEquals( INITIAL_SIZE, dataSource.getMetrics().availableCount(), "Pool not initialized correctly" );
+
+ for ( int i = INITIAL_SIZE; i > 0; i-- ) {
+ assertEquals( max( MAX_SIZE, i ), dataSource.getMetrics().availableCount(), "Pool not resized" );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertNotNull( c );
+ }
+ }
+
+ logger.info( format( "Waiting for destruction of {0} connections ", INITIAL_SIZE - MAX_SIZE ) );
+ if ( !destroyLatch.await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} flushed connections not sent for destruction", destroyLatch.getCount() ) );
+ }
+
+ assertEquals( MAX_SIZE, dataSource.getMetrics().availableCount(), "Pool not resized" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "resize Min" )
+ void resizeMin() throws SQLException {
+ int INITIAL_SIZE = 10, NEW_MIN_SIZE = 15, MAX_SIZE = 35, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .connectionCache( LocalConnectionCache.single() ) // this test expects thread local cache
+ .maxSize( MAX_SIZE )
+ .initialSize( INITIAL_SIZE )
+ );
+
+ CountDownLatch creationLatch = new CountDownLatch( INITIAL_SIZE );
+ ReadyDataSourceListener listener = new ReadyDataSourceListener( creationLatch );
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+ logger.info( format( "Awaiting fill of all the {0} initial connections on the pool", INITIAL_SIZE ) );
+ if ( !creationLatch.await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} connections not created", creationLatch.getCount() ) );
+ }
+
+ assertEquals( INITIAL_SIZE, dataSource.getMetrics().availableCount(), "Pool not initialized correctly" );
+ dataSource.getConfiguration().connectionPoolConfiguration().setMinSize( NEW_MIN_SIZE );
+
+ CountDownLatch newMinLatch = new CountDownLatch( 1 );
+ listener.setCreationLatch( newMinLatch );
+
+ // This should cause a new connection to be created (not necessarily the one that's being returned)
+ try ( Connection c = dataSource.getConnection() ) {
+ assertNotNull( c );
+ }
+ if ( !newMinLatch.await( TIMEOUT_MS, MILLISECONDS ) ) {
+ fail( format( "{0} new connections not created", newMinLatch.getCount() ) );
+ }
+ assertEquals( INITIAL_SIZE + 1, dataSource.getMetrics().availableCount(), "Pool not resized" );
+
+ // This will come from thread local cache, and unfortunately not increase the size of the pool
+ try ( Connection c = dataSource.getConnection() ) {
+ assertNotNull( c );
+ }
+ assertEquals( INITIAL_SIZE + 1, dataSource.getMetrics().availableCount(), "Pool not resized" );
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+
+ private static class ReadyDataSourceListener implements AgroalDataSourceListener {
+ private CountDownLatch creationLatch;
+
+ @SuppressWarnings( "WeakerAccess" )
+ ReadyDataSourceListener(CountDownLatch creationLatch) {
+ this.creationLatch = creationLatch;
+ }
+
+ public void setCreationLatch(CountDownLatch latch) {
+ creationLatch = latch;
+ }
+
+ @Override
+ public void onConnectionPooled(Connection connection) {
+ creationLatch.countDown();
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SecurityTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SecurityTests.java
new file mode 100644
index 0000000..c9ab415
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SecurityTests.java
@@ -0,0 +1,171 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.AgroalSecurityProvider;
+import io.agroal.api.security.NamePrincipal;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static java.text.MessageFormat.format;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class SecurityTests {
+
+ private static final Logger logger = getLogger( SecurityTests.class.getName() );
+
+ private static final String DEFAULT_USER = "def";
+
+ // --- //
+
+ @Test
+ @DisplayName( "Test password rotation" )
+ @SuppressWarnings( "InstantiationOfUtilityClass" )
+ void passwordRotation() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( RotationPassword.PASSWORDS.size() )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( CredentialsDataSource.class )
+ .principal( new NamePrincipal( DEFAULT_USER ) )
+ .credential( new RotationPassword() )
+ .addSecurityProvider( new PasswordRotationProvider() )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, new WarningsAgroalDatasourceListener() ) ) {
+ for ( String expectedPassword : RotationPassword.PASSWORDS ) {
+ Connection connection = dataSource.getConnection();
+ CredentialsConnection credentialsConnection = connection.unwrap( CredentialsConnection.class );
+ logger.info( format( "Got connection {0} with username {1} and password {2}", connection, credentialsConnection.getUser(), credentialsConnection.getPassword() ) );
+
+ assertEquals( DEFAULT_USER, credentialsConnection.getUser() );
+ assertEquals( expectedPassword, credentialsConnection.getPassword() );
+
+ // Connection leak
+ }
+ }
+ }
+
+ // --- //
+
+ @SuppressWarnings( {"UtilityClass", "UtilityClassWithoutPrivateConstructor"} )
+ private static final class RotationPassword {
+
+ public static final List PASSWORDS = Collections.unmodifiableList( Arrays.asList( "one", "two", "secret", "unknown" ) );
+
+ private static final AtomicInteger COUNTER = new AtomicInteger( 0 );
+
+ @SuppressWarnings( "WeakerAccess" )
+ RotationPassword() {
+ }
+
+ static Properties asProperties() {
+ Properties properties = new Properties();
+ properties.setProperty( "password", getWord() );
+ return properties;
+ }
+
+ private static String getWord() {
+ return PASSWORDS.get( COUNTER.getAndIncrement() );
+ }
+ }
+
+ private static class PasswordRotationProvider implements AgroalSecurityProvider {
+
+ @SuppressWarnings( "WeakerAccess" )
+ PasswordRotationProvider() {
+ }
+
+ @Override
+ @SuppressWarnings( "InstanceofConcreteClass" )
+ public Properties getSecurityProperties(Object securityObject) {
+ if ( securityObject instanceof RotationPassword ) {
+ return RotationPassword.asProperties();
+ }
+ return null;
+ }
+ }
+
+ private static class WarningsAgroalDatasourceListener implements AgroalDataSourceListener {
+
+ @SuppressWarnings( "WeakerAccess" )
+ WarningsAgroalDatasourceListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ fail( "Unexpected warning: " + message );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ fail( "Unexpected warning", throwable );
+ }
+ }
+
+ public static class CredentialsDataSource implements MockDataSource {
+
+ private String user, password;
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ return new CredentialsConnection( user, password );
+ }
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ private static class CredentialsConnection implements MockConnection {
+
+ private final String user, password;
+
+ CredentialsConnection(String user, String password) {
+ this.user = user;
+ this.password = password;
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ return iface.cast( this );
+ }
+
+ String getUser() {
+ return user;
+ }
+
+ String getPassword() {
+ return password;
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SimpleMapContext.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SimpleMapContext.java
new file mode 100644
index 0000000..b4b1952
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/SimpleMapContext.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2023 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NameAlreadyBoundException;
+import javax.naming.NameClassPair;
+import javax.naming.NameParser;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Luis Barreiro
+ */
+public class SimpleMapContext implements Context {
+
+ private final Map map = new ConcurrentHashMap<>();
+
+ @Override
+ public Object lookup(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Object lookup(String name) throws NamingException {
+ return map.get( name );
+ }
+
+ @Override
+ public void bind(Name name, Object obj) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void bind(String name, Object obj) throws NamingException {
+ if ( map.putIfAbsent( name, obj ) != null ) {
+ throw new NameAlreadyBoundException( "Name already bound: " + name );
+ }
+ }
+
+ @Override
+ public void rebind(Name name, Object obj) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void rebind(String name, Object obj) throws NamingException {
+ map.put( name, obj );
+ }
+
+ @Override
+ public void unbind(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void unbind(String name) throws NamingException {
+ map.remove( name );
+ }
+
+ @Override
+ public void rename(Name oldName, Name newName) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void rename(String oldName, String newName) throws NamingException {
+ bind( newName, lookup( oldName ) );
+ unbind( oldName );
+ }
+
+ @Override
+ public NamingEnumeration list(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public NamingEnumeration list(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public NamingEnumeration listBindings(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public NamingEnumeration listBindings(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void destroySubcontext(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void destroySubcontext(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Context createSubcontext(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Context createSubcontext(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Object lookupLink(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Object lookupLink(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public NameParser getNameParser(Name name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public NameParser getNameParser(String name) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Name composeName(Name name, Name prefix) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public String composeName(String name, String prefix) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Object addToEnvironment(String propName, Object propVal) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Object removeFromEnvironment(String propName) throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public Hashtable, ?> getEnvironment() throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ @Override
+ public void close() throws NamingException {
+ }
+
+ @Override
+ public String getNameInNamespace() throws NamingException {
+ throw new NamingException( "Not implemented" );
+ }
+
+ public void clear() {
+ map.clear();
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/TimeoutTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/TimeoutTests.java
new file mode 100644
index 0000000..2662c53
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/TimeoutTests.java
@@ -0,0 +1,368 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.cache.ConnectionCache;
+import io.agroal.api.configuration.AgroalDataSourceConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import io.agroal.test.MockDataSource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.lang.System.nanoTime;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.time.Duration.ofSeconds;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class TimeoutTests {
+
+ static final Logger logger = getLogger( TimeoutTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver();
+ if ( Utils.isWindowsOS() ) {
+ Utils.windowsTimerHack();
+ }
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "Acquisition timeout" )
+ void basicAcquisitionTimeoutTest() throws SQLException {
+ int MAX_POOL_SIZE = 100, ACQUISITION_TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( MAX_POOL_SIZE )
+ .acquisitionTimeout( ofMillis( ACQUISITION_TIMEOUT_MS ) )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ for ( int i = 0; i < MAX_POOL_SIZE; i++ ) {
+ Connection connection = dataSource.getConnection();
+ assertFalse( connection.isClosed(), "Expected open connection" );
+ // connection.close();
+ }
+ logger.info( format( "Holding all {0} connections from the pool and requesting a new one", MAX_POOL_SIZE ) );
+
+ long start = nanoTime(), timeoutBound = (long) ( ACQUISITION_TIMEOUT_MS * 1.1 );
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting acquisition timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Acquisition timeout after {0}ms - Configuration is {1}ms", elapsed, ACQUISITION_TIMEOUT_MS ) );
+ assertTrue( elapsed >= ACQUISITION_TIMEOUT_MS, "Acquisition timeout before time" );
+ }
+ }
+
+ @Test
+ @DisplayName( "Acquisition timeout of new connection" )
+ void acquisitionTimeoutOfNewConnectionTest() throws SQLException {
+ int ACQUISITION_TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 10 )
+ .acquisitionTimeout( ofMillis( ACQUISITION_TIMEOUT_MS ) )
+ .connectionCache( ConnectionCache.none() )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( SleepyDatasource.class )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ SleepyDatasource.setSleep();
+
+ long start = nanoTime(), timeoutBound = (long) ( ACQUISITION_TIMEOUT_MS * 1.1 );
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting acquisition timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Acquisition timeout after {0}ms - Configuration is {1}ms", elapsed, ACQUISITION_TIMEOUT_MS ) );
+ assertTrue( elapsed >= ACQUISITION_TIMEOUT_MS, "Acquisition timeout before time" );
+
+ SleepyDatasource.unsetSleep();
+
+ // Try again, to ensure that the Agroal thread has not become stuck after that first getConnection call
+ logger.info( "Attempting another getConnection() call" );
+ try ( Connection c = dataSource.getConnection() ) {
+ assertFalse( c.isClosed(), "Expected a good, healthy connection" );
+ }
+
+ dataSource.getConfiguration().connectionPoolConfiguration().setMinSize( 2 );
+ SleepyDatasource.setSleep();
+
+ // Try again, to ensure that even if new connections can't be established, getConnection calls still succeed
+ logger.info( "Attempting getConnection() on a sleepy datasource" );
+ try ( Connection c = dataSource.getConnection() ) {
+ assertFalse( c.isClosed(), "Expected a good, healthy connection" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "Login timeout" )
+ void loginTimeoutTest() throws SQLException {
+ int LOGIN_TIMEOUT_S = 2;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( LoginTimeoutDatasource.class )
+ .loginTimeout( ofSeconds( LOGIN_TIMEOUT_S ) )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ LoginTimeoutDatasource.setTimeout();
+
+ long start = nanoTime(), timeoutBound = LOGIN_TIMEOUT_S * 1500;
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting login timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Login timeout after {0}ms - Configuration is {1}s", elapsed, LOGIN_TIMEOUT_S ) );
+ assertTrue( elapsed >= LOGIN_TIMEOUT_S * 1000, "Login timeout before time" );
+
+ LoginTimeoutDatasource.unsetTimeout();
+
+ // Try again, to ensure that the Agroal thread has not become stuck after that first getConnection call
+ logger.info( "Attempting another getConnection() call" );
+ try ( Connection c = dataSource.getConnection() ) {
+ assertFalse( c.isClosed(), "Expected a good, healthy connection" );
+ }
+ }
+
+ AgroalDataSourceConfigurationSupplier bogusConfiguration = new AgroalDataSourceConfigurationSupplier()
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .acquisitionTimeout( ofSeconds( LOGIN_TIMEOUT_S ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( LoginTimeoutDatasource.class )
+ .loginTimeout( ofSeconds( 2 * LOGIN_TIMEOUT_S ) )
+ )
+ );
+
+ OnWarningListener warningListener = new OnWarningListener();
+ LoginTimeoutDatasource.setTimeout();
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( bogusConfiguration, warningListener ) ) {
+ assertTrue( warningListener.getWarning().get(), "Expected a warning on the size of acquisition timeout" );
+
+ logger.info( "Checking datasource health" );
+ assertTimeoutPreemptively( ofMillis( LOGIN_TIMEOUT_S * 1500 ), () -> assertThrows( SQLException.class, () -> dataSource.isHealthy( true ) ), "Expecting SQLException on heath check" );
+ }
+ }
+
+ @Test
+ @DisplayName( "Login timeout on initial connections" )
+ void loginTimeoutInitialTest() throws SQLException {
+ int LOGIN_TIMEOUT_S = 1, INITIAL_SIZE = 5;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( INITIAL_SIZE )
+ .maxSize( INITIAL_SIZE )
+ .acquisitionTimeout( ofSeconds( 2 * LOGIN_TIMEOUT_S ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( LoginTimeoutDatasource.class )
+ .loginTimeout( ofSeconds( LOGIN_TIMEOUT_S ) )
+ )
+ );
+
+ LoginTimeoutDatasource.setTimeout();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+
+ long start = nanoTime(), timeoutBound = LOGIN_TIMEOUT_S * 2500;
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting login timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Acquisition timeout after {0}ms - Configuration is {1}ms", elapsed, LOGIN_TIMEOUT_S * 2000 ) );
+ assertTrue( elapsed >= LOGIN_TIMEOUT_S * 2000, "Acquisition timeout before time" );
+
+ assertEquals( 0, dataSource.getMetrics().creationCount(), "Expected no created connection" );
+ }
+ }
+
+ @Test
+ @DisplayName( "Pool-less Login timeout" )
+ void poollessLoginTimeoutTest() throws SQLException, InterruptedException {
+ int ACQUISITION_TIMEOUT_MS = 1500, LOGIN_TIMEOUT_S = 1; // acquisition timeout > login timeout
+ CountDownLatch latch = new CountDownLatch( 1 );
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .dataSourceImplementation( AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL_POOLLESS )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .acquisitionTimeout( ofMillis( ACQUISITION_TIMEOUT_MS ) )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( LoginTimeoutDatasource.class )
+ .loginTimeout( ofSeconds( LOGIN_TIMEOUT_S ) )
+ )
+ );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ LoginTimeoutDatasource.unsetTimeout();
+
+ new Thread(() -> {
+ try (Connection c = dataSource.getConnection() ) {
+ latch.countDown();
+ assertFalse( c.isClosed(), "Expected good connection" );
+ logger.info( "Holding connection and sleeping for a duration slightly smaller then acquisition timeout" );
+ Thread.sleep( (long) (ACQUISITION_TIMEOUT_MS * 0.8) );
+ } catch ( SQLException e ) {
+ fail( "Unexpected exception", e );
+ } catch ( InterruptedException e ) {
+ fail( e );
+ }
+ } ).start();
+
+ // await good connection to poison data source
+ assertTrue( latch.await( ACQUISITION_TIMEOUT_MS, MILLISECONDS ) );
+ LoginTimeoutDatasource.setTimeout();
+
+ long start = nanoTime(), timeoutBound = ACQUISITION_TIMEOUT_MS + LOGIN_TIMEOUT_S * 1500;
+ assertTimeoutPreemptively( ofMillis( timeoutBound ), () -> assertThrows( SQLException.class, dataSource::getConnection ), "Expecting login timeout" );
+
+ long elapsed = NANOSECONDS.toMillis( nanoTime() - start );
+ logger.info( format( "Login timeout after {0}ms - Configuration is {1}ms + {2}s", elapsed, ACQUISITION_TIMEOUT_MS, LOGIN_TIMEOUT_S ) );
+ assertTrue( elapsed >= ACQUISITION_TIMEOUT_MS * 0.8 + LOGIN_TIMEOUT_S * 1000, "Login timeout before time" );
+ }
+ }
+
+ // --- //
+
+ private static class OnWarningListener implements AgroalDataSourceListener {
+
+ private final AtomicBoolean warning = new AtomicBoolean( false );
+
+ OnWarningListener() {
+ }
+
+ @Override
+ public void onWarning(String message) {
+ warning.set( true );
+ }
+
+ @Override
+ public void onWarning(Throwable throwable) {
+ warning.set( true );
+ }
+
+ AtomicBoolean getWarning() {
+ return warning;
+ }
+ }
+
+ public static class SleepyDatasource implements MockDataSource {
+
+ private static boolean doSleep;
+
+ public static void setSleep() {
+ doSleep = true;
+ }
+
+ public static void unsetSleep() {
+ doSleep = false;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ if ( !doSleep ) {
+ return new SleepyMockConnection();
+ }
+
+ try {
+ logger.info( "This connection will take a while to get established ..." );
+ Thread.sleep( Integer.MAX_VALUE );
+ } catch ( InterruptedException e ) {
+ logger.info( "Datasource disturbed in it's sleep" );
+ }
+ throw new SQLException( "I have a bad awakening!" );
+ }
+
+ private static class SleepyMockConnection implements MockConnection {
+ SleepyMockConnection() {
+ }
+ }
+ }
+
+ public static class LoginTimeoutDatasource implements MockDataSource {
+
+ private static boolean doTimeout;
+ private int loginTimeout;
+
+ public static void setTimeout() {
+ doTimeout = true;
+ }
+
+ public static void unsetTimeout() {
+ doTimeout = false;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ assertNotEquals( 0, loginTimeout, "Expected login timeout to be set to something" );
+ if ( !doTimeout ) {
+ return new LoginTimeoutConnection();
+ }
+
+ try {
+ logger.info( "Pretending to wait for connection to be established ..." );
+ Thread.sleep( loginTimeout * 1000L );
+ throw new SQLException( "Login timeout after " + loginTimeout + " seconds." );
+ } catch ( InterruptedException e ) {
+ throw new SQLException( e );
+ }
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ loginTimeout = seconds;
+ }
+
+ private static class LoginTimeoutConnection implements MockConnection {
+ LoginTimeoutConnection() {
+ }
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/Utils.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/Utils.java
new file mode 100644
index 0000000..afaa494
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/Utils.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+public abstract class Utils {
+ /**
+ * This method will start a daemon thread that will sleep indefinitely in order to be able to park with a higher
+ * resolution for windows. Without this hack, LockSupport.parkNanos() will not be able to park for less than ~16ms
+ * on windows.
+ *
+ * @see blog
+ * @see jdk bug
+ */
+ public static void windowsTimerHack() {
+ Thread t = new Thread(() -> {
+ try {
+ Thread.sleep(Long.MAX_VALUE);
+ } catch (InterruptedException e) { // a delicious interrupt, omm, omm
+ }
+ });
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public static boolean isWindowsOS() {
+ return System.getProperty( "os.name" ).startsWith( "Windows" );
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ValidationTests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ValidationTests.java
new file mode 100644
index 0000000..a95d6c7
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/ValidationTests.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.AgroalDataSourceListener;
+import io.agroal.api.configuration.AgroalConnectionPoolConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockConnection;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static io.agroal.test.MockDriver.deregisterMockDriver;
+import static io.agroal.test.MockDriver.registerMockDriver;
+import static java.text.MessageFormat.format;
+import static java.time.Duration.ofMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class ValidationTests {
+
+ static final Logger logger = getLogger( ValidationTests.class.getName() );
+
+ @BeforeAll
+ static void setupMockDriver() {
+ registerMockDriver( ValidationThrowsConnection.class );
+ }
+
+ @AfterAll
+ static void teardown() {
+ deregisterMockDriver();
+ }
+
+ // --- //
+
+ @Test
+ @DisplayName( "validation throws fatal exception" )
+ void validationThrowsTest() throws SQLException, InterruptedException {
+ int MAX_POOL_SIZE = 3, VALIDATION_MS = 1000, IDLE_VALIDATION_MS = 100;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( MAX_POOL_SIZE )
+ .maxSize( MAX_POOL_SIZE )
+ .validationTimeout( ofMillis( VALIDATION_MS ) )
+ .idleValidationTimeout( ofMillis( IDLE_VALIDATION_MS ) )
+ .acquisitionTimeout( ofMillis( 2 * VALIDATION_MS ) )
+ .connectionValidator( AgroalConnectionPoolConfiguration.ConnectionValidator.defaultValidator() )
+ .exceptionSorter( AgroalConnectionPoolConfiguration.ExceptionSorter.fatalExceptionSorter() )
+ );
+
+ InvalidationListener listener = new InvalidationListener( MAX_POOL_SIZE );
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ logger.info( format( "Awaiting for validation of all the {0} connections on the pool", MAX_POOL_SIZE ) );
+ listener.awaitValidation( 3 * VALIDATION_MS );
+
+ assertEquals( MAX_POOL_SIZE, dataSource.getMetrics().invalidCount(), "Expected connection invalid count" );
+ assertEquals( 0, dataSource.getMetrics().availableCount(), "Expected no available connections" );
+
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ assertEquals( MAX_POOL_SIZE + 1, dataSource.getMetrics().creationCount(), "Expected connection creation" );
+ }
+
+ logger.info( format( "Short sleep to trigger idle validation" ) );
+ Thread.sleep( 2 * IDLE_VALIDATION_MS );
+
+ try ( Connection connection = dataSource.getConnection() ) {
+ assertNotNull( connection.getSchema(), "Expected non null value" );
+ assertEquals( MAX_POOL_SIZE + 1, dataSource.getMetrics().invalidCount(), "Expected connection invalid count" );
+ assertEquals( MAX_POOL_SIZE + 2, dataSource.getMetrics().creationCount(), "Expected connection creation" );
+ }
+ }
+ }
+
+ @Test
+ @DisplayName( "idle validation test" )
+ void idleValidationTest() throws SQLException, InterruptedException {
+ int POOL_SIZE = 1, IDLE_VALIDATION_MS = 100, TIMEOUT_MS = 1000;
+
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ .metricsEnabled()
+ .connectionPoolConfiguration( cp -> cp
+ .initialSize( POOL_SIZE )
+ .maxSize( POOL_SIZE )
+ .idleValidationTimeout( ofMillis( IDLE_VALIDATION_MS ) )
+ .acquisitionTimeout( ofMillis( TIMEOUT_MS ) )
+ .connectionValidator( AgroalConnectionPoolConfiguration.ConnectionValidator.emptyValidator() )
+ );
+
+ BeforeValidationListener listener = new BeforeValidationListener();
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier, listener ) ) {
+
+ logger.info( format( "Short sleep to trigger idle validation" ) );
+ Thread.sleep( 2 * IDLE_VALIDATION_MS );
+
+ assertEquals( POOL_SIZE, dataSource.getMetrics().availableCount(), "Expected connection available count" );
+ assertEquals( 0, dataSource.getMetrics().invalidCount(), "Expected connection invalid count" );
+ assertEquals( 0, listener.getValidationAttempts(), "Expected validation count" );
+
+ try ( Connection c = dataSource.getConnection() ) {
+ assertEquals( 1, listener.getValidationAttempts(), "Expected validation count" );
+ logger.info( "Got valid idle connection " + c);
+ }
+
+ assertEquals( POOL_SIZE, dataSource.getMetrics().availableCount(), "Expected connection available count" );
+ assertEquals( 1, dataSource.getMetrics().acquireCount(), "Expected connection acquire count" );
+ assertEquals( 0, dataSource.getMetrics().invalidCount(), "Expected connection invalid count" );
+ }
+ }
+
+ // --- //
+
+ private static class InvalidationListener implements AgroalDataSourceListener {
+ private final CountDownLatch latch;
+
+ @SuppressWarnings( "WeakerAccess" )
+ InvalidationListener(int validationCount) {
+ latch = new CountDownLatch( validationCount );
+ }
+
+ @Override
+ public void onConnectionInvalid(Connection connection) {
+ latch.countDown();
+ }
+
+ void awaitValidation(int timeoutMS) {
+ try {
+ if ( !latch.await( timeoutMS, MILLISECONDS ) ) {
+ fail( format( "Validation of {0} connections", latch.getCount() ) );
+ }
+ } catch ( InterruptedException e ) {
+ fail( "Test fail due to interrupt" );
+ }
+ }
+ }
+
+ private static class BeforeValidationListener implements AgroalDataSourceListener {
+ private final AtomicInteger counter = new AtomicInteger( 0 );
+
+ @SuppressWarnings( "WeakerAccess" )
+ BeforeValidationListener() {
+ }
+
+ @Override
+ public void beforeConnectionValidation(Connection connection) {
+ counter.incrementAndGet();
+ }
+
+ int getValidationAttempts() {
+ return counter.get();
+ }
+
+ }
+
+ // --- //
+
+ public static class ValidationThrowsConnection implements MockConnection {
+
+ private boolean closed;
+
+ @Override
+ public void close() throws SQLException {
+ closed = true;
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException {
+ return closed;
+ }
+
+ @Override
+ public String getSchema() throws SQLException {
+ return "validation_only";
+ }
+
+ @Override
+ public boolean isValid(int timeout) throws SQLException {
+ logger.info( "Throwing exception on validation" );
+ throw new SQLException( "Throwing on validation" );
+ }
+ }
+}
diff --git a/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/XATests.java b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/XATests.java
new file mode 100644
index 0000000..8eca6d4
--- /dev/null
+++ b/jdbc-pool/src/test/java/org/xbib/jdbc/pool/test/XATests.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2017 Red Hat, Inc. and individual contributors as indicated by the @author tags.
+// You may not use this file except in compliance with the Apache License, Version 2.0.
+
+package org.xbib.jdbc.pool.test;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.configuration.AgroalDataSourceConfiguration;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.test.MockXAConnection;
+import io.agroal.test.MockXADataSource;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.XAConnection;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.logging.Logger;
+
+import static io.agroal.test.AgroalTestGroup.FUNCTIONAL;
+import static java.util.logging.Logger.getLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Luis Barreiro
+ */
+@Tag( FUNCTIONAL )
+public class XATests {
+
+ static final Logger logger = getLogger( XATests.class.getName() );
+
+ // --- //
+
+ @Test
+ @DisplayName( "XAConnection close test" )
+ void xaConnectionCloseTests() throws SQLException {
+ AgroalDataSourceConfigurationSupplier configurationSupplier = new AgroalDataSourceConfigurationSupplier()
+ // using pooless datasource as it closes connection on the calling thread
+ .dataSourceImplementation( AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL_POOLLESS )
+ .connectionPoolConfiguration( cp -> cp
+ .maxSize( 1 )
+ .connectionFactoryConfiguration( cf -> cf
+ .connectionProviderClass( RequiresCloseXADataSource.class ) ) );
+
+ try ( AgroalDataSource dataSource = AgroalDataSource.from( configurationSupplier ) ) {
+ try ( Connection c = dataSource.getConnection() ) {
+ c.getSchema();
+ }
+ // ensure close() is called on the xaConnection object and not in the xaConnection.getConnection() object
+ assertEquals( 1, RequiresCloseXADataSource.getClosed(), "XAConnection not closed" );
+ }
+ }
+
+ // --- //
+
+ public static class RequiresCloseXADataSource implements MockXADataSource {
+
+ private static int closed;
+
+ static void incrementClosed() {
+ closed++;
+ }
+
+ @SuppressWarnings( "WeakerAccess" )
+ static int getClosed() {
+ return closed;
+ }
+
+ @Override
+ public XAConnection getXAConnection() throws SQLException {
+ return new MyMockXAConnection();
+ }
+
+ private static class MyMockXAConnection implements MockXAConnection {
+ MyMockXAConnection() {
+ }
+
+ @Override
+ @SuppressWarnings( "ObjectToString" )
+ public void close() throws SQLException {
+ logger.info( "Closing XAConnection " + this );
+ incrementClosed();
+ }
+ }
+ }
+}