This is an automated email from the ASF dual-hosted git repository. psteitz pushed a commit to branch POOL_2_X in repository https://gitbox.apache.org/repos/asf/commons-pool.git
The following commit(s) were added to refs/heads/POOL_2_X by this push: new 1c4b1deb JIRA: POOL-420 Correct time-keeping in GKOP create to eliminate possibility of configured maxWait on borrow to be exceeded. Also add Duration-based borrow method and change addObject to return immediately (rather than potentially waiting up to maxWaitDuration) if the pool has no capacity to create when this method is invoked. new 5769a311 Merge branch 'POOL_2_X' of https://github.com/apache/commons-pool into POOL_2_X 1c4b1deb is described below commit 1c4b1deb3656e611e07161b22ec6ea14cf6ea70a Author: Phil Steitz <phil.ste...@gmail.com> AuthorDate: Mon May 19 16:44:18 2025 -0700 JIRA: POOL-420 Correct time-keeping in GKOP create to eliminate possibility of configured maxWait on borrow to be exceeded. Also add Duration-based borrow method and change addObject to return immediately (rather than potentially waiting up to maxWaitDuration) if the pool has no capacity to create when this method is invoked. --- src/changes/changes.xml | 3 + .../commons/pool2/impl/BaseGenericObjectPool.java | 26 +++++ .../commons/pool2/impl/GenericKeyedObjectPool.java | 124 ++++++++++++++++++--- .../commons/pool2/impl/GenericObjectPool.java | 11 +- .../pool2/impl/TestGenericKeyedObjectPool.java | 52 ++++++++- 5 files changed, 183 insertions(+), 33 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d56cd543..c978ef83 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -47,6 +47,9 @@ The <action> type attribute can be add,update,fix,remove. <body> <release version="2.12.2" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required."> <!-- FIX --> + <action type="fix" issue="POOL-420" dev="psteitz" due-to="Phil Steitz">The maximum wait time for GenericKeyedObjectPool.borrowObject(*) may exceed configured maximum wait time. This is the same issue as POOL-418, but for GKOP. + Also included in this fix is a change to addObject that prevents it from waiting for capacity to create. That method now returns immediately when there is no capcity to add to the pool under the given key. + </action> <action type="fix" dev="ggregory" due-to="Gary Gregory">Remove -nouses directive from maven-bundle-plugin. OSGi package imports now state 'uses' definitions for package imports, this doesn't affect JPMS (from org.apache.commons:commons-parent:80).</action> <action type="fix" issue="POOL-418" dev="ggregory" due-to="Gary Gregory">The maximum wait time for GenericObjectPool.borrowObject(*) may exceed expectations due to a spurious thread wakeup. The remaining duration was incorrectly calculated and the method did not end up waiting long enough. diff --git a/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java index 4efde590..573bf006 100644 --- a/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java +++ b/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java @@ -1938,6 +1938,32 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject implements Aut } } + /** + * Returns the duration since the given start time. + * + * @param startInstant the start time + * @return the duration since the given start time + */ + final Duration durationSince(final Instant startInstant) { + return Duration.between(startInstant, Instant.now()); + } + + /** + * Waits for notification on the given object for the specified duration. + * Duration.ZERO causes the thread to wait indefinitely. + * + * @param obj the object to wait on + * @param duration the duration to wait + * @throws InterruptedException if interrupted while waiting + * @throws IllegalArgumentException if the duration is negative + */ + final void wait(final Object obj, final Duration duration) throws InterruptedException { + if (!duration.isNegative()) { + obj.wait(duration.toMillis(), duration.getNano() % 1_000_000); + } + } + + /** * Stops the evictor. */ diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java index df367e32..c6ce6f62 100644 --- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java +++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java @@ -29,7 +29,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -330,9 +329,18 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> @Override public void addObject(final K key) throws Exception { assertOpen(); - register(key); + final ObjectDeque<T> objectDeque = register(key); try { - addIdleObject(key, create(key)); + // Attempt create and add only if there is capacity to add + // > to the overall instance count + // > to the pool under the key + final int maxtTotalPerKey = getMaxTotalPerKey(); + final int maxTotal = getMaxTotal(); + if ((maxTotal < 0 || getNumActive() + getNumIdle() < maxTotal) + && (maxtTotalPerKey < 0 || objectDeque.allObjects.size() < maxtTotalPerKey)) { + // Attempt to create and add a new instance under key + addIdleObject(key, create(key, getMaxWaitDuration())); + } } finally { deregister(key); } @@ -400,8 +408,8 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> * </p> * * @param key pool key - * @param borrowMaxWaitMillis The time to wait in milliseconds for an object - * to become available + * @param maxWaitDuration The time to wait for an object to become + * available * * @return object instance from the keyed pool * @throws NoSuchElementException if a keyed object instance cannot be @@ -409,10 +417,12 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> * * @throws Exception if a keyed object instance cannot be returned due to an * error + * @since 2.12.2 */ - public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception { + public T borrowObject(final K key, final Duration maxWaitDuration) throws Exception { assertOpen(); - + final Instant startInstant = Instant.now(); + Duration remainingWaitDuration = maxWaitDuration; final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && getNumActive() > getMaxTotal() - 3) { @@ -426,27 +436,28 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; - final Instant waitTime = Instant.now(); final ObjectDeque<T> objectDeque = register(key); try { while (p == null) { + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); create = false; p = objectDeque.getIdleObjects().pollFirst(); if (p == null) { - p = create(key); + p = create(key, remainingWaitDuration); if (!PooledObject.isNull(p)) { create = true; } + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); } if (blockWhenExhausted) { if (PooledObject.isNull(p)) { - p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst() - : objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); + p = maxWaitDuration.isNegative() ? objectDeque.getIdleObjects().takeFirst() + : objectDeque.getIdleObjects().pollFirst(remainingWaitDuration); } if (PooledObject.isNull(p)) { throw new NoSuchElementException(appendStats( - "Timeout waiting for idle object, borrowMaxWaitMillis=" + borrowMaxWaitMillis)); + "Timeout waiting for idle object, borrowMaxWaitMillis=" + maxWaitDuration.toMillis())); } } else if (PooledObject.isNull(p)) { throw new NoSuchElementException(appendStats("Pool exhausted")); @@ -466,7 +477,8 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> } p = null; if (create) { - final NoSuchElementException nsee = new NoSuchElementException(appendStats("Unable to activate object")); + final NoSuchElementException nsee = new NoSuchElementException( + appendStats("Unable to activate object")); nsee.initCause(e); throw nsee; } @@ -502,11 +514,78 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> deregister(key); } - updateStatsBorrow(p, Duration.between(waitTime, Instant.now())); + updateStatsBorrow(p, Duration.between(startInstant, Instant.now())); return p.getObject(); } + + /** + * Borrows an object from the sub-pool associated with the given key using + * the specified waiting time which only applies if + * {@link #getBlockWhenExhausted()} is true. + * <p> + * If there is one or more idle instances available in the sub-pool + * associated with the given key, then an idle instance will be selected + * based on the value of {@link #getLifo()}, activated and returned. If + * activation fails, or {@link #getTestOnBorrow() testOnBorrow} is set to + * {@code true} and validation fails, the instance is destroyed and the + * next available instance is examined. This continues until either a valid + * instance is returned or there are no more idle instances available. + * </p> + * <p> + * If there are no idle instances available in the sub-pool associated with + * the given key, behavior depends on the {@link #getMaxTotalPerKey() + * maxTotalPerKey}, {@link #getMaxTotal() maxTotal}, and (if applicable) + * {@link #getBlockWhenExhausted()} and the value passed in to the + * {@code borrowMaxWaitMillis} parameter. If the number of instances checked + * out from the sub-pool under the given key is less than + * {@code maxTotalPerKey} and the total number of instances in + * circulation (under all keys) is less than {@code maxTotal}, a new + * instance is created, activated and (if applicable) validated and returned + * to the caller. If validation fails, a {@code NoSuchElementException} + * will be thrown. If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + * </p> + * <p> + * If the associated sub-pool is exhausted (no available idle instances and + * no capacity to create new ones), this method will either block + * ({@link #getBlockWhenExhausted()} is true) or throw a + * {@code NoSuchElementException} + * ({@link #getBlockWhenExhausted()} is false). + * The length of time that this method will block when + * {@link #getBlockWhenExhausted()} is true is determined by the value + * passed in to the {@code borrowMaxWait} parameter. + * </p> + * <p> + * When {@code maxTotal} is set to a positive value and this method is + * invoked when at the limit with no idle instances available under the requested + * key, an attempt is made to create room by clearing the oldest 15% of the + * elements from the keyed sub-pools. + * </p> + * <p> + * When the pool is exhausted, multiple calling threads may be + * simultaneously blocked waiting for instances to become available. A + * "fairness" algorithm has been implemented to ensure that threads receive + * available instances in request arrival order. + * </p> + * + * @param key pool key + * @param maxWaitMillis The time to wait in milliseconds for an object to become + * available + * + * @return object instance from the keyed pool + * @throws NoSuchElementException if a keyed object instance cannot be + * returned because the pool is exhausted. + * + * @throws Exception if a keyed object instance cannot be returned due to an + * error + */ + + public T borrowObject(final K key, final long maxWaitMillis) throws Exception { + return borrowObject(key, Duration.ofMillis(maxWaitMillis)); + } + /** * Calculate the number of objects that need to be created to attempt to * maintain the minimum number of idle objects while not exceeded the limits @@ -708,11 +787,16 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> /** * Creates a new pooled object or null. * - * @param key Key associated with new pooled object. + * @param key Key associated with new pooled object. + * @param maxWaitDuration The time to wait in this method. If negative or ZERO, + * this method may wait indefinitely. * @return The new, wrapped pooled object. May return null. * @throws Exception If the objection creation fails. */ - private PooledObject<T> create(final K key) throws Exception { + private PooledObject<T> create(final K key, Duration maxWaitDuration) throws Exception { + final Instant startInstant = Instant.now(); + Duration remainingWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration; + int maxTotalPerKeySave = getMaxTotalPerKey(); // Per key if (maxTotalPerKeySave < 0) { maxTotalPerKeySave = Integer.MAX_VALUE; @@ -744,6 +828,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> // call the factory Boolean create = null; while (create == null) { + remainingWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration.minus(durationSince(startInstant)); synchronized (objectDeque.makeObjectCountLock) { final long newCreateCount = objectDeque.getCreateCount().incrementAndGet(); // Check against the per key limit @@ -762,7 +847,10 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> // bring the pool to capacity. Those calls might also // fail so wait until they complete and then re-test if // the pool is at capacity or not. - objectDeque.makeObjectCountLock.wait(); + if (!remainingWaitDuration.isNegative()) { + objectDeque.makeObjectCountLock.wait(remainingWaitDuration.toMillis(), + remainingWaitDuration.getNano() % 1_000_000); + } } } else { // The pool is not at capacity. Create a new object. @@ -1571,7 +1659,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T> try { // If there is no capacity to add, create will return null // and addIdleObject will no-op. - addIdleObject(mostLoadedKey, create(mostLoadedKey)); + addIdleObject(mostLoadedKey, create(mostLoadedKey, Duration.ZERO)); } catch (final Exception e) { swallowException(e); } finally { diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java index 8421062a..3a88431a 100644 --- a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java +++ b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java @@ -84,12 +84,6 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T> private static final String ONAME_BASE = "org.apache.commons.pool2:type=GenericObjectPool,name="; - private static void wait(final Object obj, final Duration duration) throws InterruptedException { - if (!duration.isNegative()) { - obj.wait(duration.toMillis(), duration.getNano() % 1_000_000); - } - } - private volatile String factoryType; private volatile int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; @@ -491,7 +485,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T> * If the factory makeObject returns null, this method throws a NullPointerException. * </p> * - * @param maxWaitDuration The time to wait for an object to become available. + * @param maxWaitDuration The time to wait for capacity to create * @return The new wrapped pooled object or null. * @throws Exception if the object factory's {@code makeObject} fails */ @@ -600,9 +594,6 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T> } } - private Duration durationSince(final Instant startInstant) { - return Duration.between(startInstant, Instant.now()); - } /** * Tries to ensure that {@code idleCount} idle instances exist in the pool. diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java index 52f58646..82aff064 100644 --- a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java +++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.lang.management.ManagementFactory; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -315,11 +316,11 @@ public class TestGenericKeyedObjectPool extends AbstractTestKeyedObjectPool { @Override public void run() { - try { - pool.returnObject(key, pool.borrowObject(key)); - } catch (final Exception e) { - // Ignore - } + try { + pool.returnObject(key, pool.borrowObject(key)); + } catch (Exception e) { + // Ignore + } } } @@ -2045,6 +2046,47 @@ public class TestGenericKeyedObjectPool extends AbstractTestKeyedObjectPool { assertThrows(NoSuchElementException.class, () -> gkoPool.borrowObject("a")); } + /** + * JIRA: POOL-420 (clone of POOL-418 for GKOP) + * + * Test to make sure that a client thread that triggers a create that fails does + * not wait longer than the maxWait time. + * + * Bug was that the time spent waiting for the create to complete was not being + * counted against the maxWait time. + */ + @Test + public void testMaxWaitTimeOutOnTime() throws Exception { + final Duration maxWaitDuration = Duration.ofSeconds(1); + final SimpleFactory<String> factory = new SimpleFactory<>(); + factory.makeLatency = 500; + factory.setValidationEnabled(true); // turn on factory-level validatiom + factory.setValid(false); // make validation fail uniformly + final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory); + + pool.setBlockWhenExhausted(true); + pool.setMaxWait(maxWaitDuration); + pool.setMaxTotalPerKey(1); + pool.setMaxTotal(1); + pool.setTestOnCreate(true); + final Instant startTime = Instant.now(); + + // Try to borrow an object. Validation will fail. + // Then we will wait on the pool. + try { + pool.borrowObject("a"); + } catch (NoSuchElementException ex) { + // expected + } + + // Should have timeed out after 1000 ms from the start time + final Duration duration = Duration.between(startTime, Instant.now()); + assertTrue(duration.toMillis() < maxWaitDuration.toMillis() + 10, // allow for some timing delay + "Thread A should have timed out after " + maxWaitDuration.toMillis() + " ms, but took " + duration.toMillis() + " ms"); + pool.close(); + } + + /* * Test multi-threaded pool access. Multiple keys, multiple threads, but maxActive only allows half the threads to succeed. *