Skip to content

Commit 380ecd9

Browse files
committed
GenericObjectPool.borrowObject(Duration) doesn't obey its borrowMaxWait
Duration argument when the argument is different from GenericObjectPool.getMaxWaitDuration()
1 parent 555182d commit 380ecd9

File tree

3 files changed

+103
-34
lines changed

3 files changed

+103
-34
lines changed

src/changes/changes.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ The <action> type attribute can be add,update,fix,remove.
6464
Add ReslientPooledObjectFactory to provide resilence against factory outages.
6565
</action>
6666
</release>
67+
<release version="2.12.1" date="YYYY-MM-DD" description="This is a feature and maintenance release (Java 8 or above).">
68+
<!-- FIX -->
69+
<action type="fix" dev="ggregory" due-to="Gary Gregory">Use java.time.Instant precision in org.apache.commons.pool2.impl.ThrowableCallStack.Snapshot throwable message.</action>
70+
<action type="fix" dev="ggregory" due-to="Gary Gregory">GenericObjectPool.borrowObject(Duration) doesn't obey its borrowMaxWait Duration argument when the argument is different from GenericObjectPool.getMaxWaitDuration().</action>
71+
<!-- ADD -->
72+
<!-- UPDATE -->
73+
<action type="update" dev="ggregory">Bump org.apache.commons:commons-parent from 62 to 73.</action>
74+
<action type="update" dev="ggregory">Bump org.ow2.asm:asm-util from 9.5 to 9.7.</action>
75+
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump commons-lang3 from 3.13.0 to 3.16.0.</action>
76+
<action type="update" dev="ggregory" due-to="Gary Gregory">[site] Bump org.apache.bcel:bcel from 6.7.0 to 6.8.2.</action>
77+
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump org.hamcrest:hamcrest-library from 2.2 to 3.0.</action>
78+
</release>
6779
<release version="2.12.0" date="2023-MM-DD" description="This is a feature and maintenance release (Java 8 or above).">
6880
<!-- FIX -->
6981
<action dev="psteitz" type="fix" issue="POOL-401">

src/main/java/org/apache/commons/pool3/impl/GenericObjectPool.java

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void addObject() throws E {
220220
if (factory == null) {
221221
throw new IllegalStateException("Cannot add objects without a factory.");
222222
}
223-
addIdleObject(create());
223+
addIdleObject(create(getMaxWaitDuration()));
224224
}
225225

226226
/**
@@ -274,61 +274,57 @@ public T borrowObject() throws E {
274274
* available instances in request arrival order.
275275
* </p>
276276
*
277-
* @param borrowMaxWaitDuration The time to wait for an object
277+
* @param maxWaitDuration The time to wait for an object
278278
* to become available
279279
*
280280
* @return object instance from the pool
281281
* @throws NoSuchElementException if an instance cannot be returned
282282
* @throws E if an object instance cannot be returned due to an error
283283
* @since 2.10.0
284284
*/
285-
public T borrowObject(final Duration borrowMaxWaitDuration) throws E {
285+
public T borrowObject(final Duration maxWaitDuration) throws E {
286286
assertOpen();
287-
287+
final Instant startInstant = Instant.now();
288+
final boolean negativeDuration = maxWaitDuration.isNegative();
289+
Duration remainingWaitDuration = maxWaitDuration;
288290
final AbandonedConfig ac = this.abandonedConfig;
289-
if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 &&
290-
getNumActive() > getMaxTotal() - 3) {
291+
if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && getNumActive() > getMaxTotal() - 3) {
291292
removeAbandoned(ac);
292293
}
293-
294294
PooledObject<T> p = null;
295-
296295
// Get local copy of current config so it is consistent for entire
297296
// method execution
298297
final boolean blockWhenExhausted = getBlockWhenExhausted();
299-
300298
boolean create;
301-
final Instant waitTime = Instant.now();
302-
303299
while (p == null) {
300+
remainingWaitDuration = remainingWaitDuration.minus(durationSince(startInstant));
304301
create = false;
305302
p = idleObjects.pollFirst();
306303
if (p == null) {
307-
p = create();
304+
p = create(remainingWaitDuration);
308305
if (!PooledObject.isNull(p)) {
309306
create = true;
310307
}
311308
}
312309
if (blockWhenExhausted) {
313310
if (PooledObject.isNull(p)) {
314311
try {
315-
p = borrowMaxWaitDuration.isNegative() ? idleObjects.takeFirst() : idleObjects.pollFirst(borrowMaxWaitDuration);
312+
p = negativeDuration ? idleObjects.takeFirst() : idleObjects.pollFirst(maxWaitDuration);
316313
} catch (final InterruptedException e) {
317314
// Don't surface exception type of internal locking mechanism.
318315
throw cast(e);
319316
}
320317
}
321318
if (PooledObject.isNull(p)) {
322319
throw new NoSuchElementException(appendStats(
323-
"Timeout waiting for idle object, borrowMaxWaitDuration=" + borrowMaxWaitDuration));
320+
"Timeout waiting for idle object, borrowMaxWaitDuration=" + remainingWaitDuration));
324321
}
325322
} else if (PooledObject.isNull(p)) {
326323
throw new NoSuchElementException(appendStats("Pool exhausted"));
327324
}
328325
if (!p.allocate()) {
329326
p = null;
330327
}
331-
332328
if (!PooledObject.isNull(p)) {
333329
try {
334330
factory.activateObject(p);
@@ -373,9 +369,7 @@ public T borrowObject(final Duration borrowMaxWaitDuration) throws E {
373369
}
374370
}
375371
}
376-
377-
updateStatsBorrow(p, Duration.between(waitTime, Instant.now()));
378-
372+
updateStatsBorrow(p, durationSince(startInstant));
379373
return p.getObject();
380374
}
381375

