[ 
https://issues.apache.org/jira/browse/POOL-426?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18047514#comment-18047514
 ] 

Raju Gupta edited comment on POOL-426 at 12/25/25 12:11 PM:
------------------------------------------------------------

[~chadwilson] Think this might be related. I have added a test for the same at 
[here| [https://github.com/apache/commons-pool/pull/451 
|https://github.com/apache/commons-pool/pull/451.]] Let me know if this works 
for you.


was (Author: JIRAUSER308107):
[~chadwilson] Think this might be related. I have added a test for the same at 
[here|[https://github.com/apache/commons-pool/pull/451|https://github.com/apache/commons-pool/pull/451.]]
 Let me know if this works for you.

> GenericObjectPool does not respect maxIdle configuration
> --------------------------------------------------------
>
>                 Key: POOL-426
>                 URL: https://issues.apache.org/jira/browse/POOL-426
>             Project: Commons Pool
>          Issue Type: Bug
>            Reporter: Raju Gupta
>            Priority: Critical
>
> h2. Summary
> {{GenericObjectPool.addObject()}} does not respect the {{maxIdle}} limit due 
> to a race condition, allowing the pool to exceed the configured maximum idle 
> objects.
> *Related:* POOL-425
> h2. Root Cause
> The {{addObject()}} method (lines 210-221) has a check-then-act race 
> condition:
> {code:java}
> @Override
> public void addObject() throws Exception {
>     assertOpen();
>     if (factory == null) {
>         throw new IllegalStateException("Cannot add objects without a 
> factory.");
>     }
>     final int localMaxTotal = getMaxTotal();
>     final int localMaxIdle = getMaxIdle();
>     if (getNumIdle() < localMaxIdle && (localMaxTotal < 0 || 
> createCount.get() < localMaxTotal)) {
>         addIdleObject(create(getMaxWaitDuration()));
>     }
> }
> {code}
> *Problem:* The check {{getNumIdle() < localMaxIdle}} and object 
> creation/addition are not atomic. Multiple threads can pass the check 
> simultaneously before any add objects, exceeding {{{}maxIdle{}}}.
> *Why {{maxTotal}} works but {{maxIdle}} doesn't:* The {{create()}} method 
> enforces {{maxTotal}} via synchronized access to {{{}createCount{}}}, but no 
> such protection exists for {{{}maxIdle{}}}.
> h2. Reproduction
> h3. Test Case 1:
> {code:java}
> @Test
> @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
> void testAddObjectRespectsMaxIdleLimit() throws Exception {
>     genericObjectPool.setMaxIdle(1);
>     genericObjectPool.addObject();
>     genericObjectPool.addObject();
>     assertEquals(1, genericObjectPool.getNumIdle(), "should be one idle");
>     genericObjectPool.setMaxIdle(-1);
>     genericObjectPool.addObject();
>     genericObjectPool.addObject();
>     genericObjectPool.addObject();
>     assertEquals(4, genericObjectPool.getNumIdle(), "should be four idle");
> }
> {code}
> h3. Test Case 2: Concurrent (Race Condition)
> {code:java}
> @Test
> @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
> void testAddObjectConcurrentCallsRespectsMaxIdle() throws Exception {
>     final int maxIdleLimit = 5;
>     final int numThreads = 10;
>     genericObjectPool.setMaxIdle(maxIdleLimit);
>     final CountDownLatch startLatch = new CountDownLatch(1);
>     final CountDownLatch doneLatch = new CountDownLatch(numThreads);
>     List<Runnable> tasks = getRunnables(numThreads, startLatch, doneLatch);
>     ExecutorService executorService = 
> Executors.newFixedThreadPool(numThreads);
>     tasks.forEach(executorService::submit);
>     try {
>         startLatch.countDown(); // Start all threads simultaneously
>         doneLatch.await(); // Wait for all threads to complete
>     } finally {
>         executorService.shutdown();
>         assertTrue(executorService.awaitTermination(Long.MAX_VALUE, 
> TimeUnit.NANOSECONDS));
>     }
>     assertTrue(genericObjectPool.getNumIdle() <= maxIdleLimit,
>         "Concurrent addObject() calls should not exceed maxIdle limit of " + 
> maxIdleLimit +
>             ", but found " + genericObjectPool.getNumIdle() + " idle 
> objects");
> }
> private List<Runnable> getRunnables(final int numThreads,
>                                     final CountDownLatch startLatch,
>                                     final CountDownLatch doneLatch) {
>     List<Runnable> tasks = new ArrayList<>();
>     for(int i = 0; i < numThreads; i++) {
>         tasks.add(() -> {
>             try {
>                 startLatch.await(); // Wait for all threads to be ready
>                 genericObjectPool.addObject();
>             } catch (Exception e) {
>                 Thread.currentThread().interrupt(); // Restore interrupt 
> status
>             } finally {
>                 doneLatch.countDown();
>             }
>         });
>     }
>     return tasks;
> }
> {code}
> h2. Proposed Fix
> The pool should ensure to *the minimum of these two values* as the effective 
> limit when deciding whether to add new objects. This ensures the pool 
> respects whichever limit is lower and prevents violating either configuration 
> constraint.
> See PR for the fix implementation: 
> [https://github.com/apache/commons-pool/pull/450]
> And see this PR for running the tests :- 
> https://github.com/apache/commons-pool/pull/451



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to