From 7f03d8b4445d6dae3a7fb4b844b90c12a53908da Mon Sep 17 00:00:00 2001 From: "R.M. Morrien" Date: Mon, 8 Apr 2024 17:17:50 +0200 Subject: [PATCH] Part 2 for issue #24900 Start with a new PoolManagertImpl unit test to understand enlisted versus busy states of Resource handles and the wiring inside a transaction to keep track of all used resources. --- .../connectors/ConnectorRuntime.java | 3 +- .../resource/pool/ConnectionPool.java | 58 +++- .../resource/pool/PoolManagerImpl.java | 17 +- .../resource/pool/ConnectionPoolTest.java | 45 +-- .../resource/pool/InjectionUtil.java | 37 +++ .../resource/pool/PoolManagerImplTest.java | 309 ++++++++++++++++++ .../pool/mock/JavaEETransactionMock.java | 136 ++++++++ 7 files changed, 565 insertions(+), 40 deletions(-) create mode 100644 appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/InjectionUtil.java create mode 100644 appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/PoolManagerImplTest.java create mode 100644 appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/mock/JavaEETransactionMock.java diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ConnectorRuntime.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ConnectorRuntime.java index 972b22999c1..eca2cbe8104 100755 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ConnectorRuntime.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ConnectorRuntime.java @@ -234,9 +234,8 @@ public class ConnectorRuntime implements com.sun.appserv.connectors.internal.api @Inject private Provider resourceManagerProvider; - /* protected for unit test */ @Inject - protected ProcessEnvironment processEnvironment; + private ProcessEnvironment processEnvironment; @Inject private DriverLoader driverLoader; 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 4fcd48d927d..9457688b2d9 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 @@ -123,6 +123,7 @@ public class ConnectionPool implements ResourcePool, ConnectionLeakListener, Res * 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 @@ -353,8 +354,10 @@ protected synchronized void initPool(ResourceAllocator allocator) throws Pooling /** * Schedules the resizer timer task. If a task is currently scheduled, it would be cancelled and a new one is scheduled. + *