@@ -510,19 +504,19 @@ public void close() {
510504
* If the factory makeObject returns null, this method throws a NullPointerException.
511505
* </p>
512506
*
507+
* @param maxWaitDuration The time to wait for an object to become available.
513508
* @return The new wrapped pooled object or null.
514509
* @throws E if the object factory's {@code makeObject} fails
515510
*/
516-
private PooledObject<T> create() throws E {
511+
private PooledObject<T> create(final Duration maxWaitDuration) throws E {
517512
int localMaxTotal = getMaxTotal();
518513
// This simplifies the code later in this method
519514
if (localMaxTotal < 0) {
520515
localMaxTotal = Integer.MAX_VALUE;
521516
}
522517

523518
final Instant localStartInstant = Instant.now();
524-
final Duration maxWaitDurationRaw = getMaxWaitDuration();
525-
final Duration localMaxWaitDuration = maxWaitDurationRaw.isNegative() ? Duration.ZERO : maxWaitDurationRaw;
519+
final Duration localMaxWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration;
526520

527521
// Flag that indicates if create should:
528522
// - TRUE: call the factory to create an object
@@ -561,9 +555,9 @@ private PooledObject<T> create() throws E {
561555
}
562556
}
563557

564-
// Do not block more if localMaxWaitDuration is set.
558+
// Do not block more if localMaxWaitDuration > 0.
565559
if (create == null && localMaxWaitDuration.compareTo(Duration.ZERO) > 0 &&
566-
Duration.between(localStartInstant, Instant.now()).compareTo(localMaxWaitDuration) >= 0) {
560+
durationSince(localStartInstant).compareTo(localMaxWaitDuration) >= 0) {
567561
create = Boolean.FALSE;
568562
}
569563
}
@@ -600,10 +594,14 @@ private PooledObject<T> create() throws E {
600594
}
601595

602596
createdCount.incrementAndGet();
603-
allObjects.put(IdentityWrapper.on(p), p);
597+
allObjects.put(new IdentityWrapper<>(p.getObject()), p);
604598
return p;
605599
}
606600

