Raju Gupta created POOL-426:
-------------------------------

             Summary: GenericObjectPool does not respect minIdle configuration
                 Key: POOL-426
                 URL: https://issues.apache.org/jira/browse/POOL-426
             Project: Commons Pool
          Issue Type: Bug
            Reporter: Raju Gupta


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 :- 



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

Reply via email to