+ * package protected for unit tests */ - private void scheduleResizerTask() { + protected void scheduleResizerTask() { if (resizerTask != null) { // cancel the current task resizerTask.cancel(); @@ -395,7 +398,7 @@ public void addResource(ResourceAllocator alloc) throws PoolingException { } /** - * marks resource as free. This method should be used instead of directly calling + * Marks resource as free. This method should be used instead of directly calling * resoureHandle.getResourceState().setBusy(false) OR getResourceState(resourceHandle).setBusy(false) as this method * handles stopping of connection leak tracing If connection leak tracing is enabled, takes care of stopping connection * leak tracing @@ -408,7 +411,7 @@ protected void setResourceStateToFree(ResourceHandle resourceHandle) { } /** - * marks resource as busy. This method should be used instead of directly calling + * Marks resource as busy. This method should be used instead of directly calling * resoureHandle.getResourceState().setBusy(true) OR getResourceState(resourceHandle).setBusy(true) as this method * handles starting of connection leak tracing If connection leak tracing is enabled, takes care of starting connection * leak tracing @@ -421,10 +424,10 @@ protected void setResourceStateToBusy(ResourceHandle resourceHandle) { } /** - * returns resource from the pool. + * Returns a resource from the connection pool. * * @return a free pooled resource object matching the ResourceSpec - * @throws PoolingException - if any error occurrs - or the pool has reached its max size and the + * @throws PoolingException - if any error occurs - or the pool has reached its max size and the * max-connection-wait-time-in-millis has expired. */ @Override @@ -674,6 +677,11 @@ private ResourceHandle getResourceFromTransaction(Transaction transaction, Resou } } + // TODO: This 'if (state.isFree())' logic suggests the state can already be Busy. + // Document why it can already be busy. + // Is this because you can have a transaction within a transaction and reuse the same resource? + // But in that case: shouldn't the state be already set to 'busy' by the previous code + // in the same thread and still remain busy? if (state.isFree()) { setResourceStateToBusy(resourceHandle); } @@ -759,6 +767,12 @@ protected boolean isConnectionValid(ResourceHandle resourceHandle, ResourceAlloc * @return boolean representing the match status of the connection */ protected boolean matchConnection(ResourceHandle resource, ResourceAllocator alloc) { + // TODO: Explain what matching is in detail. + // TODO: Explain that if matchConnections is disabled in the connectionpool why 'true' is still returned and not false?! + // Old documentation mentions: "match-connections / default: true / If true, enables connection matching. You + // can set to false if connections are homogeneous." Jakarta documentation: + // jakarta.resource.spi.ManagedConnectionFactory.matchManagedConnections(Set, Subject, ConnectionRequestInfo) mentions: + // "criteria used for matching is specific to a resource adapter and is not prescribed by the Connector specification." boolean matched = true; if (matchConnections) { matched = alloc.matchConnection(resource); @@ -835,9 +849,10 @@ protected ResourceHandle getResourceFromPool(ResourceAllocator resourceAllocator } if (resourceFromPool != null) { - // Set correct state + // Set state to Busy setResourceStateToBusy(resourceFromPool); } else { + // Set state to Busy via resizePoolAndGetNewResource call resourceFromPool = resizePoolAndGetNewResource(resourceAllocator); } } finally { @@ -905,7 +920,10 @@ private ResourceHandle getMatchedResourceFromPool(ResourceAllocator alloc) { while ((handle = dataStructure.getResource()) != null) { if (matchConnection(handle, alloc)) { matchedResourceFromPool = handle; + // TODO: ensure the state is not already isBusy here setResourceStateToBusy(matchedResourceFromPool); + + // Break from the while loop and do not add the handle to the activeResources list. break; } activeResources.add(handle); @@ -915,6 +933,7 @@ private ResourceHandle getMatchedResourceFromPool(ResourceAllocator alloc) { for (ResourceHandle activeResource : activeResources) { dataStructure.returnResource(activeResource); } + // No need to clear the list, clear() call is probably here to try to help the garbage collector. activeResources.clear(); } @@ -1057,9 +1076,6 @@ public void deleteResource(ResourceHandle resourceHandle) { } } - /** - * this method is called to indicate that the resource is not used by a bean/application anymore - */ @Override public void resourceClosed(ResourceHandle handle) throws IllegalStateException { LOG.log(FINE, "Resource was closed, processing handle: {0}", handle); @@ -1164,6 +1180,8 @@ protected boolean cleanupResource(ResourceHandle handle) { public void resourceErrorOccurred(ResourceHandle resourceHandle) throws IllegalStateException { LOG.log(FINE, "Resource error occured: {0}", resourceHandle); if (failAllConnections) { + // TODO: leakDetector is not updated and isBusy state of this resource is not updated correctly: possible bug. + // leakDetector should be updated in the doFailAllConnectionsProcessing method. The resource can be updated here. doFailAllConnectionsProcessing(); return; } @@ -1182,7 +1200,8 @@ public void resourceErrorOccurred(ResourceHandle resourceHandle) throws IllegalS throw new IllegalStateException(); } - // mark as not busy + // Mark as not busy. Even if it is removed from the Pool datastructure, + // it is good to clean it up, at least to clean up the leakDetector. setResourceStateToFree(resourceHandle); state.touchTimestamp(); @@ -1213,6 +1232,9 @@ private void doFailAllConnectionsProcessing() { } emptyPool(); + // TODO: leakDetector might have been used and it is not cleaned up. + // should call leakDetector.reset + try { createResources(allocator, steadyPoolSize); LOG.log(FINE, "Successfully created new resources."); @@ -1224,7 +1246,7 @@ private void doFailAllConnectionsProcessing() { } /** - * this method is called when a resource is enlisted in + * This method is called when a resource is enlisted in a transaction. * * @param tran Transaction * @param resource ResourceHandle @@ -1235,18 +1257,28 @@ public void resourceEnlisted(Transaction tran, ResourceHandle resource) throws I } /** - * this method is called when transaction tran is completed + * This method is called when transaction tran is completed. * * @param tran Transaction * @param status status of transaction */ @Override public void transactionCompleted(Transaction tran, int status) throws IllegalStateException { + // transactionCompleted will update all relevant resource handles to be no longer enlisted List delistedResources = poolTxHelper.transactionCompleted(tran, status, poolInfo); + for (ResourceHandle resource : delistedResources) { - // Application might not have closed the connection. + // Application might not have closed the connection, free the resource only if it is not in use anymore. if (isResourceUnused(resource)) { freeResource(resource); + // Resource is now returned to the pool and another thread can use it, cannot log it anymore. + } else { + // TODO: Why would the application not close a busy connection if the transaction completed and the resource handle is + // delisted from the transaction? Is this done to leave the resource as used in the connection pool and let it + // time out and be cleaned up by the timer? + // The poolTxHelper.transactionCompleted already altered the enlisted state to be no longer enlisted in any transaction. + // So this resource is no longer part of an outer transaction for example. + // Would expect a warning here in case the resource handle state still marked as busy. } } } diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/PoolManagerImpl.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/PoolManagerImpl.java index df200ea13a6..fdff0cc7412 100644 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/PoolManagerImpl.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/resource/pool/PoolManagerImpl.java @@ -120,20 +120,21 @@ public void createEmptyConnectionPool(PoolInfo poolInfo, PoolType pooltype, Hash /** * Create and initialize pool if not created already. + *

+ * package default for unit test * - * @param poolInfo Name of the pool to be created - * @param poolType - PoolType - * @return ResourcePool - newly created pool + * @param poolInfo pool identifier of the pool to be created + * @param poolType the type of pool to be created + * @param env the jndi information to find the ConnectorConnectionPool configuration used to configure the pool * @throws PoolingException when unable to create/initialize pool */ - private ResourcePool createAndInitPool(final PoolInfo poolInfo, PoolType poolType, Hashtable env) throws PoolingException { + void createAndInitPool(final PoolInfo poolInfo, PoolType poolType, Hashtable env) throws PoolingException { ResourcePool pool = getPool(poolInfo); if (pool == null) { pool = ResourcePoolFactoryImpl.newInstance(poolInfo, poolType, env); addPool(pool); LOG.log(INFO, "Created connection pool and added it to PoolManager: {0}", pool); } - return pool; } // invoked by DataSource objects to obtain a connection @@ -241,7 +242,8 @@ public boolean switchOnMatching(PoolInfo poolInfo) { return true; } - private void addPool(ResourcePool pool) { + /* package protected for unit test */ + protected void addPool(ResourcePool pool) { LOG.log(FINE, "Adding pool {0} to pooltable", pool.getPoolInfo()); poolTable.put(pool.getPoolInfo(), pool); } @@ -380,6 +382,7 @@ public void putbackBadResourceToPool(ResourceHandle resourceHandle) { ResourcePool pool = poolTable.get(poolInfo); if (pool != null) { synchronized (pool) { + // TODO: why is resourceClosed called AND resourceErrorOccurred? pool.resourceClosed(resourceHandle); resourceHandle.setConnectionErrorOccurred(); pool.resourceErrorOccurred(resourceHandle); @@ -396,6 +399,8 @@ public void putbackResourceToPool(ResourceHandle resourceHandle, boolean errorOc ResourcePool pool = poolTable.get(poolInfo); if (pool != null) { if (errorOccurred) { + // TODO: this code is different from putbackBadResourceToPool logic, explain why + // TODO: why is resourceHandle.setConnectionErrorOccurred(); not called? pool.resourceErrorOccurred(resourceHandle); } else { pool.resourceClosed(resourceHandle); 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 0f970ce1fd7..f4c73f4a65d 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 @@ -68,8 +68,6 @@ 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; @@ -94,7 +92,7 @@ public class ConnectionPoolTest { // they can probably also be made as a unit test here / or in a similar unit test @BeforeEach - public void createAndPopulateMocks() throws PoolingException, ResourceException { + public void createAndPopulateMocks() throws Exception { List mocks = new ArrayList<>(); // Mock ManagedConnection @@ -124,18 +122,18 @@ public void createAndPopulateMocks() throws PoolingException, ResourceException // Make sure ConnectorRuntime singleton is initialized MyConnectorRuntime connectorRuntime = new MyConnectorRuntime(); - ProcessEnvironment processEnvironment = new ProcessEnvironment(); - connectorRuntime.setProcessEnvironment(processEnvironment); connectorRuntime.postConstruct(); } 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); + Hashtable env = new Hashtable<>(); + env.put("maxPoolSize", Integer.valueOf(maxPoolSize)); + env.put("maxWaitTimeInMillis", Integer.valueOf(maxWaitTimeInMillis)); + env.put("poolResizeQuantity", Integer.valueOf(poolResizeQuantity)); + + connectionPool = new MyConnectionPool(poolInfo, env); assertEquals(0, connectionPool.getSteadyPoolSize()); assertEquals(maxPoolSize, connectionPool.getMaxPoolSize()); @@ -187,7 +185,7 @@ void basicConnectionPoolTest() throws Exception { // Test issue #24843: make the state of resource1 not busy anymore (it should not happen but it happens in rare cases), // resource should still be closed without throwing an exception. - resource1.getResourceState().setBusy(false); + connectionPool.setResourceStateToFree(resource1); connectionPool.resourceClosed(resource1); assertResourceIsNotBusy(resource1); @@ -450,12 +448,8 @@ private void cleanupConnectionPool() { 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<>()); + public MyConnectionPool(PoolInfo poolInfo, Hashtable env) throws PoolingException { + super(ConnectionPoolTest.getPoolInfo(), env); } @Override @@ -465,6 +459,13 @@ protected ConnectorConnectionPool getPoolConfigurationFromJndi(Hashtable env) th ConnectorConnectionPool connectorConnectionPool = ConnectionPoolObjectsUtils .createDefaultConnectorPoolObject(poolInfo, null); + int myMaxPoolSize = (int) env.get("maxPoolSize"); + int maxWaitTimeInMillis = (int) env.get("maxWaitTimeInMillis"); + int poolResizeQuantity = (int) env.get("poolResizeQuantity"); + + assertTrue(myMaxPoolSize > 0); + assertTrue(poolResizeQuantity > 0); + // Override some defaults connectorConnectionPool.setSteadyPoolSize("0"); connectorConnectionPool.setMaxPoolSize("" + myMaxPoolSize); @@ -473,12 +474,18 @@ protected ConnectorConnectionPool getPoolConfigurationFromJndi(Hashtable env) th return connectorConnectionPool; } + + protected void scheduleResizerTask() { + // Do not schedule any resize tasks + } } public class MyConnectorRuntime extends ConnectorRuntime { + private ProcessEnvironment processEnvironment = new ProcessEnvironment(); - public void setProcessEnvironment(ProcessEnvironment processEnvironment) { - this.processEnvironment = processEnvironment; + public MyConnectorRuntime() throws Exception { + // Force 'injection' of private field processEnvironment + InjectionUtil.injectPrivateField(ConnectorRuntime.class, this, "processEnvironment", processEnvironment); } @Override @@ -498,7 +505,7 @@ public DelegatingClassLoader getConnectorClassLoader() { } } - private static PoolInfo getPoolInfo() { + public static PoolInfo getPoolInfo() { SimpleJndiName jndiName = new SimpleJndiName("myPool"); return new PoolInfo(jndiName); } diff --git a/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/InjectionUtil.java b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/InjectionUtil.java new file mode 100644 index 00000000000..854d80d4b49 --- /dev/null +++ b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/InjectionUtil.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Eclipse Foundation and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.enterprise.resource.pool; + +import java.lang.reflect.Field; + +public class InjectionUtil { + + /** + * Use injection to fill in private fields in (final) classes. E.g. fields annotated with jakarta.inject.Inject. + * + * @param clazz the class to be altered + * @param clazzInstance the instance of the class that needs to be altered + * @param fieldName the name of the field in the class + * @param fieldValue the new value for the field + * @throws Exception if the injection of the value failed + */ + public static void injectPrivateField(Class clazz, Object clazzInstance, String fieldName, Object fieldValue) throws Exception { + Field declaredField = clazz.getDeclaredField(fieldName); + declaredField.setAccessible(true); + declaredField.set(clazzInstance, fieldValue); + } +} diff --git a/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/PoolManagerImplTest.java b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/PoolManagerImplTest.java new file mode 100644 index 00000000000..9ace3d5b60e --- /dev/null +++ b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/PoolManagerImplTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2024 Eclipse Foundation and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.enterprise.resource.pool; + +import static com.sun.enterprise.resource.pool.ConnectionPoolTest.getPoolInfo; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isNull; +import static org.easymock.EasyMock.notNull; +import static org.easymock.EasyMock.replay; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.appserv.connectors.internal.api.ConnectorConstants.PoolType; +import com.sun.appserv.connectors.internal.api.PoolingException; +import com.sun.enterprise.connectors.ConnectorRuntime; +import com.sun.enterprise.resource.ClientSecurityInfo; +import com.sun.enterprise.resource.ResourceHandle; +import com.sun.enterprise.resource.ResourceSpec; +import com.sun.enterprise.resource.allocator.ConnectorAllocator; +import com.sun.enterprise.resource.allocator.LocalTxConnectorAllocator; +import com.sun.enterprise.resource.allocator.NoTxConnectorAllocator; +import com.sun.enterprise.resource.allocator.ResourceAllocator; +import com.sun.enterprise.resource.pool.mock.JavaEETransactionMock; +import com.sun.enterprise.transaction.api.JavaEETransaction; +import com.sun.enterprise.transaction.api.JavaEETransactionManager; +import jakarta.resource.spi.ManagedConnection; +import jakarta.resource.spi.ManagedConnectionFactory; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Set; +import org.glassfish.api.admin.ProcessEnvironment; +import org.glassfish.api.invocation.InvocationManager; +import org.glassfish.api.invocation.InvocationManagerImpl; +import org.glassfish.api.naming.GlassfishNamingManager; +import org.glassfish.api.naming.SimpleJndiName; +import org.glassfish.resourcebase.resources.api.PoolInfo; +import org.glassfish.resourcebase.resources.naming.ResourceNamingService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PoolManagerImplTest { + + // Mocked instances + private GlassfishNamingManager glassfishNamingManager; + private JavaEETransactionManager javaEETransactionManager; + private ManagedConnection managedConnection; + private ManagedConnectionFactory managedConnectionFactory; + + // Regular fields + private ClientSecurityInfo clientSecurityInfo = null; + private PoolManagerImpl poolManagerImpl = new MyPoolManagerImpl(); + private PoolInfo poolInfo = getPoolInfo(); + private JavaEETransaction javaEETransaction = new MyJavaEETransaction(); + + @BeforeEach + public void createAndPopulateMocks() throws Exception { + List mocks = new ArrayList<>(); + + // Mock JavaEETransactionManager + javaEETransactionManager = createNiceMock(JavaEETransactionManager.class); + mocks.add(javaEETransactionManager); + + // Mock GlassfishNamingManager + glassfishNamingManager = createNiceMock(GlassfishNamingManager.class); + mocks.add(glassfishNamingManager); + + // Mock ManagedConnection + managedConnection = createNiceMock(ManagedConnection.class); + expect(managedConnection.getConnection(isNull(), isNull())) + .andReturn(new MyDatabaseConnection()).anyTimes(); + mocks.add(managedConnection); + + // Mock ManagedConnectionFactory + ManagedConnectionFactory localConnectionFactory = createMock(ManagedConnectionFactory.class); + expect(localConnectionFactory.createManagedConnection(isNull(), isNull())) + .andReturn(managedConnection) + .atLeastOnce(); + // Must return a not null object in matchManagedConnections to ensure matching in the ConnectionPool is 'true' + expect(localConnectionFactory.matchManagedConnections(notNull(), isNull(), isNull())) + .andReturn(managedConnection) + .atLeastOnce(); + managedConnectionFactory = localConnectionFactory; + mocks.add(managedConnectionFactory); + + replay(mocks.toArray()); + + // Make sure ConnectorRuntime singleton is initialized + MyConnectorRuntime connectorRuntime = new MyConnectorRuntime(); + connectorRuntime.postConstruct(); + } + + /** + * Test to show the getResource behavior + */ + @Test + void getResourceTest() throws Exception { + poolManagerImpl.createEmptyConnectionPool(poolInfo, PoolType.STANDARD_POOL, new Hashtable<>()); + + ResourceSpec resourceSpec = new ResourceSpec(new SimpleJndiName("myResourceSpec"), ResourceSpec.JNDI_NAME); + resourceSpec.setPoolInfo(poolInfo); + + // Test getting a single resource, this will return a 'userConnection' / 'connection handle' object representing the + // physical connection, e.g. a database connection. + + // Test using the No transaction allocator + ResourceAllocator noTxAllocator = new NoTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, + null, null, null); + Object resource = poolManagerImpl.getResource(resourceSpec, noTxAllocator, clientSecurityInfo); + assertTrue(resource instanceof MyDatabaseConnection); + + // Test using the Local transaction allocator + ResourceAllocator localTxAllocator = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, + null, null, null, false); + resource = poolManagerImpl.getResource(resourceSpec, localTxAllocator, clientSecurityInfo); + assertTrue(resource instanceof MyDatabaseConnection); + + // Test using the XA transaction allocator + ResourceAllocator xAllocator = new ConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, + null, null, null, false); + resource = poolManagerImpl.getResource(resourceSpec, xAllocator, clientSecurityInfo); + assertTrue(resource instanceof MyDatabaseConnection); + + // Resources from the pool should be occupied + assertPoolStatusNumberOfConnectionsUsed(3); + + // Get resource does not return a ResourceHandle, so we cannot return the resources to the pool. + // For now just flush all resources from the pool. + poolManagerImpl.flushConnectionPool(poolInfo); + + assertPoolStatusNumberOfConnectionsUsed(0); + + // Kill the pool + poolManagerImpl.killPool(poolInfo); + assertNull(poolManagerImpl.getPoolStatus(poolInfo)); + } + + /** + * Test to show the getResourceFromPool behavior in relation to the ResourceHandle enlisted and busy states, while using + * the LocalTxConnectorAllocator. + */ + @Test + public void getResourceFromPoolTest() throws Exception { + poolManagerImpl.createEmptyConnectionPool(poolInfo, PoolType.STANDARD_POOL, new Hashtable<>()); + assertPoolStatusNumberOfConnectionsUsed(0); + assertPoolStatusNumberOfConnectionsFree(0); + + ResourceSpec resourceSpec = new ResourceSpec(new SimpleJndiName("myResourceSpec"), ResourceSpec.JNDI_NAME); + resourceSpec.setPoolInfo(poolInfo); + + // Test using the Local transaction allocator + ResourceAllocator localTxAllocator = new LocalTxConnectorAllocator(null, managedConnectionFactory, resourceSpec, null, + null, null, null, false); + ResourceHandle resource = poolManagerImpl.getResourceFromPool(resourceSpec, localTxAllocator, clientSecurityInfo, javaEETransaction); + assertNotNull(resource); + assertTrue(resource.getUserConnection() instanceof MyDatabaseConnection); + // The resource is not (yet) enlisted in a transaction + assertFalse(resource.isEnlisted()); + // Expecting all getResource from pool calls to mark the resource as busy + // ConnectionPool: getResourceFromPool / getUnenlistedResource / prefetch calls. + assertTrue(resource.getResourceState().isBusy()); + + // Enlist the resource in the transaction + resource.enlistedInTransaction(javaEETransaction); + assertTrue(resource.isEnlisted()); + assertTrue(resource.getResourceState().isBusy()); + + // One resource from the pool should be added to the pool and should be occupied + assertPoolStatusNumberOfConnectionsUsed(1); + assertPoolStatusNumberOfConnectionsFree(0); + + // Return the resource to the pool + poolManagerImpl.putbackResourceToPool(resource, false); + + // When resource is returned to the pool the state is no longer busy + assertFalse(resource.getResourceState().isBusy()); + + // Resource is still enlisted, because it is still part of a transaction + assertTrue(resource.isEnlisted()); + + // Resource is still in use + assertPoolStatusNumberOfConnectionsUsed(1); + assertPoolStatusNumberOfConnectionsFree(0); + + // Stop the transaction to get the resource delisted / unenlisted + // Mimic com.sun.enterprise.transaction.JavaEETransactionImpl.commit() call + poolManagerImpl.transactionCompleted(javaEETransaction, jakarta.transaction.Status.STATUS_COMMITTED); + + // Resource must be delisted / unenlisted by the transactionCompleted logic + // (warning for next assert: in multi-threaded tests resource can be used instantly by another thread!) + assertFalse(resource.isEnlisted()); + // Resource is returned to the pool, resource should be freed + // (warning for next assert: in multi-threaded tests resource can be used instantly by another thread!) + assertFalse(resource.getResourceState().isBusy()); + + // Connection should no longer be in use + assertPoolStatusNumberOfConnectionsUsed(0); + assertPoolStatusNumberOfConnectionsFree(1); + + // Kill the pool + poolManagerImpl.killPool(poolInfo); + assertNull(poolManagerImpl.getPoolStatus(poolInfo)); + } + + private void assertPoolStatusNumberOfConnectionsUsed(int expectedNumber) { + PoolStatus poolStatus = poolManagerImpl.getPoolStatus(poolInfo); + assertEquals(expectedNumber, poolStatus.getNumConnUsed()); + } + + private void assertPoolStatusNumberOfConnectionsFree(int expectedNumber) { + PoolStatus poolStatus = poolManagerImpl.getPoolStatus(poolInfo); + assertEquals(expectedNumber, poolStatus.getNumConnFree()); + } + + private class MyDatabaseConnection { + } + + private class MyConnectorRuntime extends ConnectorRuntime { + private InvocationManager manager = new InvocationManagerImpl(); + private ProcessEnvironment processEnvironment = new ProcessEnvironment(); + private ResourceNamingService resourceNamingService = new ResourceNamingService(); + + public MyConnectorRuntime() throws Exception { + // Force 'injection', unluckily ResourceNamingService is marked as final + // otherwise we could mock it, or subclass it for this unit test. + InjectionUtil.injectPrivateField(ResourceNamingService.class, resourceNamingService, "namingManager", glassfishNamingManager); + + // Force 'injection' of private field processEnvironment + InjectionUtil.injectPrivateField(ConnectorRuntime.class, this, "processEnvironment", processEnvironment); + } + + @Override + public JavaEETransactionManager getTransactionManager() { + return javaEETransactionManager; + } + + @Override + public InvocationManager getInvocationManager() { + return manager; + } + + @Override + public ResourceNamingService getResourceNamingService() { + return resourceNamingService ; + } + + @Override + public PoolManager getPoolManager() { + return poolManagerImpl; + } + } + + // We cannot depend on the real JavaEETransactionImpl due to dependency limitations, create our own + private class MyJavaEETransaction extends JavaEETransactionMock { + + private HashMap resourceTable = new HashMap<>(); + + @Override + public Set getAllParticipatingPools() { + return resourceTable.keySet(); + } + + @Override + public Set getResources(Object poolInfo) { + return resourceTable.get(poolInfo); + } + + @Override + public void setResources(Set resources, Object poolInfo) { + resourceTable.put(poolInfo, resources); + } + } + + private class MyPoolManagerImpl extends PoolManagerImpl { + + // Override createAndInitPool to be able to use our MyConnectionPool implementation to be able to override + // getPoolConfigurationFromJndi and scheduleResizerTask in the ConnectionPool class. + @Override + void createAndInitPool(PoolInfo poolInfo, PoolType poolType, Hashtable env) throws PoolingException { + // Abuse env hashTable to get fields into the getPoolConfigurationFromJndi method + env.put("maxPoolSize", Integer.valueOf(10)); + env.put("maxWaitTimeInMillis", Integer.valueOf(500)); + env.put("poolResizeQuantity", Integer.valueOf(1)); + + ConnectionPoolTest.MyConnectionPool pool = new ConnectionPoolTest.MyConnectionPool(poolInfo, env); + addPool(pool); + } + } +} diff --git a/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/mock/JavaEETransactionMock.java b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/mock/JavaEETransactionMock.java new file mode 100644 index 00000000000..5945c0d1c91 --- /dev/null +++ b/appserver/connectors/connectors-runtime/src/test/java/com/sun/enterprise/resource/pool/mock/JavaEETransactionMock.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 Eclipse Foundation and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.enterprise.resource.pool.mock; + +import com.sun.enterprise.transaction.api.JavaEETransaction; +import com.sun.enterprise.transaction.api.SimpleResource; +import com.sun.enterprise.transaction.spi.TransactionalResource; +import jakarta.persistence.EntityManagerFactory; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import java.util.Set; +import javax.transaction.xa.XAResource; + +/** + * Mock class without any implementation + */ +public class JavaEETransactionMock implements JavaEETransaction { + + @Override + public void commit() + throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { + } + + @Override + public boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException, SystemException { + return false; + } + + @Override + public boolean enlistResource(XAResource xaRes) throws RollbackException, IllegalStateException, SystemException { + return false; + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public void registerSynchronization(Synchronization sync) throws RollbackException, IllegalStateException, SystemException { + } + + @Override + public void rollback() throws IllegalStateException, SystemException { + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + } + + @Override + public SimpleResource getExtendedEntityManagerResource(EntityManagerFactory factory) { + return null; + } + + @Override + public SimpleResource getTxEntityManagerResource(EntityManagerFactory factory) { + return null; + } + + @Override + public void addTxEntityManagerMapping(EntityManagerFactory factory, SimpleResource em) { + } + + @Override + public void addExtendedEntityManagerMapping(EntityManagerFactory factory, SimpleResource em) { + } + + @Override + public void removeExtendedEntityManagerMapping(EntityManagerFactory factory) { + } + + @Override + public void setContainerData(T data) { + } + + @Override + public T getContainerData() { + return null; + } + + @Override + public Set getAllParticipatingPools() { + return null; + } + + @Override + public Set getResources(Object poolInfo) { + return null; + } + + @Override + public TransactionalResource getLAOResource() { + return null; + } + + @Override + public void setLAOResource(TransactionalResource h) { + } + + @Override + public TransactionalResource getNonXAResource() { + return null; + } + + @Override + public void setResources(Set resources, Object poolInfo) { + } + + @Override + public boolean isLocalTx() { + return false; + } + + @Override + public boolean isTimedOut() { + return false; + } +}