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 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 connectionType; + + public Empty() { + this( MockConnection.Empty.class ); + } + + public Empty( Class 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(); + } + } + } +}