601+
private Duration durationSince(final Instant startInstant) {
602+
return Duration.between(startInstant, Instant.now());
603+
}
604+
607605
/**
608606
* Destroys a wrapped pooled object.
609607
*
@@ -648,7 +646,7 @@ private void ensureIdle(final int idleCount, final boolean always) throws E {
648646
}
649647

650648
while (idleObjects.size() < idleCount) {
651-
final PooledObject<T> p = create();
649+
final PooledObject<T> p = create(getMaxWaitDuration());
652650
if (PooledObject.isNull(p)) {
653651
// Can't create objects, no reason to think another call to
654652
// create will work. Give up.

src/test/java/org/apache/commons/pool3/impl/TestGenericObjectPool.java

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import javax.management.MBeanServer;
5252
import javax.management.ObjectName;
5353

54+
import org.apache.commons.lang3.time.DurationUtils;
5455
import org.apache.commons.pool3.BasePooledObjectFactory;
5556
import org.apache.commons.pool3.ObjectPool;
5657
import org.apache.commons.pool3.PoolUtils;
@@ -1070,6 +1071,52 @@ public void testBorrowObjectFairness() throws Exception {
10701071
}
10711072
}
10721073

1074+
@Test/* maxWaitMillis x2 + padding */
1075+
@Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
1076+
public void testBorrowObjectOverrideMaxWaitLarge() throws Exception {
1077+
try (final GenericObjectPool<String, InterruptedException> pool = new GenericObjectPool<>(createSlowObjectFactory(60_000))) {
1078+
pool.setMaxTotal(1);
1079+
pool.setMaxWait(Duration.ofMillis(1_000)); // large
1080+
pool.setBlockWhenExhausted(false);
1081+
// thread1 tries creating a slow object to make pool full.
1082+
final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(pool, 0);
1083+
thread1.start();
1084+
// Wait for thread1's reaching to create().
1085+
Thread.sleep(100);
1086+
// The test needs to make sure a wait happens in create().
1087+
final Duration d = DurationUtils.of(() -> assertThrows(NoSuchElementException.class, () -> pool.borrowObject(Duration.ofMillis(1)),
1088+
"borrowObject must fail quickly due to timeout parameter"));
1089+
final long millis = d.toMillis();
1090+
final long nanos = d.toNanos();
1091+
assertTrue(nanos > 0, () -> "borrowObject(Duration) argument not respected: " + nanos);
1092+
assertTrue(millis >= 0, () -> "borrowObject(Duration) argument not respected: " + millis); // not > 0 to account for spurious waits
1093+
assertTrue(millis < 50, () -> "borrowObject(Duration) argument not respected: " + millis);
1094+
}
1095+
}
1096+
1097+
@Test/* maxWaitMillis x2 + padding */
1098+
@Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
1099+
public void testBorrowObjectOverrideMaxWaitSmall() throws Exception {
1100+
try (final GenericObjectPool<String, InterruptedException> pool = new GenericObjectPool<>(createSlowObjectFactory(60_000))) {
1101+
pool.setMaxTotal(1);
1102+
pool.setMaxWait(Duration.ofMillis(1)); // small
1103+
pool.setBlockWhenExhausted(false);
1104+
// thread1 tries creating a slow object to make pool full.
1105+
final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(pool, 0);
1106+
thread1.start();
1107+
// Wait for thread1's reaching to create().
1108+
Thread.sleep(100);
1109+
// The test needs to make sure a wait happens in create().
1110+
final Duration d = DurationUtils.of(() -> assertThrows(NoSuchElementException.class, () -> pool.borrowObject(Duration.ofMillis(500)),
1111+
"borrowObject must fail slowly due to timeout parameter"));
1112+
final long millis = d.toMillis();
1113+
final long nanos = d.toNanos();
1114+
assertTrue(nanos > 0, () -> "borrowObject(Duration) argument not respected: " + nanos);
1115+
assertTrue(millis >= 0, () -> "borrowObject(Duration) argument not respected: " + millis); // not > 0 to account for spurious waits
1116+
assertTrue(millis < 600, () -> "borrowObject(Duration) argument not respected: " + millis);
1117+
assertTrue(millis > 490, () -> "borrowObject(Duration) argument not respected: " + millis);
1118+
}
1119+
}
10731120
@Test
10741121
public void testBorrowTimings() throws Exception {
10751122
// Borrow
@@ -2642,28 +2689,40 @@ public void testPreparePool() throws Exception {
26422689
assertEquals(1, genericObjectPool.getNumIdle());
26432690
}
26442691

2692+
@Test/* maxWaitMillis x2 + padding */
2693+
@Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
2694+
public void testReturnBorrowObjectWithingMaxWaitDuration() throws Exception {
2695+
final Duration maxWaitDuration = Duration.ofMillis(500);
2696+
try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(createSlowObjectFactory(60_000))) {
2697+
createSlowObjectFactoryPool.setMaxTotal(1);
2698+
createSlowObjectFactoryPool.setMaxWait(maxWaitDuration);
2699+
// thread1 tries creating a slow object to make pool full.
2700+
final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createSlowObjectFactoryPool, 0);
2701+
thread1.start();
2702+
// Wait for thread1's reaching to create().
2703+
Thread.sleep(100);
2704+
// another one tries borrowObject. It should return within maxWaitMillis.
2705+
assertThrows(NoSuchElementException.class, () -> createSlowObjectFactoryPool.borrowObject(maxWaitDuration),
2706+
"borrowObject must fail due to timeout by maxWaitMillis");
2707+
assertTrue(thread1.isAlive());
2708+
}
2709+
}
2710+
26452711
@Test /* maxWaitMillis x2 + padding */
26462712
@Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
26472713
public void testReturnBorrowObjectWithingMaxWaitMillis() throws Exception {
26482714
final long maxWaitMillis = 500;
2649-
2650-
try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(
2651-
createSlowObjectFactory(60000))) {
2715+
try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(createSlowObjectFactory(60000))) {
26522716
createSlowObjectFactoryPool.setMaxTotal(1);
26532717
createSlowObjectFactoryPool.setMaxWait(Duration.ofMillis(maxWaitMillis));
2654-
26552718
// thread1 tries creating a slow object to make pool full.
2656-
final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createSlowObjectFactoryPool,
2657-
0);
2719+
final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createSlowObjectFactoryPool, 0);
26582720
thread1.start();
2659-
26602721
// Wait for thread1's reaching to create().
26612722
Thread.sleep(100);
2662-
26632723
// another one tries borrowObject. It should return within maxWaitMillis.
26642724
assertThrows(NoSuchElementException.class, () -> createSlowObjectFactoryPool.borrowObject(maxWaitMillis),
26652725
"borrowObject must fail due to timeout by maxWaitMillis");
2666-
26672726
assertTrue(thread1.isAlive());
26682727
}
26692728
}

0 commit comments

Comments
 (0)