diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/ConnectionPool.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/ConnectionPool.java index 390e1144090..6a99a0f12cb 100644 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/ConnectionPool.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/ConnectionPool.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Set; import java.util.Timer; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; import javax.naming.NamingException; @@ -67,20 +68,80 @@ public class ConnectionPool implements ResourcePool, ConnectionLeakListener, Res private static final Logger LOG = LogDomains.getLogger(ConnectionPool.class, LogDomains.RSR_LOGGER); // pool life-cycle config properties + /** + * Represents the "max-pool-size" configuration value.
+ * Specifies the maximum number of connections that can be created to satisfy client requests.
+ * Default: 32 + */ protected int maxPoolSize; + + /** + * Represents the "steady-pool-size" configuration value.
+ * Specifies the initial and minimum number of connections maintained in the pool.
+ * Default: 8 + */ protected int steadyPoolSize; - /** used by resizer to downsize the pool */ + + /** + * Represents the "pool-resize-quantity" configuration value.
+ * Specifies the number of idle connections to be destroyed if the existing number of connections is above the + * steady-pool-size (subject to the max-pool-size limit).
+ * This is enforced periodically at the idle-timeout-in-seconds interval. An idle connection is one that has not been + * used for a period of idle-timeout-in-seconds. When the pool size reaches steady-pool-size, connection removal + * stops.
+ * Default: 2 + */ protected int resizeQuantity; - /** The total time a thread is willing to wait for a resource object. */ + + /** + * Represents the "max-wait-time-in-millis" configuration value.
+ * Specifies the amount of time, in milliseconds, that the caller is willing to wait for a connection. If 0, the caller + * is blocked indefinitely until a resource is available or an error occurs.
+ * Default: 60.000ms + */ protected int maxWaitTime; - /** time (in ms) before destroying a free resource */ + + /** + * Represents the "idle-timeout-in-seconds" configuration value, but in millis.
+ * Specifies the maximum time that a connection can remain idle in the pool. After this amount of time, the pool can + * close this connection.
+ * Default: 300.000ms + */ protected long idletime; // pool config properties + /** + * Represents the "fail-all-connections" configuration value.
+ * If true, closes all connections in the pool if a single validation check fails.
+ * Default: false + */ protected boolean failAllConnections; + + /** + * Represents the "match-connections" configuration value.
+ * If true, enables connection matching. You can set to false if connections are homogeneous.
+ * Default: true + */ protected boolean matchConnections; + /** + * Represents the "is-connection-validation-required" configuration value.
+ * Specifies whether connections have to be validated before being given to the application. If a resource’s validation + * fails, it is destroyed, and a new resource is created and returned.
+ * Default: false + * + * TODO: rename to 'connectionValidationRequired' + */ protected boolean validation; + + /** + * Represents the "prefer-validate-over-recreate property" configuration value.
+ * Specifies that validating idle connections is preferable to closing them. This property has no effect on non-idle + * connections. If set to true, idle connections are validated during pool resizing, and only those found to be invalid + * are destroyed and recreated. If false, all idle connections are destroyed and recreated during pool resizing.
+ * Default: false + */ protected boolean preferValidateOverRecreate; + // hold on to the resizer task so we can cancel/reschedule it. protected Resizer resizerTask; @@ -88,17 +149,55 @@ public class ConnectionPool implements ResourcePool, ConnectionLeakListener, Res protected Timer timer; // advanced pool config properties + /** + * Represents the "connection-creation-retry-attempts" configuration value.
+ * If the value connectionCreationRetryAttempts_ is > 0 then connectionCreationRetry_ is true, otherwise false. + */ protected boolean connectionCreationRetry_; + /** + * Represents the "connection-creation-retry-attempts" configuration.
+ * Specifies the number of attempts to create a new connection.
+ * Default: 0 + */ protected int connectionCreationRetryAttempts_; + + /** + * Represents the "connection-creation-retry-interval-in-seconds" configuration, but in millis.
+ * Specifies the time interval between attempts to create a connection when connection-creation-retry-attempts is + * greater than 0.
+ * Default: 10.000ms + */ protected long conCreationRetryInterval_; + + /** + * Represents the "validate-atmost-once-period-in-seconds" configuration, but in millis.
+ * Specifies the time interval within which a connection is validated at most once. Minimizes the number of validation + * calls. A value of zero allows unlimited validation calls.
+ * Default: 10.000ms. + */ protected long validateAtmostPeriodInMilliSeconds_; + + /** + * Represents the "max-connection-usage-count" configuration.
+ * Specifies the number of times a connections is reused by the pool, after which it is closed. A zero value disables + * this feature.
+ * Default: 0 + */ protected int maxConnectionUsage_; - // To validate a Sun RA Pool Connection if it hasnot been validated + + // To validate a Sun RA Pool Connection if it has not been validated // in the past x sec. (x=idle-timeout) // The property will be set from system property - // com.sun.enterprise.connectors.ValidateAtmostEveryIdleSecs=true + /** + * Represents the "validate-atmost-once-period-in-seconds" configuration.
+ * Specifies the time interval within which a connection is validated at most once. Minimizes the number of validation + * calls. A value of zero allows unlimited validation calls.
+ * Default: 0 + */ private boolean validateAtmostEveryIdleSecs; + // TODO document protected String resourceSelectionStrategyClass; protected PoolLifeCycleListener poolLifeCycleListener; @@ -129,6 +228,13 @@ public class ConnectionPool implements ResourcePool, ConnectionLeakListener, Res private boolean blocked; + /** + * Reentrant lock to ensure calls to {@link #getResourceFromPool(ResourceAllocator, ResourceSpec)} and + * {@link #freeResource(ResourceHandle)} are not executed at the same time to solve issue 24843, because one is getting + * resources from the pool and possibly resizing the pool while the other is returning resources to the pool. + */ + private final ReentrantLock getResourceFromPoolAndFreeResourceMethodsLock = new ReentrantLock(true); + public ConnectionPool(PoolInfo poolInfo, Hashtable env) throws PoolingException { this.poolInfo = poolInfo; setPoolConfiguration(env); @@ -675,50 +781,55 @@ protected ResourceHandle getResourceFromPool(ResourceAllocator resourceAllocator ResourceHandle resourceHandle; List freeResources = new ArrayList<>(); - try { - while ((resourceHandle = dataStructure.getResource()) != null) { - if (resourceHandle.hasConnectionErrorOccurred()) { - dataStructure.removeResource(resourceHandle); - continue; - } + try { + getResourceFromPoolAndFreeResourceMethodsLock.lock(); + try { + while ((resourceHandle = dataStructure.getResource()) != null) { + if (resourceHandle.hasConnectionErrorOccurred()) { + dataStructure.removeResource(resourceHandle); + continue; + } - if (matchConnection(resourceHandle, resourceAllocator)) { + if (matchConnection(resourceHandle, resourceAllocator)) { - boolean isValid = isConnectionValid(resourceHandle, resourceAllocator); - if (resourceHandle.hasConnectionErrorOccurred() || !isValid) { - if (failAllConnections) { - resourceFromPool = createSingleResourceAndAdjustPool(resourceAllocator, resourceSpec); - // No need to match since the resource is created with the allocator of caller. + boolean isValid = isConnectionValid(resourceHandle, resourceAllocator); + if (resourceHandle.hasConnectionErrorOccurred() || !isValid) { + if (failAllConnections) { + resourceFromPool = createSingleResourceAndAdjustPool(resourceAllocator, resourceSpec); + // No need to match since the resource is created with the allocator of caller. + break; + } + dataStructure.removeResource(resourceHandle); + // Resource is invalid, continue iteration. + continue; + } + if (resourceHandle.isShareable() == resourceAllocator.shareableWithinComponent()) { + // Got a matched, valid resource + resourceFromPool = resourceHandle; break; } - dataStructure.removeResource(resourceHandle); - // Resource is invalid, continue iteration. - continue; - } - if (resourceHandle.isShareable() == resourceAllocator.shareableWithinComponent()) { - // Got a matched, valid resource - resourceFromPool = resourceHandle; - break; + freeResources.add(resourceHandle); + } else { + freeResources.add(resourceHandle); } - freeResources.add(resourceHandle); - } else { - freeResources.add(resourceHandle); } + } finally { + // Return all unmatched, free resources + for (ResourceHandle freeResource : freeResources) { + dataStructure.returnResource(freeResource); + } + freeResources.clear(); } - } finally { - // Return all unmatched, free resources - for (ResourceHandle freeResource : freeResources) { - dataStructure.returnResource(freeResource); - } - freeResources.clear(); - } - if (resourceFromPool != null) { - // Set correct state - setResourceStateToBusy(resourceFromPool); - } else { - resourceFromPool = resizePoolAndGetNewResource(resourceAllocator); + if (resourceFromPool != null) { + // Set correct state + setResourceStateToBusy(resourceFromPool); + } else { + resourceFromPool = resizePoolAndGetNewResource(resourceAllocator); + } + } finally { + getResourceFromPoolAndFreeResourceMethodsLock.unlock(); } return resourceFromPool; @@ -734,26 +845,27 @@ protected ResourceHandle getResourceFromPool(ResourceAllocator resourceAllocator * @throws PoolingException when not able to create resources */ private ResourceHandle resizePoolAndGetNewResource(ResourceAllocator resourceAllocator) throws PoolingException { + // Must be called from the thread holding the lock to this pool. ResourceHandle newResource = null; int numOfConnsToCreate = 0; - if (dataStructure.getResourcesSize() < steadyPoolSize) { + int dataStructureSize = dataStructure.getResourcesSize(); + if (dataStructureSize < steadyPoolSize) { // May be all invalid resources are destroyed as // a result no free resource found and no. of resources is less than steady-pool-size - numOfConnsToCreate = steadyPoolSize - dataStructure.getResourcesSize(); - } else if (dataStructure.getResourcesSize() + resizeQuantity <= maxPoolSize) { - + numOfConnsToCreate = steadyPoolSize - dataStructureSize; + } else if (dataStructureSize + resizeQuantity <= maxPoolSize) { // Create and add resources of quantity "resizeQuantity" numOfConnsToCreate = resizeQuantity; - } else if (dataStructure.getResourcesSize() < maxPoolSize) { - + } else if (dataStructureSize < maxPoolSize) { // This else if "test condition" is not needed. Just to be safe. // still few more connections (less than "resizeQuantity" and to reach the count of maxPoolSize) // can be added - numOfConnsToCreate = maxPoolSize - dataStructure.getResourcesSize(); + numOfConnsToCreate = maxPoolSize - dataStructureSize; } + if (numOfConnsToCreate > 0) { createResources(resourceAllocator, numOfConnsToCreate); newResource = getMatchedResourceFromPool(resourceAllocator); @@ -962,7 +1074,9 @@ public void resourceClosed(ResourceHandle handle) throws IllegalStateException { if (poolLifeCycleListener != null && !handle.getDestroyByLeakTimeOut()) { poolLifeCycleListener.connectionReleased(handle.getId()); } - LOG.log(FINE, "Resource was freed after it`s closure: {0}", handle); + + // Note handle might already be altered by another thread before it is logged! + LOG.log(FINE, "Resource was freed after its closure: {0}", handle); } /** @@ -994,23 +1108,29 @@ protected void freeUnenlistedResource(ResourceHandle h) { } protected void freeResource(ResourceHandle resourceHandle) { - if (cleanupResource(resourceHandle)) { - // Only when resource handle usage count is more than maxConnUsage - if (maxConnectionUsage_ > 0 && resourceHandle.getUsageCount() >= maxConnectionUsage_) { - performMaxConnectionUsageOperation(resourceHandle); - } else { - // Put it back to the free collection. - dataStructure.returnResource(resourceHandle); - // update the monitoring data - if (poolLifeCycleListener != null && !resourceHandle.getDestroyByLeakTimeOut()) { - poolLifeCycleListener.decrementConnectionUsed(resourceHandle.getId()); - poolLifeCycleListener.incrementNumConnFree(false, steadyPoolSize); + try { + getResourceFromPoolAndFreeResourceMethodsLock.lock(); + if (cleanupResource(resourceHandle)) { + // Only when resource handle usage count is more than maxConnUsage + if (maxConnectionUsage_ > 0 && resourceHandle.getUsageCount() >= maxConnectionUsage_) { + performMaxConnectionUsageOperation(resourceHandle); + } else { + // Put it back to the free collection. + dataStructure.returnResource(resourceHandle); + // update the monitoring data + if (poolLifeCycleListener != null && !resourceHandle.getDestroyByLeakTimeOut()) { + poolLifeCycleListener.decrementConnectionUsed(resourceHandle.getId()); + poolLifeCycleListener.incrementNumConnFree(false, steadyPoolSize); + } } + // For both the cases of free.add and maxConUsageOperation, a free resource is added. + // Hence notify waiting threads. + notifyWaitingThreads(); } - // for both the cases of free.add and maxConUsageOperation, a free resource is added. - // Hence notify waiting threads - notifyWaitingThreads(); + } finally { + getResourceFromPoolAndFreeResourceMethodsLock.unlock(); } + } protected boolean cleanupResource(ResourceHandle handle) { @@ -1066,6 +1186,10 @@ public void resourceErrorOccurred(ResourceHandle resourceHandle) throws IllegalS // removed in the next iteration of getUnenlistedResource // or internalGetResource dataStructure.removeResource(resourceHandle); + + // Removing a resource, means a free resource is available for the pool. + // Hence notify waiting threads. + notifyWaitingThreads(); } private void doFailAllConnectionsProcessing() { diff --git a/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/ConnectionPoolTest.java b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/ConnectionPoolTest.java index 1b7daba9e1f..e45c5d96214 100644 --- a/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/ConnectionPoolTest.java +++ b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/ConnectionPoolTest.java @@ -28,15 +28,22 @@ 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 java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.Hashtable; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; import org.easymock.IExpectationSetters; import org.glassfish.api.admin.ProcessEnvironment; @@ -46,7 +53,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.opentest4j.MultipleFailuresError; import com.sun.appserv.connectors.internal.api.ConnectorRuntimeException; import com.sun.appserv.connectors.internal.api.PoolingException; @@ -59,16 +65,23 @@ import com.sun.enterprise.resource.ResourceState; import com.sun.enterprise.resource.allocator.LocalTxConnectorAllocator; import com.sun.enterprise.resource.allocator.ResourceAllocator; -import com.sun.enterprise.resource.pool.datastructure.RWLockDataStructure; +import com.sun.enterprise.resource.pool.datastructure.DataStructure; import com.sun.enterprise.transaction.api.JavaEETransaction; +import com.sun.logging.LogDomains; import jakarta.resource.ResourceException; import jakarta.resource.spi.ManagedConnection; import jakarta.resource.spi.ManagedConnectionFactory; +import jakarta.resource.spi.RetryableUnavailableException; import jakarta.transaction.Transaction; public class ConnectionPoolTest { + private static final Logger LOG = LogDomains.getLogger(ConnectionPool.class, LogDomains.RSR_LOGGER); + + private static final String NO_AVAILABLE_RESOURCES_AND_WAIT_TIME = "No available resources and wait time "; + private static final String MS_EXPIRED = " ms expired."; + private ManagedConnection managedConnection; private ManagedConnectionFactory managedConnectionFactory; private JavaEETransaction javaEETransaction; @@ -77,6 +90,9 @@ public class ConnectionPoolTest { private ResourceSpec resourceSpec; + // TODO: Look at tests like: org.glassfish.jdbc.devtests.v3.test.MaxConnectionUsageTest and the others in the package, + // they can probably also be made as a unit test here / or in a similar unit test + @BeforeEach public void createAndPopulateMocks() throws PoolingException, ResourceException { List mocks = new ArrayList<>(); @@ -113,11 +129,14 @@ public void createAndPopulateMocks() throws PoolingException, ResourceException connectorRuntime.postConstruct(); } - private void createConnectionPool(int maxPoolSize) throws PoolingException { + private void createConnectionPool(int maxPoolSize, int maxWaitTimeInMillis, int poolResizeQuantity) throws PoolingException { PoolInfo poolInfo = ConnectionPoolTest.getPoolInfo(); MyConnectionPool.myMaxPoolSize = maxPoolSize; + MyConnectionPool.maxWaitTimeInMillis = maxWaitTimeInMillis; + MyConnectionPool.poolResizeQuantity = poolResizeQuantity; + connectionPool = new MyConnectionPool(poolInfo); - assertEquals(1, connectionPool.getSteadyPoolSize()); + assertEquals(0, connectionPool.getSteadyPoolSize()); assertEquals(maxPoolSize, connectionPool.getMaxPoolSize()); resourceSpec = new ResourceSpec(new SimpleJndiName("myResourceSpec"), ResourceSpec.JNDI_NAME); @@ -129,14 +148,21 @@ private void createConnectionPool(int maxPoolSize) throws PoolingException { */ @Test void basicConnectionPoolTest() throws Exception { - createConnectionPool(2); + // Use the lowest maxWaitTimeInMillis, because this test knows the pool is exhausted on the 3rd call + // for a resource, and we want it to fail asap. Do not use 0, because it means wait indefinitely. + final int maxWaitTimeInMillis = 1; + + // Use poolResizeQuantity 1, to have the easiest understanding and predictability of the test + final int poolResizeQuantity = 1; + + createConnectionPool(2, maxWaitTimeInMillis, poolResizeQuantity); ResourceAllocator alloc = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, null, null, null, false); Transaction transaction = javaEETransaction; - // Test how many resources are available - // Expect 0 because the pool is not initialized yet. It will be initialized when the first resource is requested + // Test how many resources are available. Expect 0 because the pool is not initialized yet. It will be initialized when + // the first resource is requested and steady pool size is configured as 0. assertResourcesSize(0); // Test getting 3 resources from the pool of 2 @@ -150,9 +176,10 @@ void basicConnectionPoolTest() throws Exception { assertResourceIsBusy(resource2); assertResourcesSize(2); - assertThrows(PoolingException.class, () -> { + PoolingException assertThrows = assertThrows(PoolingException.class, () -> { connectionPool.getResource(resourceSpec, alloc, transaction); }); + assertEquals(NO_AVAILABLE_RESOURCES_AND_WAIT_TIME + maxWaitTimeInMillis + MS_EXPIRED, assertThrows.getMessage()); // Test returning 2 resources to the pool connectionPool.resourceClosed(resource2); @@ -167,96 +194,228 @@ void basicConnectionPoolTest() throws Exception { // Test how many resources are available assertResourcesSize(2); - connectionPool.emptyPool(); + cleanupConnectionPool(); + } + + /** + * Test the "pool-resize-quantity" option + */ + @Test + void poolResizeQuantityTest() throws Exception { + int maximumPoolSize = 5; + poolResizeQuantityTest(maximumPoolSize, 1); + poolResizeQuantityTest(maximumPoolSize, 2); + poolResizeQuantityTest(maximumPoolSize, 5); + + // Test bigger than maximum pool size + poolResizeQuantityTest(maximumPoolSize, 6); + } + + private void poolResizeQuantityTest(int maximumPoolSize, int poolResizeQuantity) throws PoolingException, RetryableUnavailableException { + final int maxWaitTimeInMillis = 1; + createConnectionPool(maximumPoolSize, maxWaitTimeInMillis, poolResizeQuantity); + + ResourceAllocator alloc = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, + null, null, null, false); + Transaction transaction = javaEETransaction; + + // Test how many resources are available + // Expect 0 because the pool is not initialized yet. It will be initialized when the first resource is requested and + // steady pool size is configured as 0 assertResourcesSize(0); + + // Get 1 resource, and the pool should increase from 0 to poolResizeQuantity, unless it hits the max connection pool + // size. + ResourceHandle resource = connectionPool.getResource(resourceSpec, alloc, transaction); + assertNotNull(resource); + + // Test the "pool-resize-quantity" logic. "poolResizeQuantity" number of new resource should have been added to the + // pool. + if (poolResizeQuantity <= maximumPoolSize) { + assertResourcesSize(poolResizeQuantity); + } else { + // Pool size should not exceed maximumPoolSize + assertResourcesSize(maximumPoolSize); + } + connectionPool.resourceClosed(resource); + + cleanupConnectionPool(); } + /** + * This test is about testing getResource and resourceClosed calls in a multi threaded situation. All calls should + * succeed if the pool size allows the total number of threads to complete within the maxWaitTimeInMillis value. + */ @Test @Timeout(value = 10) void basicConnectionPoolMultiThreadedTest() throws Exception { - int maxConnectionPoolSize = 30; - createConnectionPool(maxConnectionPoolSize); + // Use a low value to try and fill up the whole pool + int maxConnectionPoolSize = 5; + + // In this test the max wait time can be ignored because all threads succeed. + // So the "max-wait-time-in-millis" value is set to 0, to wait indefinitely. + final int maxWaitTimeInMillis = 0; + + // Use poolResizeQuantity 1, to have the easiest understanding and predictability of the test. + // It also means the highest amount of pool resize calls. + final int poolResizeQuantity = 1; + + createConnectionPool(maxConnectionPoolSize, maxWaitTimeInMillis, poolResizeQuantity); ResourceAllocator alloc = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, null, null, null, false); + // Keep track of resources and number of tasks executed + final Set usedResouceHandles = Collections.synchronizedSet(new HashSet<>()); + AtomicInteger numberOfThreadsFinished = new AtomicInteger(); + + // Make sure there are no resources in the pool before we start processing tasks + assertResourcesSize(0); + int taskCount = 100; List> tasks = new ArrayList<>(taskCount); for (int i = 0; i < taskCount; i++) { tasks.add(() -> { - while (true) { - try { - // Get resource - ResourceHandle resource = connectionPool.getResource(resourceSpec, alloc, javaEETransaction); - assertNotNull(resource); - assertResourceIsBusy(resource); - - // Keep resource a bit occupied, to ensure more than 1 resource from the - // pool is used at the same time - Thread.sleep(0, 10); - // Return resource - connectionPool.resourceClosed(resource); - assertResourceIsNotBusy(resource); - return null; - } catch (PoolingException e) { - // Try again a bit later, should get a connection at one point - Thread.sleep(0, 5); - } + // Get resource. PoolingException should only be possible if the maxWaitTimeInMillis is too low for the amount of tasks. + ResourceHandle resource = connectionPool.getResource(resourceSpec, alloc, javaEETransaction); + + assertNotNull(resource); + assertResourceIsBusy(resource); + + // Keep track of unique resources, we want to validate if resources are being reused + // which is the basic usage of the pool. + boolean isNewlyAdded = usedResouceHandles.add(resource); + if (isNewlyAdded) { + // Adding should only be ok for the first 'maxConnectionPoolSize' number of tasks that start + LOG.log(Level.FINE, "Adding resource: {0}", resource); + } else { + LOG.log(Level.FINE, "Reusing resource: {0}", resource); } + + // Keep resource a bit occupied, to ensure more than 1 resource from the + // pool is used at the same time and the max pool size is reached. + Thread.sleep(0, 10); + + // Return resource, this also eventually calls connectionPool.notifyWaitingThreads(), + // which allows waiting threads to continue when a resource is available. + connectionPool.resourceClosed(resource); + + // Cannot check the resource result here, it will already be re-used by another thread! + // assertResourceIsNotBusy(resource); + + numberOfThreadsFinished.incrementAndGet(); + return null; }); } runTheTasks(tasks); + + // Pool should be filled to maximum pool size. Resources can be idle for "idle-timeout-in-seconds" (which is 789 + // seconds) before they can be removed from the pool. assertResourcesSize(maxConnectionPoolSize); - connectionPool.emptyPool(); - assertResourcesSize(0); + + cleanupConnectionPool(); + + // Just another proof that all 1000 threads finished. + assertEquals(taskCount, numberOfThreadsFinished.get()); + + // No new resources are being used, check the remaining ones for the state. + for (ResourceHandle resource : usedResouceHandles) { + assertResourceIsNotBusy(resource); + } + + // Validate there are no more resources created, than the maximum pool size, because they should be reused + assertTrue(usedResouceHandles.size() <= maxConnectionPoolSize, "failed, size=" + usedResouceHandles.size()); } + /** + * This test is about testing getResource and resourceErrorOccurred calls in a multi threaded situation. All calls + * should be marked as failed and be removed from the pool. + */ @Test @Timeout(value = 10) void resourceErrorOccurredMultiThreadedTest() throws Exception { int maxConnectionPoolSize = 30; - createConnectionPool(maxConnectionPoolSize); + + // In this test the threads are not kept busy, but all are marked as failed. The value is set low, but might be + // dependent on the cpu speed. So it is not set too low. As rule of thumb: the value can be as low as the runtime of + // this test. The lower the 'maxConnectionPoolSize' value or the higher the number of 'taskCount' threads means this + // value needs to increase. + final int maxWaitTimeInMillis = 500; + + // Use poolResizeQuantity 1, to have the easiest understanding and predictability of the test + final int poolResizeQuantity = 1; + + createConnectionPool(maxConnectionPoolSize, maxWaitTimeInMillis, poolResizeQuantity); ResourceAllocator alloc = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, null, null, null, false); + // Keep track of resources and number of tasks executed + final Set usedResouceHandles = Collections.synchronizedSet(new HashSet<>()); + AtomicInteger numberOfThreadsFinished = new AtomicInteger(); + + // Make sure there are no resources in the pool before we start processing tasks + assertResourcesSize(0); + int taskCount = 100; List> tasks = new ArrayList<>(taskCount); for (int i = 0; i < taskCount; i++) { tasks.add(() -> { - while (true) { - try { - // Get resource - ResourceHandle resource = connectionPool.getResource(resourceSpec, alloc, javaEETransaction); - assertNotNull(resource); - assertResourceIsBusy(resource); - - // Keep resource a bit occupied, to ensure more than 1 resource from the - // pool is used at the same time - Thread.sleep(0, 10); - // Return resource - connectionPool.resourceErrorOccurred(resource); - assertResourceIsNotBusy(resource); - return null; - } catch (PoolingException e) { - // Try again a bit later, should get a connection at one point - Thread.sleep(0, 5); - } + // Get resource. PoolingException should only be possible if the maxWaitTimeInMillis is too low for the amount of tasks. + ResourceHandle resource = connectionPool.getResource(resourceSpec, alloc, javaEETransaction); + assertNotNull(resource); + assertResourceIsBusy(resource); + + // Keep track of unique resources, we want to validate if resources are NOT being reused + // because the resource is marked "error occurred", we expect it to be replaced with a new resource. + boolean isNewlyAdded = usedResouceHandles.add(resource); + if (isNewlyAdded) { + LOG.log(Level.FINE, "Adding resource: {0}", resource); + } else { + // We only call resourceErrorOccurred, thus resources should have been removed from 'ConnectionPool.dataStructure' + LOG.log(Level.SEVERE, "Adding resource failed for: {0}, thus a resource is being reused", resource); + fail("Resource may not be reused in this test."); } + + // No need to keep resource busy, since we will return it as failed and may not be reused again + + // Return resource + connectionPool.resourceErrorOccurred(resource); + + // We can test the resource results, because the resource should not be reused by another thread + assertResourceIsNotBusy(resource); + + numberOfThreadsFinished.incrementAndGet(); + return null; }); } runTheTasks(tasks); - connectionPool.emptyPool(); + // Pool should be empty: no steady pool size configured and all resources should have been removed from the pool. assertResourcesSize(0); + + cleanupConnectionPool(); + + // Just another proof that all 1000 threads finished. + assertEquals(taskCount, numberOfThreadsFinished.get()); + + // Failed connections should not be reused in the connection pool. New resources should have been created. Since this + // tests returns all resources using resourceErrorOccurred the amount must be equal to the number of threads started. + assertEquals(taskCount, usedResouceHandles.size()); } - private void runTheTasks(List> tasks) throws InterruptedException, MultipleFailuresError { + private void runTheTasks(List> tasks) throws Exception { ExecutorService threadPool = Executors.newFixedThreadPool(1000); - List> futures = threadPool.invokeAll(tasks, 10, TimeUnit.SECONDS); + List> futures = threadPool.invokeAll(tasks, 30, TimeUnit.SECONDS); assertAll( () -> assertTrue(futures.stream().allMatch(f -> f.isDone()), "All done."), () -> assertFalse(futures.stream().anyMatch(f -> f.isCancelled()), "None cancelled.")); + + // Call get() to see if there was any assertion error in the thread execution, this call + // forces the ExecutionException to be thrown, if the Callable execution failed. + for (Future future : futures) { + future.get(); + } } private void assertResourceIsBusy(ResourceHandle resource) { @@ -269,20 +428,28 @@ private void assertResourceIsBusy(ResourceHandle resource) { private void assertResourceIsNotBusy(ResourceHandle resource) { // Resource must be marked as not isBusy / isFree right after call to resourceClosed ResourceState resourceState = resource.getResourceState(); - assertTrue(resourceState.isFree()); - assertFalse(resourceState.isBusy()); + assertTrue(resourceState.isFree(), "assertResourceIsNotBusy - Expected isFree for resource" + resource); + assertFalse(resourceState.isBusy(), "assertResourceIsNotBusy - Expected !isBusy for resource" + resource); } private void assertResourcesSize(int expectedSize) { - RWLockDataStructure dataStructure = (RWLockDataStructure) connectionPool.dataStructure; + DataStructure dataStructure = connectionPool.dataStructure; assertEquals(expectedSize, dataStructure.getResourcesSize()); assertEquals(expectedSize, dataStructure.getAllResources() .size()); } + private void cleanupConnectionPool() { + // Clean the pool + connectionPool.emptyPool(); + assertResourcesSize(0); + } + public static class MyConnectionPool extends ConnectionPool { public static int myMaxPoolSize; + public static int maxWaitTimeInMillis; + public static int poolResizeQuantity; public MyConnectionPool(PoolInfo poolInfo) throws PoolingException { super(ConnectionPoolTest.getPoolInfo(), new Hashtable<>()); @@ -290,12 +457,16 @@ public MyConnectionPool(PoolInfo poolInfo) throws PoolingException { @Override protected ConnectorConnectionPool getPoolConfigurationFromJndi(Hashtable env) throws PoolingException { + // Note: this Util has other defaults than the real pool. + // Example: maxWaitTimeInMillis is: 7889, while "max-wait-time-in-millis" is documented as 60.000ms ConnectorConnectionPool connectorConnectionPool = ConnectionPoolObjectsUtils .createDefaultConnectorPoolObject(poolInfo, null); - // lower the default pool size - connectorConnectionPool.setSteadyPoolSize("1"); + // Override some defaults + connectorConnectionPool.setSteadyPoolSize("0"); connectorConnectionPool.setMaxPoolSize("" + myMaxPoolSize); + connectorConnectionPool.setMaxWaitTimeInMillis("" + maxWaitTimeInMillis); + connectorConnectionPool.setPoolResizeQuantity("" + poolResizeQuantity); return connectorConnectionPool; }