| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.pool3.impl; |
| |
| 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.assertTrue; |
| |
| import java.lang.management.ManagementFactory; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| 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 javax.management.MBeanServer; |
| import javax.management.ObjectName; |
| |
| import org.apache.commons.pool3.TestException; |
| import org.apache.commons.pool3.Waiter; |
| import org.apache.commons.pool3.WaiterFactory; |
| import org.apache.commons.pool3.impl.TestGenericObjectPool.SimpleFactory; |
| import org.junit.jupiter.api.AfterEach; |
| import org.junit.jupiter.api.BeforeEach; |
| import org.junit.jupiter.api.Test; |
| import org.junit.jupiter.api.Timeout; |
| |
| /** |
| */ |
| class TestBaseGenericObjectPool { |
| |
| BaseGenericObjectPool<String, TestException> pool; |
| SimpleFactory factory; |
| |
| @BeforeEach |
| public void setUp() { |
| factory = new SimpleFactory(); |
| pool = new GenericObjectPool<>(factory); |
| } |
| |
| @AfterEach |
| public void tearDown() { |
| pool.close(); |
| pool = null; |
| factory = null; |
| } |
| |
| @Test |
| void testActiveTimeStatistics() { |
| for (int i = 0; i < 99; i++) { // must be < MEAN_TIMING_STATS_CACHE_SIZE |
| pool.updateStatsReturn(Duration.ofMillis(i)); |
| } |
| assertEquals(49, pool.getMeanActiveTimeMillis(), Double.MIN_VALUE); |
| } |
| |
| @Test |
| void testBorrowWaitStatistics() { |
| final DefaultPooledObject<String> p = (DefaultPooledObject<String>) factory.makeObject(); |
| pool.updateStatsBorrow(p, Duration.ofMillis(10)); |
| pool.updateStatsBorrow(p, Duration.ofMillis(20)); |
| pool.updateStatsBorrow(p, Duration.ofMillis(20)); |
| pool.updateStatsBorrow(p, Duration.ofMillis(30)); |
| assertEquals(20, pool.getMeanBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| assertEquals(30, pool.getMaxBorrowWaitTimeMillis(), 0); |
| } |
| |
| void testBorrowWaitStatisticsMax() { |
| final DefaultPooledObject<String> p = (DefaultPooledObject<String>) factory.makeObject(); |
| assertEquals(0, pool.getMaxBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| pool.updateStatsBorrow(p, Duration.ZERO); |
| assertEquals(0, pool.getMaxBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| pool.updateStatsBorrow(p, Duration.ofMillis(20)); |
| assertEquals(20, pool.getMaxBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| pool.updateStatsBorrow(p, Duration.ofMillis(20)); |
| assertEquals(20, pool.getMaxBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| pool.updateStatsBorrow(p, Duration.ofMillis(10)); |
| assertEquals(20, pool.getMaxBorrowWaitTimeMillis(), Double.MIN_VALUE); |
| } |
| |
| @Test |
| void testEvictionTimerMultiplePools() throws InterruptedException { |
| final AtomicIntegerFactory factory = new AtomicIntegerFactory(); |
| factory.setValidateLatency(50); |
| try (GenericObjectPool<AtomicInteger, RuntimeException> evictingPool = new GenericObjectPool<>(factory)) { |
| evictingPool.setDurationBetweenEvictionRuns(Duration.ofMillis(100)); |
| evictingPool.setNumTestsPerEvictionRun(5); |
| evictingPool.setTestWhileIdle(true); |
| evictingPool.setMinEvictableIdleDuration(Duration.ofMillis(50)); |
| for (int i = 0; i < 10; i++) { |
| try { |
| evictingPool.addObject(); |
| } catch (final Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| for (int i = 0; i < 1000; i++) { |
| try (GenericObjectPool<AtomicInteger, RuntimeException> nonEvictingPool = new GenericObjectPool<>(factory)) { |
| // empty |
| } |
| } |
| |
| Thread.sleep(1000); |
| assertEquals(0, evictingPool.getNumIdle()); |
| } |
| } |
| |
| /** |
| * POOL-393 |
| * Tests JMX registration does not add too much latency to pool creation. |
| */ |
| @SuppressWarnings("resource") // pools closed in finally block |
| @Test |
| @Timeout(value = 10_000, unit = TimeUnit.MILLISECONDS) |
| void testJMXRegistrationLatency() { |
| final int numPools = 1000; |
| final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); |
| final ArrayList<GenericObjectPool<Waiter, ?>> pools = new ArrayList<>(); |
| try { |
| // final long startTime = System.currentTimeMillis(); |
| for (int i = 0; i < numPools; i++) { |
| final GenericObjectPool<Waiter, ?> gop = new GenericObjectPool<>(new WaiterFactory<>(0, 0, 0, 0, 0, 0), new GenericObjectPoolConfig<>()); |
| assertNotNull(gop.getJmxName()); |
| pools.add(gop); |
| } |
| // System.out.println("Duration: " + (System.currentTimeMillis() - startTime)); |
| final ObjectName oname = pools.get(numPools - 1).getJmxName(); |
| assertEquals(1, mbs.queryNames(oname, null).size()); |
| } finally { |
| pools.forEach(GenericObjectPool::close); |
| } |
| } |
| |
| @Test |
| void testCollectDetailedStatisticsDefault() { |
| // Test that collectDetailedStatistics defaults to true for backward compatibility |
| assertTrue(pool.getCollectDetailedStatistics()); |
| } |
| |
| @Test |
| void testCollectDetailedStatisticsConfiguration() { |
| // Test configuration through config object |
| final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>(); |
| config.setCollectDetailedStatistics(false); |
| try (GenericObjectPool<String, TestException> testPool = new GenericObjectPool<>(factory, config)) { |
| assertFalse(testPool.getCollectDetailedStatistics()); |
| } |
| // Test runtime configuration |
| pool.setCollectDetailedStatistics(false); |
| assertFalse(pool.getCollectDetailedStatistics()); |
| pool.setCollectDetailedStatistics(true); |
| assertTrue(pool.getCollectDetailedStatistics()); |
| } |
| |
| @Test |
| void testCollectDetailedStatisticsDisabled() throws Exception { |
| // Configure pool to disable detailed statistics |
| pool.setCollectDetailedStatistics(false); |
| final DefaultPooledObject<String> pooledObject = (DefaultPooledObject<String>) factory.makeObject(); |
| // Record initial values |
| final long initialActiveTime = pool.getMeanActiveTimeMillis(); |
| final long initialIdleTime = pool.getMeanIdleDuration().toMillis(); |
| final long initialWaitTime = pool.getMeanBorrowWaitTimeMillis(); |
| final long initialMaxWaitTime = pool.getMaxBorrowWaitTimeMillis(); |
| // Update statistics - should be ignored for detailed stats |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(100)); |
| pool.updateStatsReturn(Duration.ofMillis(200)); |
| // Basic counters should still work |
| assertEquals(1, pool.getBorrowedCount()); |
| assertEquals(1, pool.getReturnedCount()); |
| // Detailed statistics should remain unchanged |
| assertEquals(initialActiveTime, pool.getMeanActiveTimeMillis()); |
| assertEquals(initialIdleTime, pool.getMeanIdleDuration().toMillis()); |
| assertEquals(initialWaitTime, pool.getMeanBorrowWaitTimeMillis()); |
| assertEquals(initialMaxWaitTime, pool.getMaxBorrowWaitTimeMillis()); |
| } |
| |
| @Test |
| void testCollectDetailedStatisticsEnabled() throws Exception { |
| // Ensure detailed statistics are enabled (default) |
| pool.setCollectDetailedStatistics(true); |
| final DefaultPooledObject<String> pooledObject = (DefaultPooledObject<String>) factory.makeObject(); |
| // Update statistics |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(100)); |
| pool.updateStatsReturn(Duration.ofMillis(200)); |
| // All counters should work |
| assertEquals(1, pool.getBorrowedCount()); |
| assertEquals(1, pool.getReturnedCount()); |
| // Detailed statistics should be updated |
| assertEquals(200, pool.getMeanActiveTimeMillis()); |
| assertEquals(100, pool.getMeanBorrowWaitTimeMillis()); |
| assertEquals(100, pool.getMaxBorrowWaitTimeMillis()); |
| } |
| |
| @Test |
| void testCollectDetailedStatisticsToggling() throws Exception { |
| final DefaultPooledObject<String> pooledObject = (DefaultPooledObject<String>) factory.makeObject(); |
| // Start with detailed stats enabled |
| pool.setCollectDetailedStatistics(true); |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(50)); |
| pool.updateStatsReturn(Duration.ofMillis(100)); |
| assertEquals(50, pool.getMeanBorrowWaitTimeMillis()); |
| assertEquals(100, pool.getMeanActiveTimeMillis()); |
| // Disable detailed stats |
| pool.setCollectDetailedStatistics(false); |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(200)); |
| pool.updateStatsReturn(Duration.ofMillis(300)); |
| // Detailed stats should remain at previous values |
| assertEquals(50, pool.getMeanBorrowWaitTimeMillis()); |
| assertEquals(100, pool.getMeanActiveTimeMillis()); |
| // Basic counters should continue to increment |
| assertEquals(2, pool.getBorrowedCount()); |
| assertEquals(2, pool.getReturnedCount()); |
| } |
| |
| @Test |
| void testStatsStoreConcurrentAccess() throws Exception { |
| // Test the lock-free StatsStore implementation under concurrent load |
| final int numThreads = 10; |
| final int operationsPerThread = 1000; |
| final ExecutorService executor = Executors.newFixedThreadPool(numThreads); |
| final CountDownLatch startLatch = new CountDownLatch(1); |
| final CountDownLatch completeLatch = new CountDownLatch(numThreads); |
| final List<Future<Void>> futures = new ArrayList<>(); |
| // Create threads that will concurrently update statistics |
| for (int i = 0; i < numThreads; i++) { |
| final int threadId = i; |
| futures.add(executor.submit(() -> { |
| try { |
| final DefaultPooledObject<String> pooledObject = (DefaultPooledObject<String>) factory.makeObject(); |
| // Wait for all threads to be ready |
| startLatch.await(); |
| // Perform concurrent operations |
| for (int j = 0; j < operationsPerThread; j++) { |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(threadId * 10 + j)); |
| pool.updateStatsReturn(Duration.ofMillis(threadId * 20 + j)); |
| } |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } finally { |
| completeLatch.countDown(); |
| } |
| return null; |
| })); |
| } |
| // Start all threads simultaneously |
| startLatch.countDown(); |
| // Wait for completion |
| assertTrue(completeLatch.await(30, TimeUnit.SECONDS), "Concurrent test should complete within 30 seconds"); |
| // Verify no exceptions occurred |
| for (Future<Void> future : futures) { |
| future.get(); // Will throw if there was an exception |
| } |
| // Verify that statistics were collected (exact values may vary due to race conditions) |
| assertEquals(numThreads * operationsPerThread, pool.getBorrowedCount()); |
| assertEquals(numThreads * operationsPerThread, pool.getReturnedCount()); |
| // Mean values should be reasonable (not zero or wildly incorrect) |
| assertTrue(pool.getMeanActiveTimeMillis() >= 0); |
| assertTrue(pool.getMeanBorrowWaitTimeMillis() >= 0); |
| assertTrue(pool.getMaxBorrowWaitTimeMillis() >= 0); |
| executor.shutdown(); |
| assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); |
| } |
| |
| @Test |
| void testStatsStoreCircularBuffer() throws Exception { |
| // Test that StatsStore properly handles circular buffer behavior |
| final DefaultPooledObject<String> pooledObject = (DefaultPooledObject<String>) factory.makeObject(); |
| // Fill beyond the cache size (100) to test circular behavior |
| final int cacheSize = 100; // BaseGenericObjectPool.MEAN_TIMING_STATS_CACHE_SIZE |
| for (int i = 0; i < cacheSize + 50; i++) { |
| pool.updateStatsBorrow(pooledObject, Duration.ofMillis(i)); |
| pool.updateStatsReturn(Duration.ofMillis(i * 2)); |
| } |
| // Statistics should still be meaningful after circular buffer wrapping |
| assertTrue(pool.getMeanActiveTimeMillis() > 0); |
| assertTrue(pool.getMeanBorrowWaitTimeMillis() > 0); |
| assertTrue(pool.getMaxBorrowWaitTimeMillis() > 0); |
| // The mean should reflect recent values, not all historical values |
| // (exact assertion depends on circular buffer implementation) |
| assertTrue(pool.getMeanBorrowWaitTimeMillis() >= 50); // Should be influenced by recent higher values |
| } |
| |
| @Test |
| void testDetailedStatisticsConfigIntegration() { |
| // Test that config property is properly applied during pool construction |
| final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>(); |
| config.setCollectDetailedStatistics(false); |
| try (GenericObjectPool<String, TestException> testPool = new GenericObjectPool<>(factory, config)) { |
| assertFalse(testPool.getCollectDetailedStatistics(), "Pool should respect collectDetailedStatistics setting from config"); |
| // Test that toString includes the new property |
| final String configString = config.toString(); |
| assertTrue(configString.contains("collectDetailedStatistics"), "Config toString should include collectDetailedStatistics property"); |
| } |
| } |
| } |