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"));
+    }
 
 }