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 b7b4322  [POOL-326] Threading issue, NullPointerException and 
IllegalStateException in GenericKeyedObjectPool.
b7b4322 is described below

commit b7b4322aada5d6d5521d47b5fed769b40753d68e
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Fri Feb 8 12:48:53 2019 -0500

    [POOL-326] Threading issue, NullPointerException and
    IllegalStateException in GenericKeyedObjectPool.
---
 src/changes/changes.xml                            |   3 +
 .../commons/pool2/impl/GenericKeyedObjectPool.java |  30 ++--
 .../apache/commons/pool2/ObjectPoolIssue326.java   | 183 +++++++++++++++++++++
 3 files changed, 202 insertions(+), 14 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c189121..7026378 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,9 @@ The <action> type attribute can be add,update,fix,remove.
     <action dev="ggregory" issue="POOL-360" type="update">
       Update optional library cglib from 3.2.9 to 3.2.10.
     </action>
+    <action dev="ggregory" issue="POOL-326" type="fix" due-to="Chris Allison, 
Phil Steitz">
+      Threading issue, NullPointerException and IllegalStateException in 
GenericKeyedObjectPool.
+    </action>
   </release>  
   <release version="2.6.0" date="2018-07-06" description="This is a 
maintenance release.">
     <action dev="ggregory" issue="POOL-336" type="update" due-to="Wolfgang 
