You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2021/05/26 19:38:21 UTC
[commons-pool] branch master updated: Implement AbandonedConfig for
GenericKeyedObjectPool (#67)
This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-pool.git
The following commit(s) were added to refs/heads/master by this push:
new bf2c991 Implement AbandonedConfig for GenericKeyedObjectPool (#67)
bf2c991 is described below
commit bf2c9910d51b85e50c3731b637f94a440b25236f
Author: JSurf <js...@gmx.de>
AuthorDate: Wed May 26 21:38:13 2021 +0200
Implement AbandonedConfig for GenericKeyedObjectPool (#67)
* Implement AbandonedConfig for GenericKeyedObjectPool
* Add UsageTracking
* Add since tags
* Add defaults
Co-authored-by: jviebig <je...@vitec.com>
---
.../commons/pool2/impl/GenericKeyedObjectPool.java | 409 +++++++++++++++-----
.../pool2/impl/GenericKeyedObjectPoolMXBean.java | 45 +++
.../pool2/impl/TestAbandonedKeyedObjectPool.java | 413 +++++++++++++++++++++
.../proxy/BaseTestProxiedKeyedObjectPool.java | 43 ++-
4 files changed, 814 insertions(+), 96 deletions(-)
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 50c7f2b..a6f585b 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
@@ -25,6 +25,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -41,6 +42,7 @@ import org.apache.commons.pool2.PoolUtils;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectState;
import org.apache.commons.pool2.SwallowedExceptionListener;
+import org.apache.commons.pool2.UsageTracking;
/**
* A configurable {@code KeyedObjectPool} implementation.
@@ -85,7 +87,7 @@ import org.apache.commons.pool2.SwallowedExceptionListener;
* @since 2.0
*/
public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
- implements KeyedObjectPool<K, T>, GenericKeyedObjectPoolMXBean<K> {
+ implements KeyedObjectPool<K, T>, GenericKeyedObjectPoolMXBean<K>, UsageTracking<T> {
/**
* Maintains information on the per key queue for a given key.
@@ -183,6 +185,8 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
+ private static final Duration DEFAULT_REMOVE_ABANDONED_TIMEOUT = Duration.ofSeconds(Integer.MAX_VALUE);
+
// JMX specific attributes
private static final String ONAME_BASE =
"org.apache.commons.pool2:type=GenericKeyedObjectPool,name=";
@@ -233,6 +237,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
private K evictionKey = null; // @GuardedBy("evictionLock")
+ // Additional configuration properties for abandoned object tracking
+ private volatile AbandonedConfig abandonedConfig;
+
/**
* Create a new {@code GenericKeyedObjectPool} using defaults from
* {@link GenericKeyedObjectPoolConfig}.
@@ -268,6 +275,26 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
/**
+ * Creates a new {@code GenericKeyedObjectPool} that tracks and destroys
+ * objects that are checked out, but never returned to the pool.
+ *
+ * @param factory The object factory to be used to create object instances
+ * used by this pool
+ * @param config The base pool configuration to use for this pool instance.
+ * The configuration is used by value. Subsequent changes to
+ * the configuration object will not be reflected in the
+ * pool.
+ * @param abandonedConfig Configuration for abandoned object identification
+ * and removal. The configuration is used by value.
+ * @since 2.10.0
+ */
+ public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T> factory,
+ final GenericKeyedObjectPoolConfig<T> config, final AbandonedConfig abandonedConfig) {
+ this(factory, config);
+ setAbandonedConfig(abandonedConfig);
+ }
+
+ /**
* Add an object to the set of idle objects for a given key.
*
* @param key The key to associate with the idle object
@@ -385,6 +412,12 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception {
assertOpen();
+ final AbandonedConfig ac = this.abandonedConfig;
+ if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) &&
+ (getNumActive() > getMaxTotal() - 3)) {
+ removeAbandoned(ac);
+ }
+
PooledObject<T> p = null;
// Get local copy of current config so it is consistent for entire
@@ -767,6 +800,12 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
}
+ final AbandonedConfig ac = this.abandonedConfig;
+ if (ac != null && ac.getLogAbandoned()) {
+ p.setLogAbandoned(true);
+ p.setRequireFullStackTrace(ac.getRequireFullStackTrace());
+ }
+
createdCount.incrementAndGet();
objectDeque.getAllObjects().put(new IdentityWrapper<>(p.getObject()), p);
return p;
@@ -909,123 +948,126 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
public void evict() throws Exception {
assertOpen();
- if (getNumIdle() == 0) {
- return;
- }
+ if (getNumIdle() > 0) {
- PooledObject<T> underTest = null;
- final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();
+ PooledObject<T> underTest = null;
+ final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();
- synchronized (evictionLock) {
- final EvictionConfig evictionConfig = new EvictionConfig(
- getMinEvictableIdleTime(),
- getSoftMinEvictableIdleTime(),
- getMinIdlePerKey());
+ synchronized (evictionLock) {
+ final EvictionConfig evictionConfig = new EvictionConfig(
+ getMinEvictableIdleTime(),
+ getSoftMinEvictableIdleTime(),
+ getMinIdlePerKey());
- final boolean testWhileIdle = getTestWhileIdle();
+ final boolean testWhileIdle = getTestWhileIdle();
- for (int i = 0, m = getNumTests(); i < m; i++) {
- if(evictionIterator == null || !evictionIterator.hasNext()) {
- if (evictionKeyIterator == null ||
- !evictionKeyIterator.hasNext()) {
- final List<K> keyCopy = new ArrayList<>();
- final Lock readLock = keyLock.readLock();
- readLock.lock();
- try {
- keyCopy.addAll(poolKeyList);
- } finally {
- readLock.unlock();
- }
- evictionKeyIterator = keyCopy.iterator();
- }
- while (evictionKeyIterator.hasNext()) {
- evictionKey = evictionKeyIterator.next();
- final ObjectDeque<T> objectDeque = poolMap.get(evictionKey);
- if (objectDeque == null) {
- continue;
+ for (int i = 0, m = getNumTests(); i < m; i++) {
+ if(evictionIterator == null || !evictionIterator.hasNext()) {
+ if (evictionKeyIterator == null ||
+ !evictionKeyIterator.hasNext()) {
+ final List<K> keyCopy = new ArrayList<>();
+ final Lock readLock = keyLock.readLock();
+ readLock.lock();
+ try {
+ keyCopy.addAll(poolKeyList);
+ } finally {
+ readLock.unlock();
+ }
+ evictionKeyIterator = keyCopy.iterator();
}
+ while (evictionKeyIterator.hasNext()) {
+ evictionKey = evictionKeyIterator.next();
+ final ObjectDeque<T> objectDeque = poolMap.get(evictionKey);
+ if (objectDeque == null) {
+ continue;
+ }
- final Deque<PooledObject<T>> idleObjects = objectDeque.getIdleObjects();
- evictionIterator = new EvictionIterator(idleObjects);
- if (evictionIterator.hasNext()) {
- break;
+ final Deque<PooledObject<T>> idleObjects = objectDeque.getIdleObjects();
+ evictionIterator = new EvictionIterator(idleObjects);
+ if (evictionIterator.hasNext()) {
+ break;
+ }
+ evictionIterator = null;
}
+ }
+ if (evictionIterator == null) {
+ // Pools exhausted
+ return;
+ }
+ final Deque<PooledObject<T>> idleObjects;
+ try {
+ underTest = evictionIterator.next();
+ idleObjects = evictionIterator.getIdleObjects();
+ } catch (final NoSuchElementException nsee) {
+ // Object was borrowed in another thread
+ // Don't count this as an eviction test so reduce i;
+ i--;
evictionIterator = null;
+ continue;
}
- }
- if (evictionIterator == null) {
- // Pools exhausted
- return;
- }
- final Deque<PooledObject<T>> idleObjects;
- try {
- underTest = evictionIterator.next();
- idleObjects = evictionIterator.getIdleObjects();
- } catch (final NoSuchElementException nsee) {
- // Object was borrowed in another thread
- // Don't count this as an eviction test so reduce i;
- i--;
- evictionIterator = null;
- continue;
- }
- if (!underTest.startEvictionTest()) {
- // Object was borrowed in another thread
- // Don't count this as an eviction test so reduce i;
- i--;
- continue;
- }
+ if (!underTest.startEvictionTest()) {
+ // Object was borrowed in another thread
+ // Don't count this as an eviction test so reduce i;
+ i--;
+ continue;
+ }
- // User provided eviction policy could throw all sorts of
- // crazy exceptions. Protect against such an exception
- // killing the eviction thread.
- boolean evict;
- try {
- evict = evictionPolicy.evict(evictionConfig, underTest,
- poolMap.get(evictionKey).getIdleObjects().size());
- } catch (final Throwable t) {
- // Slightly convoluted as SwallowedExceptionListener
- // uses Exception rather than Throwable
- PoolUtils.checkRethrow(t);
- swallowException(new Exception(t));
- // Don't evict on error conditions
- evict = false;
- }
+ // User provided eviction policy could throw all sorts of
+ // crazy exceptions. Protect against such an exception
+ // killing the eviction thread.
+ boolean evict;
+ try {
+ evict = evictionPolicy.evict(evictionConfig, underTest,
+ poolMap.get(evictionKey).getIdleObjects().size());
+ } catch (final Throwable t) {
+ // Slightly convoluted as SwallowedExceptionListener
+ // uses Exception rather than Throwable
+ PoolUtils.checkRethrow(t);
+ swallowException(new Exception(t));
+ // Don't evict on error conditions
+ evict = false;
+ }
- if (evict) {
- destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
- destroyedByEvictorCount.incrementAndGet();
- } else {
- if (testWhileIdle) {
- boolean active = false;
- try {
- factory.activateObject(evictionKey, underTest);
- active = true;
- } catch (final Exception e) {
- destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
- destroyedByEvictorCount.incrementAndGet();
- }
- if (active) {
- if (!factory.validateObject(evictionKey, underTest)) {
+ if (evict) {
+ destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
+ destroyedByEvictorCount.incrementAndGet();
+ } else {
+ if (testWhileIdle) {
+ boolean active = false;
+ try {
+ factory.activateObject(evictionKey, underTest);
+ active = true;
+ } catch (final Exception e) {
destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
destroyedByEvictorCount.incrementAndGet();
- } else {
- try {
- factory.passivateObject(evictionKey, underTest);
- } catch (final Exception e) {
+ }
+ if (active) {
+ if (!factory.validateObject(evictionKey, underTest)) {
destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
destroyedByEvictorCount.incrementAndGet();
+ } else {
+ try {
+ factory.passivateObject(evictionKey, underTest);
+ } catch (final Exception e) {
+ destroy(evictionKey, underTest, true, DestroyMode.NORMAL);
+ destroyedByEvictorCount.incrementAndGet();
+ }
}
}
}
- }
- if (!underTest.endEvictionTest(idleObjects)) {
- // TODO - May need to add code here once additional
- // states are used
+ if (!underTest.endEvictionTest(idleObjects)) {
+ // TODO - May need to add code here once additional
+ // states are used
+ }
}
}
}
}
+ final AbandonedConfig ac = this.abandonedConfig;
+ if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
+ removeAbandoned(ac);
+ }
}
/**
@@ -1039,6 +1081,21 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
/**
+ * Gets whether this pool identifies and logs any abandoned objects.
+ *
+ * @return {@code true} if abandoned object removal is configured for this
+ * pool and removal events are to be logged otherwise {@code false}
+ *
+ * @see AbandonedConfig#getLogAbandoned()
+ * @since 2.10.0
+ */
+ @Override
+ public boolean getLogAbandoned() {
+ final AbandonedConfig ac = this.abandonedConfig;
+ return ac != null && ac.getLogAbandoned();
+ }
+
+ /**
* Gets the cap on the number of "idle" instances per key in the pool.
* If maxIdlePerKey is set too low on heavily loaded systems it is possible
* you will see objects being destroyed and almost immediately new objects
@@ -1218,6 +1275,72 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
return result;
}
+
+ /**
+ * Gets whether a check is made for abandoned objects when an object is borrowed
+ * from this pool.
+ *
+ * @return {@code true} if abandoned object removal is configured to be
+ * activated by borrowObject otherwise {@code false}
+ *
+ * @see AbandonedConfig#getRemoveAbandonedOnBorrow()
+ * @since 2.10.0
+ */
+ @Override
+ public boolean getRemoveAbandonedOnBorrow() {
+ final AbandonedConfig ac = this.abandonedConfig;
+ return ac != null && ac.getRemoveAbandonedOnBorrow();
+ }
+
+ /**
+ * Gets whether a check is made for abandoned objects when the evictor runs.
+ *
+ * @return {@code true} if abandoned object removal is configured to be
+ * activated when the evictor runs otherwise {@code false}
+ *
+ * @see AbandonedConfig#getRemoveAbandonedOnMaintenance()
+ * @since 2.10.0
+ */
+ @Override
+ public boolean getRemoveAbandonedOnMaintenance() {
+ final AbandonedConfig ac = this.abandonedConfig;
+ return ac != null && ac.getRemoveAbandonedOnMaintenance();
+ }
+
+ /**
+ * Gets the timeout before which an object will be considered to be
+ * abandoned by this pool.
+ *
+ * @return The abandoned object timeout in seconds if abandoned object
+ * removal is configured for this pool; Integer.MAX_VALUE otherwise.
+ *
+ * @see AbandonedConfig#getRemoveAbandonedTimeout()
+ * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration()
+ * @deprecated Use {@link #getRemoveAbandonedTimeoutDuration()}.
+ * @since 2.10.0
+ */
+ @Override
+ @Deprecated
+ public int getRemoveAbandonedTimeout() {
+ final AbandonedConfig ac = this.abandonedConfig;
+ return ac != null ? ac.getRemoveAbandonedTimeout() : Integer.MAX_VALUE;
+ }
+
+ /**
+ * Gets the timeout before which an object will be considered to be
+ * abandoned by this pool.
+ *
+ * @return The abandoned object timeout in seconds if abandoned object
+ * removal is configured for this pool; Integer.MAX_VALUE otherwise.
+ *
+ * @see AbandonedConfig#getRemoveAbandonedTimeout()
+ * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration()
+ * @since 2.10.0
+ */
+ public Duration getRemoveAbandonedTimeoutDuration() {
+ final AbandonedConfig ac = this.abandonedConfig;
+ return ac != null ? ac.getRemoveAbandonedTimeoutDuration() : DEFAULT_REMOVE_ABANDONED_TIMEOUT;
+ }
//--- inner classes ----------------------------------------------
@@ -1297,6 +1420,17 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
}
/**
+ * Gets whether or not abandoned object removal is configured for this pool.
+ *
+ * @return true if this pool is configured to detect and remove
+ * abandoned objects
+ * @since 2.10.0
+ */
+ @Override
+ public boolean isAbandonedConfig() {
+ return abandonedConfig != null;
+ }
+ /**
* Provides information on all the objects in the pool, both idle (waiting
* to be borrowed) and active (currently borrowed).
* <p>
@@ -1388,6 +1522,50 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
//--- internal attributes --------------------------------------------------
/**
+ * Recovers abandoned objects which have been checked out but
+ * not used since longer than the removeAbandonedTimeout.
+ *
+ * @param abandonedConfig The configuration to use to identify abandoned objects
+ */
+ @SuppressWarnings("resource") // PrintWriter is managed elsewhere
+ private void removeAbandoned(final AbandonedConfig abandonedConfig) {
+ for (final Entry<K, GenericKeyedObjectPool<K, T>.ObjectDeque<T>> pool : poolMap.entrySet()) {
+ Map<IdentityWrapper<T>, PooledObject<T>> allObjects = pool.getValue().getAllObjects();
+
+ // Generate a list of abandoned objects to remove
+ final long nowMillis = System.currentTimeMillis();
+ final long timeoutMillis =
+ nowMillis - abandonedConfig.getRemoveAbandonedTimeoutDuration().toMillis();
+ final ArrayList<PooledObject<T>> remove = new ArrayList<>();
+ final Iterator<PooledObject<T>> it = allObjects.values().iterator();
+ while (it.hasNext()) {
+ final PooledObject<T> pooledObject = it.next();
+ synchronized (pooledObject) {
+ if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
+ pooledObject.getLastUsedTime() <= timeoutMillis) {
+ pooledObject.markAbandoned();
+ remove.add(pooledObject);
+ }
+ }
+ }
+
+ // Now remove the abandoned objects
+ final Iterator<PooledObject<T>> itr = remove.iterator();
+ while (itr.hasNext()) {
+ final PooledObject<T> pooledObject = itr.next();
+ if (abandonedConfig.getLogAbandoned()) {
+ pooledObject.printStackTrace(abandonedConfig.getLogWriter());
+ }
+ try {
+ invalidateObject(pool.getKey(), pooledObject.getObject(), DestroyMode.ABANDONED);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
* Returns an object to a keyed sub-pool.
* <p>
* If {@link #getMaxIdlePerKey() maxIdle} is set to a positive value and the
@@ -1539,6 +1717,29 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
}
/**
+ * Sets the abandoned object removal configuration.
+ *
+ * @param abandonedConfig the new configuration to use. This is used by value.
+ *
+ * @see AbandonedConfig
+ * @since 2.10.0
+ */
+ @SuppressWarnings("resource") // PrintWriter is managed elsewhere
+ public void setAbandonedConfig(final AbandonedConfig abandonedConfig) {
+ if (abandonedConfig == null) {
+ this.abandonedConfig = null;
+ } else {
+ this.abandonedConfig = new AbandonedConfig();
+ this.abandonedConfig.setLogAbandoned(abandonedConfig.getLogAbandoned());
+ this.abandonedConfig.setLogWriter(abandonedConfig.getLogWriter());
+ this.abandonedConfig.setRemoveAbandonedOnBorrow(abandonedConfig.getRemoveAbandonedOnBorrow());
+ this.abandonedConfig.setRemoveAbandonedOnMaintenance(abandonedConfig.getRemoveAbandonedOnMaintenance());
+ this.abandonedConfig.setRemoveAbandonedTimeout(abandonedConfig.getRemoveAbandonedTimeoutDuration());
+ this.abandonedConfig.setUseUsageTracking(abandonedConfig.getUseUsageTracking());
+ this.abandonedConfig.setRequireFullStackTrace(abandonedConfig.getRequireFullStackTrace());
+ }
+ }
+ /**
* Sets the configuration.
*
* @param conf the new configuration to use. This is used by value.
@@ -1630,6 +1831,8 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
builder.append(evictionKeyIterator);
builder.append(", evictionKey=");
builder.append(evictionKey);
+ builder.append(", abandonedConfig=");
+ builder.append(abandonedConfig);
}
/**
@@ -1646,4 +1849,20 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
}
}
}
+
+ /**
+ * @since 2.10.0
+ */
+ @Override
+ public void use(final T pooledObject) {
+ final AbandonedConfig abandonedCfg = this.abandonedConfig;
+ if (abandonedCfg != null && abandonedCfg.getUseUsageTracking()) {
+ poolMap.values().stream()
+ .map(pool -> pool.getAllObjects().get(new IdentityWrapper<>(pooledObject)))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .ifPresent(PooledObject::use);
+ }
+ }
+
}
diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPoolMXBean.java b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPoolMXBean.java
index 1f48dfc..40c83bd 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPoolMXBean.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPoolMXBean.java
@@ -92,6 +92,15 @@ public interface GenericKeyedObjectPoolMXBean<K> {
boolean getLifo();
/**
+ * See {@link GenericKeyedObjectPool#getLogAbandoned()}
+ * @return See {@link GenericKeyedObjectPool#getLogAbandoned()}
+ * @since 2.10.0
+ */
+ default boolean getLogAbandoned() {
+ return false;
+ }
+
+ /**
* See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}
* @return See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}
*/
@@ -190,6 +199,33 @@ public interface GenericKeyedObjectPoolMXBean<K> {
Map<String,Integer> getNumWaitersByKey();
/**
+ * See {@link GenericKeyedObjectPool#getRemoveAbandonedOnBorrow()}
+ * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedOnBorrow()}
+ * @since 2.10.0
+ */
+ default boolean getRemoveAbandonedOnBorrow() {
+ return false;
+ }
+
+ /**
+ * See {@link GenericKeyedObjectPool#getRemoveAbandonedOnMaintenance()}
+ * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedOnMaintenance()}
+ * @since 2.10.0
+ */
+ default boolean getRemoveAbandonedOnMaintenance() {
+ return false;
+ }
+
+ /**
+ * See {@link GenericKeyedObjectPool#getRemoveAbandonedTimeout()}
+ * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedTimeout()}
+ * @since 2.10.0
+ */
+ default int getRemoveAbandonedTimeout() {
+ return 0;
+ }
+
+ /**
* See {@link GenericKeyedObjectPool#getReturnedCount()}
* @return See {@link GenericKeyedObjectPool#getReturnedCount()}
*/
@@ -227,6 +263,15 @@ public interface GenericKeyedObjectPoolMXBean<K> {
long getTimeBetweenEvictionRunsMillis();
/**
+ * See {@link GenericKeyedObjectPool#isAbandonedConfig()}
+ * @return See {@link GenericKeyedObjectPool#isAbandonedConfig()}
+ * @since 2.10.0
+ */
+ default boolean isAbandonedConfig() {
+ return false;
+ }
+
+ /**
* See {@link GenericKeyedObjectPool#isClosed()}
* @return See {@link GenericKeyedObjectPool#isClosed()}
*/
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java
new file mode 100644
index 0000000..b609f33
--- /dev/null
+++ b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java
@@ -0,0 +1,413 @@
+/*
+ * 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
+ *
+ * http://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.pool2.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.commons.pool2.DestroyMode;
+import org.apache.commons.pool2.KeyedPooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * TestCase for AbandonedObjectPool
+ */
+public class TestAbandonedKeyedObjectPool {
+
+ class ConcurrentBorrower extends Thread {
+ private final ArrayList<PooledTestObject> _borrowed;
+
+ public ConcurrentBorrower(final ArrayList<PooledTestObject> borrowed) {
+ _borrowed = borrowed;
+ }
+
+ @Override
+ public void run() {
+ try {
+ _borrowed.add(pool.borrowObject(0));
+ } catch (final Exception e) {
+ // expected in most cases
+ }
+ }
+ }
+ class ConcurrentReturner extends Thread {
+ private final PooledTestObject returned;
+ public ConcurrentReturner(final PooledTestObject obj) {
+ returned = obj;
+ }
+ @Override
+ public void run() {
+ try {
+ sleep(20);
+ pool.returnObject(0,returned);
+ } catch (final Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ private static class SimpleFactory implements KeyedPooledObjectFactory<Integer,PooledTestObject> {
+
+ private final long destroyLatency;
+ private final long validateLatency;
+
+ public SimpleFactory() {
+ destroyLatency = 0;
+ validateLatency = 0;
+ }
+
+ public SimpleFactory(final long destroyLatency, final long validateLatency) {
+ this.destroyLatency = destroyLatency;
+ this.validateLatency = validateLatency;
+ }
+
+ @Override
+ public void activateObject(final Integer key, final PooledObject<PooledTestObject> obj) {
+ obj.getObject().setActive(true);
+ }
+
+ @Override
+ public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj) throws Exception {
+ destroyObject(key, obj, DestroyMode.NORMAL);
+ }
+
+ @Override
+ public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj, final DestroyMode mode) throws Exception {
+ obj.getObject().setActive(false);
+ // while destroying instances, yield control to other threads
+ // helps simulate threading errors
+ Thread.yield();
+ if (destroyLatency != 0) {
+ Thread.sleep(destroyLatency);
+ }
+ obj.getObject().destroy(mode);
+ }
+
+ @Override
+ public PooledObject<PooledTestObject> makeObject(final Integer key) {
+ return new DefaultPooledObject<>(new PooledTestObject());
+ }
+
+ @Override
+ public void passivateObject(final Integer key, final PooledObject<PooledTestObject> obj) {
+ obj.getObject().setActive(false);
+ }
+
+ @Override
+ public boolean validateObject(final Integer key, final PooledObject<PooledTestObject> obj) {
+ try {
+ Thread.sleep(validateLatency);
+ } catch (final Exception ex) {
+ // ignore
+ }
+ return true;
+ }
+ }
+
+ private GenericKeyedObjectPool<Integer,PooledTestObject> pool = null;
+
+ private AbandonedConfig abandonedConfig = null;
+
+ @SuppressWarnings("deprecation")
+ @BeforeEach
+ public void setUp() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+
+ // -- Uncomment the following line to enable logging --
+ // abandonedConfig.setLogAbandoned(true);
+
+ abandonedConfig.setRemoveAbandonedOnBorrow(true);
+ abandonedConfig.setRemoveAbandonedTimeout(1);
+ assertEquals(TestConstants.ONE_SECOND, abandonedConfig.getRemoveAbandonedTimeoutDuration());
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ assertEquals(1, abandonedConfig.getRemoveAbandonedTimeout());
+
+ pool = new GenericKeyedObjectPool<>(
+ new SimpleFactory(),
+ new GenericKeyedObjectPoolConfig<PooledTestObject>(),
+ abandonedConfig);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ final ObjectName jmxName = pool.getJmxName();
+ final String poolName = Objects.toString(jmxName, null);
+ pool.clear();
+ pool.close();
+ pool = null;
+
+ final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+ final Set<ObjectName> result = mbs.queryNames(new ObjectName(
+ "org.apache.commoms.pool2:type=GenericKeyedObjectPool,*"), null);
+ // There should be no registered pools at this point
+ final int registeredPoolCount = result.size();
+ final StringBuilder msg = new StringBuilder("Current pool is: ");
+ msg.append(poolName);
+ msg.append(" Still open pools are: ");
+ for (final ObjectName name : result) {
+ // Clean these up ready for the next test
+ msg.append(name.toString());
+ msg.append(" created via\n");
+ msg.append(mbs.getAttribute(name, "CreationStackTrace"));
+ msg.append('\n');
+ mbs.unregisterMBean(name);
+ }
+ assertEquals( 0, registeredPoolCount,msg.toString());
+ }
+
+ /**
+ * Verify that an object that gets flagged as abandoned and is subsequently
+ * invalidated is only destroyed (and pool counter decremented) once.
+ *
+ * @throws Exception May occur in some failure modes
+ */
+ @Test
+ public void testAbandonedInvalidate() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+ abandonedConfig.setRemoveAbandonedOnMaintenance(true);
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ pool.close(); // Unregister pool created by setup
+ pool = new GenericKeyedObjectPool<>(
+ // destroys take 200 ms
+ new SimpleFactory(200, 0),
+ new GenericKeyedObjectPoolConfig<PooledTestObject>(), abandonedConfig);
+ final int n = 10;
+ pool.setMaxTotal(n);
+ pool.setBlockWhenExhausted(false);
+ pool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
+ PooledTestObject obj = null;
+ for (int i = 0; i < 5; i++) {
+ obj = pool.borrowObject(0);
+ }
+ Thread.sleep(1000); // abandon checked out instances and let evictor start
+ pool.invalidateObject(0, obj); // Should not trigger another destroy / decrement
+ Thread.sleep(2000); // give evictor time to finish destroys
+ assertEquals(0, pool.getNumActive());
+ assertEquals(5, pool.getDestroyedCount());
+ }
+
+ /**
+ * Verify that an object that gets flagged as abandoned and is subsequently returned
+ * is destroyed instead of being returned to the pool (and possibly later destroyed
+ * inappropriately).
+ *
+ * @throws Exception May occur in some failure modes
+ */
+ @Test
+ public void testAbandonedReturn() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+ abandonedConfig.setRemoveAbandonedOnBorrow(true);
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ pool.close(); // Unregister pool created by setup
+ pool = new GenericKeyedObjectPool<>(
+ new SimpleFactory(200, 0),
+ new GenericKeyedObjectPoolConfig<PooledTestObject>(), abandonedConfig);
+ final int n = 10;
+ pool.setMaxTotal(n);
+ pool.setBlockWhenExhausted(false);
+ PooledTestObject obj = null;
+ for (int i = 0; i < n - 2; i++) {
+ obj = pool.borrowObject(0);
+ }
+ Objects.requireNonNull(obj, "Unable to borrow object from pool");
+ final int deadMansHash = obj.hashCode();
+ final ConcurrentReturner returner = new ConcurrentReturner(obj);
+ Thread.sleep(2000); // abandon checked out instances
+ // Now start a race - returner waits until borrowObject has kicked
+ // off removeAbandoned and then returns an instance that borrowObject
+ // will deem abandoned. Make sure it is not returned to the borrower.
+ returner.start(); // short delay, then return instance
+ assertTrue(pool.borrowObject(0).hashCode() != deadMansHash);
+ assertEquals(0, pool.getNumIdle());
+ assertEquals(1, pool.getNumActive());
+ }
+
+ /**
+ * Tests fix for Bug 28579, a bug in AbandonedObjectPool that causes numActive to go negative
+ * in GenericKeyedObjectPool
+ *
+ * @throws Exception May occur in some failure modes
+ */
+ @Test
+ public void testConcurrentInvalidation() throws Exception {
+ final int POOL_SIZE = 30;
+ pool.setMaxTotalPerKey(POOL_SIZE);
+ pool.setMaxIdlePerKey(POOL_SIZE);
+ pool.setBlockWhenExhausted(false);
+
+ // Exhaust the connection pool
+ final ArrayList<PooledTestObject> vec = new ArrayList<>();
+ for (int i = 0; i < POOL_SIZE; i++) {
+ vec.add(pool.borrowObject(0));
+ }
+
+ // Abandon all borrowed objects
+ for (final PooledTestObject element : vec) {
+ element.setAbandoned(true);
+ }
+
+ // Try launching a bunch of borrows concurrently. Abandoned sweep will be triggered for each.
+ final int CONCURRENT_BORROWS = 5;
+ final Thread[] threads = new Thread[CONCURRENT_BORROWS];
+ for (int i = 0; i < CONCURRENT_BORROWS; i++) {
+ threads[i] = new ConcurrentBorrower(vec);
+ threads[i].start();
+ }
+
+ // Wait for all the threads to finish
+ for (int i = 0; i < CONCURRENT_BORROWS; i++) {
+ threads[i].join();
+ }
+
+ // Return all objects that have not been destroyed
+ for (final PooledTestObject pto : vec) {
+ if (pto.isActive()) {
+ pool.returnObject(0, pto);
+ }
+ }
+
+ // Now, the number of active instances should be 0
+ assertTrue( pool.getNumActive() == 0,"numActive should have been 0, was " + pool.getNumActive());
+ }
+
+ public void testDestroyModeAbandoned() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+ abandonedConfig.setRemoveAbandonedOnMaintenance(true);
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ pool.close(); // Unregister pool created by setup
+ pool = new GenericKeyedObjectPool<>(
+ // validate takes 1 second
+ new SimpleFactory(0, 0),
+ new GenericKeyedObjectPoolConfig<PooledTestObject>(), abandonedConfig);
+ pool.setTimeBetweenEvictionRuns(Duration.ofMillis(50));
+ // Borrow an object, wait long enough for it to be abandoned
+ final PooledTestObject obj = pool.borrowObject(0);
+ Thread.sleep(100);
+ assertTrue(obj.isDetached());
+ }
+
+ public void testDestroyModeNormal() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+ pool.close(); // Unregister pool created by setup
+ pool = new GenericKeyedObjectPool<>(new SimpleFactory(0, 0));
+ pool.setMaxIdlePerKey(0);
+ final PooledTestObject obj = pool.borrowObject(0);
+ pool.returnObject(0, obj);
+ assertTrue(obj.isDestroyed());
+ assertFalse(obj.isDetached());
+ }
+
+ /**
+ * Verify that an object that the evictor identifies as abandoned while it
+ * is in process of being returned to the pool is not destroyed.
+ *
+ * @throws Exception May occur in some failure modes
+ */
+ @Test
+ public void testRemoveAbandonedWhileReturning() throws Exception {
+ abandonedConfig = new AbandonedConfig();
+ abandonedConfig.setRemoveAbandonedOnMaintenance(true);
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ pool.close(); // Unregister pool created by setup
+ pool = new GenericKeyedObjectPool<>(
+ // validate takes 1 second
+ new SimpleFactory(0, 1000),
+ new GenericKeyedObjectPoolConfig<PooledTestObject>(), abandonedConfig);
+ final int n = 10;
+ pool.setMaxTotal(n);
+ pool.setBlockWhenExhausted(false);
+ pool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
+ pool.setTestOnReturn(true);
+ // Borrow an object, wait long enough for it to be abandoned
+ // then arrange for evictor to run while it is being returned
+ // validation takes a second, evictor runs every 500 ms
+ final PooledTestObject obj = pool.borrowObject(0);
+ Thread.sleep(50); // abandon obj
+ pool.returnObject(0,obj); // evictor will run during validation
+ final PooledTestObject obj2 = pool.borrowObject(0);
+ assertEquals(obj, obj2); // should get original back
+ assertTrue(!obj2.isDestroyed()); // and not destroyed
+ }
+
+ /**
+ * JIRA: POOL-300
+ */
+ @Test
+ public void testStackTrace() throws Exception {
+ abandonedConfig.setRemoveAbandonedOnMaintenance(true);
+ abandonedConfig.setLogAbandoned(true);
+ abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND);
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final BufferedOutputStream bos = new BufferedOutputStream(baos);
+ final PrintWriter pw = new PrintWriter(bos);
+ abandonedConfig.setLogWriter(pw);
+ pool.setAbandonedConfig(abandonedConfig);
+ pool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
+ final PooledTestObject o1 = pool.borrowObject(0);
+ Thread.sleep(2000);
+ assertTrue(o1.isDestroyed());
+ bos.flush();
+ assertTrue(baos.toString().indexOf("Pooled object") >= 0);
+ }
+
+ /**
+ * Test case for https://issues.apache.org/jira/browse/DBCP-260.
+ * Borrow and abandon all the available objects then attempt to borrow one
+ * further object which should block until the abandoned objects are
+ * removed. We don't want the test to block indefinitely when it fails so
+ * use maxWait be check we don't actually have to wait that long.
+ *
+ * @throws Exception May occur in some failure modes
+ */
+ @Test
+ public void testWhenExhaustedBlock() throws Exception {
+ abandonedConfig.setRemoveAbandonedOnMaintenance(true);
+ pool.setAbandonedConfig(abandonedConfig);
+ pool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
+
+ pool.setMaxTotal(1);
+
+ @SuppressWarnings("unused") // This is going to be abandoned
+ final PooledTestObject o1 = pool.borrowObject(0);
+
+ final long startMillis = System.currentTimeMillis();
+ final PooledTestObject o2 = pool.borrowObject(0, 5000);
+ final long endMillis = System.currentTimeMillis();
+
+ pool.returnObject(0, o2);
+
+ assertTrue(endMillis - startMillis < 5000);
+ }
+}
+
diff --git a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
index 486bfa1..8dd00ec 100644
--- a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
@@ -19,14 +19,21 @@ package org.apache.commons.pool2.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.Duration;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.KeyedObjectPool;
import org.apache.commons.pool2.KeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.AbandonedConfig;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
+import org.apache.commons.pool2.proxy.BaseTestProxiedObjectPool.TestObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -71,14 +78,28 @@ public abstract class BaseTestProxiedKeyedObjectPool {
private static final String DATA1 = "data1";
+ private static final Duration ABANDONED_TIMEOUT_SECS = Duration.ofSeconds(3);
+
private KeyedObjectPool<String,TestObject> pool;
+ private StringWriter log = null;
+
protected abstract ProxySource<TestObject> getproxySource();
@BeforeEach
public void setUp() {
+ log = new StringWriter();
+
+ final PrintWriter pw = new PrintWriter(log);
+ final AbandonedConfig abandonedConfig = new AbandonedConfig();
+ abandonedConfig.setLogAbandoned(true);
+ abandonedConfig.setRemoveAbandonedOnBorrow(true);
+ abandonedConfig.setUseUsageTracking(true);
+ abandonedConfig.setRemoveAbandonedTimeout(ABANDONED_TIMEOUT_SECS);
+ abandonedConfig.setLogWriter(pw);
+
final GenericKeyedObjectPoolConfig<TestObject> config = new GenericKeyedObjectPoolConfig<>();
config.setMaxTotal(3);
@@ -88,7 +109,7 @@ public abstract class BaseTestProxiedKeyedObjectPool {
@SuppressWarnings("resource")
final KeyedObjectPool<String, TestObject> innerPool =
new GenericKeyedObjectPool<>(
- factory, config);
+ factory, config, abandonedConfig);
pool = new ProxiedKeyedObjectPool<>(innerPool, getproxySource());
}
@@ -165,5 +186,25 @@ public abstract class BaseTestProxiedKeyedObjectPool {
assertThrows(IllegalStateException.class,
() -> pool.addObject(KEY1));
}
+
+ @Test
+ public void testUsageTracking() throws Exception {
+ final TestObject obj = pool.borrowObject(KEY1);
+ assertNotNull(obj);
+
+ // Use the object to trigger collection of last used stack trace
+ obj.setData(DATA1);
+
+ // Sleep long enough for the object to be considered abandoned
+ Thread.sleep(ABANDONED_TIMEOUT_SECS.plusSeconds(2).toMillis());
+
+ // Borrow another object to trigger the abandoned object processing
+ pool.borrowObject(KEY1);
+
+ final String logOutput = log.getBuffer().toString();
+
+ assertTrue(logOutput.contains("Pooled object created"));
+ assertTrue(logOutput.contains("The last code to use this object was"));
+ }
}