8205576: forkjoin/FJExceptionTableLeak.java fails "AssertionError: failed to satisfy condition"
authordl
Tue, 10 Jul 2018 10:24:08 -0700
changeset 50960 ebfb1ae41f4b
parent 50959 f9b96afb7c5e
child 50961 0d28f82ecac6
8205576: forkjoin/FJExceptionTableLeak.java fails "AssertionError: failed to satisfy condition" Reviewed-by: martin, psandoz, dholmes, tschatzl
test/jdk/java/util/concurrent/forkjoin/FJExceptionTableLeak.java
--- a/test/jdk/java/util/concurrent/forkjoin/FJExceptionTableLeak.java	Tue Jul 10 10:24:08 2018 -0700
+++ b/test/jdk/java/util/concurrent/forkjoin/FJExceptionTableLeak.java	Tue Jul 10 10:24:08 2018 -0700
@@ -34,77 +34,102 @@
 
 /*
  * @test
- * @bug 8004138
+ * @bug 8004138 8205576
  * @modules java.base/java.util.concurrent:open
+ * @run testng FJExceptionTableLeak
  * @summary Checks that ForkJoinTask thrown exceptions are not leaked.
  * This whitebox test is sensitive to forkjoin implementation details.
  */
 
+import static org.testng.Assert.*;
+import org.testng.annotations.Test;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
-import java.lang.reflect.Field;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ForkJoinTask;
 import java.util.concurrent.RecursiveAction;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.BooleanSupplier;
 
+@Test
 public class FJExceptionTableLeak {
+    final ThreadLocalRandom rnd = ThreadLocalRandom.current();
+    final VarHandle NEXT, EX;
+    final Object[] exceptionTable;
+    final ReentrantLock exceptionTableLock;
+
+    FJExceptionTableLeak() throws ReflectiveOperationException {
+        MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
+            ForkJoinTask.class, MethodHandles.lookup());
+        Class<?> nodeClass = Class.forName(
+            ForkJoinTask.class.getName() + "$ExceptionNode");
+        VarHandle exceptionTableHandle = lookup.findStaticVarHandle(
+            ForkJoinTask.class, "exceptionTable", arrayClass(nodeClass));
+        VarHandle exceptionTableLockHandle = lookup.findStaticVarHandle(
+            ForkJoinTask.class, "exceptionTableLock", ReentrantLock.class);
+        exceptionTable = (Object[]) exceptionTableHandle.get();
+        exceptionTableLock = (ReentrantLock) exceptionTableLockHandle.get();
+
+        NEXT = lookup.findVarHandle(nodeClass, "next", nodeClass);
+        EX = lookup.findVarHandle(nodeClass, "ex", Throwable.class);
+    }
+
+    static <T> Class<T[]> arrayClass(Class<T> klazz) {
+        try {
+            return (Class<T[]>) Class.forName("[L" + klazz.getName() + ";");
+        } catch (ReflectiveOperationException ex) {
+            throw new Error(ex);
+        }
+    }
+
+    Object next(Object node) { return NEXT.get(node); }
+    Throwable ex(Object node) { return (Throwable) EX.get(node); }
+
     static class FailingTaskException extends RuntimeException {}
     static class FailingTask extends RecursiveAction {
         public void compute() { throw new FailingTaskException(); }
     }
 
-    static int bucketsInuse(Object[] exceptionTable) {
-        int count = 0;
-        for (Object x : exceptionTable)
-            if (x != null) count++;
-        return count;
+    /** Counts all FailingTaskExceptions still recorded in exceptionTable. */
+    int retainedExceptions() {
+        exceptionTableLock.lock();
+        try {
+            int count = 0;
+            for (Object node : exceptionTable)
+                for (; node != null; node = next(node))
+                    if (ex(node) instanceof FailingTaskException)
+                        count++;
+            return count;
+        } finally {
+            exceptionTableLock.unlock();
+        }
     }
 
-    public static void main(String[] args) throws Exception {
-        final ForkJoinPool pool = new ForkJoinPool(4);
-        final Field exceptionTableField =
-            ForkJoinTask.class.getDeclaredField("exceptionTable");
-        exceptionTableField.setAccessible(true);
-        final Object[] exceptionTable = (Object[]) exceptionTableField.get(null);
-
-        if (bucketsInuse(exceptionTable) != 0) throw new AssertionError();
-
-        final ArrayList<FailingTask> tasks = new ArrayList<>();
+    @Test
+    public void exceptionTableCleanup() throws Exception {
+        ArrayList<FailingTask> failedTasks = failedTasks();
 
-        // Keep submitting failing tasks until most of the exception
-        // table buckets are in use
-        do {
-            for (int i = 0; i < exceptionTable.length; i++) {
-                FailingTask task = new FailingTask();
-                pool.execute(task);
-                tasks.add(task); // retain strong refs to all tasks, for now
-            }
-            for (FailingTask task : tasks) {
-                try {
-                    task.join();
-                    throw new AssertionError("should throw");
-                } catch (FailingTaskException success) {}
-            }
-        } while (bucketsInuse(exceptionTable) < exceptionTable.length * 3 / 4);
-
-        // Retain a strong ref to one last failing task;
-        // task.join() will trigger exception table expunging.
-        FailingTask lastTask = tasks.get(0);
+        // Retain a strong ref to one last failing task
+        FailingTask lastTask = failedTasks.get(rnd.nextInt(failedTasks.size()));
 
         // Clear all other strong refs, making exception table cleanable
-        tasks.clear();
+        failedTasks.clear();
 
         BooleanSupplier exceptionTableIsClean = () -> {
             try {
+                // Trigger exception table expunging as side effect
                 lastTask.join();
                 throw new AssertionError("should throw");
             } catch (FailingTaskException expected) {}
-            int count = bucketsInuse(exceptionTable);
+            int count = retainedExceptions();
             if (count == 0)
                 throw new AssertionError("expected to find last task");
             return count == 1;
@@ -112,6 +137,35 @@
         gcAwait(exceptionTableIsClean);
     }
 
+    /** Sequestered into a separate method to inhibit GC retention. */
+    ArrayList<FailingTask> failedTasks()
+        throws Exception {
+        final ForkJoinPool pool = new ForkJoinPool(rnd.nextInt(1, 4));
+
+        assertEquals(0, retainedExceptions());
+
+        final ArrayList<FailingTask> tasks = new ArrayList<>();
+
+        for (int i = exceptionTable.length; i--> 0; ) {
+            FailingTask task = new FailingTask();
+            pool.execute(task);
+            tasks.add(task); // retain strong refs to all tasks, for now
+            task = null;     // excessive GC retention paranoia
+        }
+        for (FailingTask task : tasks) {
+            try {
+                task.join();
+                throw new AssertionError("should throw");
+            } catch (FailingTaskException success) {}
+            task = null;     // excessive GC retention paranoia
+        }
+
+        if (rnd.nextBoolean())
+            gcAwait(() -> retainedExceptions() == tasks.size());
+
+        return tasks;
+    }
+
     // --------------- GC finalization infrastructure ---------------
 
     /** No guarantees, but effective in practice. */