Glas">
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 72aab16..ab5950e 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
@@ -1144,26 +1144,28 @@ public class GenericKeyedObjectPool<K, T> extends 
BaseGenericObjectPool<T>
      * @param k The key to de-register
      */
     private void deregister(final K k) {
+        Lock lock = keyLock.readLock();
         ObjectDeque<T> objectDeque;
-
-        objectDeque = poolMap.get(k);
-        final long numInterested = 
objectDeque.getNumInterested().decrementAndGet();
-        if (numInterested == 0 && objectDeque.getCreateCount().get() == 0) {
-            // Potential to remove key
-            final Lock writeLock = keyLock.writeLock();
-            writeLock.lock();
-            try {
-                if (objectDeque.getCreateCount().get() == 0 &&
-                        objectDeque.getNumInterested().get() == 0) {
+        try {
+            lock.lock();
+            objectDeque = poolMap.get(k);
+            final long numInterested = 
objectDeque.getNumInterested().decrementAndGet();
+            if (numInterested == 0 && objectDeque.getCreateCount().get() == 0) 
{
+                // Potential to remove key
+                // Upgrade to write lock
+                lock.unlock();
+                lock = keyLock.writeLock();
+                lock.lock();
+                if (objectDeque.getCreateCount().get() == 0 && 
objectDeque.getNumInterested().get() == 0) {
                     // NOTE: Keys must always be removed from both poolMap and
-                    //       poolKeyList at the same time while protected by
-                    //       keyLock.writeLock()
+                    // poolKeyList at the same time while protected by
+                    // keyLock.writeLock()
                     poolMap.remove(k);
                     poolKeyList.remove(k);
                 }
-            } finally {
-                writeLock.unlock();
             }
+        } finally {
+            lock.unlock();
         }
     }
 
diff --git a/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java 
b/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java
new file mode 100644
index 0000000..d1fbb49
--- /dev/null
+++ b/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.BaseObjectPoolConfig;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
+
+/**
+ * On my box with 4 cores this test fails at between 5s and 900s with an 
average
+ * of 240s (data from 10 runs of test).
+ * 
+ * It is hard to turn this in a unit test because it can affect the build
+ * negatively since you need to run it for a while.
+ */
+public final class ObjectPoolIssue326 {
+    public static void main(String[] args) {
+        try {
+            new ObjectPoolIssue326().run();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void run() throws Exception {
+        GenericKeyedObjectPoolConfig poolConfig = new 
GenericKeyedObjectPoolConfig();
+        poolConfig.setMaxTotal(10);
+        poolConfig.setMaxTotalPerKey(5);
+        poolConfig.setMinIdlePerKey(-1);
+        poolConfig.setMaxIdlePerKey(-1);
+        poolConfig.setLifo(true);
+        poolConfig.setFairness(true);
+        poolConfig.setMaxWaitMillis(30 * 1000);
+        poolConfig.setMinEvictableIdleTimeMillis(-1);
+        poolConfig.setSoftMinEvictableIdleTimeMillis(-1);
+        poolConfig.setNumTestsPerEvictionRun(1);
+        poolConfig.setTestOnCreate(false);
+        poolConfig.setTestOnBorrow(false);
+        poolConfig.setTestOnReturn(false);
+        poolConfig.setTestWhileIdle(false);
+        poolConfig.setTimeBetweenEvictionRunsMillis(5 * 1000);
+        
poolConfig.setEvictionPolicyClassName(BaseObjectPoolConfig.DEFAULT_EVICTION_POLICY_CLASS_NAME);
+        poolConfig.setBlockWhenExhausted(false);
+        poolConfig.setJmxEnabled(false);
+        poolConfig.setJmxNameBase(null);
+        poolConfig.setJmxNamePrefix(null);
+
+        GenericKeyedObjectPool<Integer, Object> pool = new 
GenericKeyedObjectPool<>(new ObjectFactory(), poolConfig);
+
+        // number of threads to reproduce is finicky. this count seems to be 
best for my
+        // 4 core box.
+        // too many doesn't reproduce it ever, too few doesn't either.
+        ExecutorService service = 
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
+        long startTime = System.currentTimeMillis();
+        long testIter = 0;
+        try {
+            while (true) {
+                testIter++;
+                if (testIter % 1000 == 0) {
+                    System.out.println(testIter);
+                }
+                List<Task> tasks = createTasks(pool);
+                List<Future<Object>> futures = service.invokeAll(tasks);
+                for (Future<Object> future : futures) {
+                    future.get();
+                }
+            }
+        } finally {
+            System.out.println("Time: " + (System.currentTimeMillis() - 
startTime) / 1000.0);
+            service.shutdown();
+        }
+    }
+
+    private List<Task> createTasks(GenericKeyedObjectPool<Integer, Object> 
pool) {
+        List<Task> tasks = new ArrayList<>();
+        for (int i = 0; i < 250; i++) {
+            tasks.add(new Task(pool, i));
+        }
+        return tasks;
+    }
+
+    private class ObjectFactory extends BaseKeyedPooledObjectFactory<Integer, 
Object> {
+        @Override
+        public Object create(Integer s) throws Exception {
+            return new TestObject();
+        }
+
+        @Override
+        public PooledObject<Object> wrap(Object o) {
+            return new DefaultPooledObject<>(o);
+        }
+    }
+
+    private class TestObject {
+    }
+
+    private class Task implements Callable<Object> {
+        private final GenericKeyedObjectPool<Integer, Object> m_pool;
+        private final int m_key;
+
+        Task(GenericKeyedObjectPool<Integer, Object> pool, int count) {
+            m_pool = pool;
+            m_key = count % 20;
+        }
+
+        @Override
+        public Object call() throws Exception {
+            try {
+                Object value;
+                value = m_pool.borrowObject(m_key);
+                // don't make this too long or it won't reproduce, and don't 
make it zero or it
+                // won't reproduce
+                // constant low value also doesn't reproduce
+                busyWait(System.currentTimeMillis() % 4);
+                m_pool.returnObject(m_key, value);
+                return "success";
+            } catch (NoSuchElementException e) {
+                // ignore, we've exhausted the pool
+                // not sure whether what we do here matters for reproducing
+                busyWait(System.currentTimeMillis() % 20);
+                return "exhausted";
+            }
+        }
+
+        private void busyWait(long timeMillis) {
+            // busy waiting intentionally as a simple thread.sleep fails to 
reproduce
+            long endTime = System.currentTimeMillis() + timeMillis;
+            while (System.currentTimeMillis() < endTime)
+                ;
+        }
+    }
+}
+
+/*
+ * 
+ * Example stack trace: java.util.concurrent.ExecutionException:
+ * java.lang.NullPointerException at
+ * java.util.concurrent.FutureTask.report(FutureTask.java:122) at
+ * java.util.concurrent.FutureTask.get(FutureTask.java:192) at
+ * threading_pool.ObjectPoolIssue.run(ObjectPoolIssue.java:63) at
+ * threading_pool.ObjectPoolIssue.main(ObjectPoolIssue.java:23) at
+ * sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at
+ * 
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+ * at
+ * 
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.
+ * java:43) at java.lang.reflect.Method.invoke(Method.java:498) at
+ * com.intellij.rt.execution.application.AppMain.main(AppMain.java:147) Caused
+ * by: java.lang.NullPointerException at
+ * org.apache.commons.pool2.impl.GenericKeyedObjectPool.returnObject(
+ * GenericKeyedObjectPool.java:474) at
+ * threading_pool.ObjectPoolIssue$Task.call(ObjectPoolIssue.java:112) at
+ * java.util.concurrent.FutureTask.run(FutureTask.java:266) at
+ * java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:
+ * 1142) at
+ * java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:
+ * 617) at java.lang.Thread.run(Thread.java:745)
+ * 
+ */

Reply via email to