1 /*
   2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   3  *
   4  * This code is free software; you can redistribute it and/or modify it
   5  * under the terms of the GNU General Public License version 2 only, as
   6  * published by the Free Software Foundation.
   7  *
   8  * This code is distributed in the hope that it will be useful, but WITHOUT
   9  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  11  * version 2 for more details (a copy is included in the LICENSE file that
  12  * accompanied this code).
  13  *
  14  * You should have received a copy of the GNU General Public License version
  15  * 2 along with this work; if not, write to the Free Software Foundation,
  16  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  17  *
  18  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  19  * or visit www.oracle.com if you need additional information or have any
  20  * questions.
  21  */
  22 
  23 /*
  24  * This file is available under and governed by the GNU General Public
  25  * License version 2 only, as published by the Free Software Foundation.
  26  * However, the following notice accompanied the original version of this
  27  * file:
  28  *
  29  * Written by Doug Lea and Martin Buchholz with assistance from
  30  * members of JCP JSR-166 Expert Group and released to the public
  31  * domain, as explained at
  32  * http://creativecommons.org/publicdomain/zero/1.0/
  33  */
  34 
  35 /*
  36  * @test
  37  * @bug 8004138 8205576
  38  * @modules java.base/java.util.concurrent:open
  39  * @run testng FJExceptionTableLeak
  40  * @summary Checks that ForkJoinTask thrown exceptions are not leaked.
  41  * This whitebox test is sensitive to forkjoin implementation details.
  42  */
  43 
  44 import static org.testng.Assert.*;
  45 import org.testng.annotations.Test;
  46 
  47 import static java.util.concurrent.TimeUnit.MILLISECONDS;
  48 
  49 import java.lang.ref.ReferenceQueue;
  50 import java.lang.ref.WeakReference;
  51 import java.lang.invoke.MethodHandles;
  52 import java.lang.invoke.VarHandle;
  53 import java.util.ArrayList;
  54 import java.util.concurrent.CountDownLatch;
  55 import java.util.concurrent.ForkJoinPool;
  56 import java.util.concurrent.ForkJoinTask;
  57 import java.util.concurrent.RecursiveAction;
  58 import java.util.concurrent.ThreadLocalRandom;
  59 import java.util.concurrent.locks.ReentrantLock;
  60 import java.util.function.BooleanSupplier;
  61 
  62 @Test
  63 public class FJExceptionTableLeak {
  64     final ThreadLocalRandom rnd = ThreadLocalRandom.current();
  65     final VarHandle NEXT, EX;
  66     final Object[] exceptionTable;
  67     final ReentrantLock exceptionTableLock;
  68 
  69     FJExceptionTableLeak() throws ReflectiveOperationException {
  70         MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
  71             ForkJoinTask.class, MethodHandles.lookup());
  72         Class<?> nodeClass = Class.forName(
  73             ForkJoinTask.class.getName() + "$ExceptionNode");
  74         VarHandle exceptionTableHandle = lookup.findStaticVarHandle(
  75             ForkJoinTask.class, "exceptionTable", arrayClass(nodeClass));
  76         VarHandle exceptionTableLockHandle = lookup.findStaticVarHandle(
  77             ForkJoinTask.class, "exceptionTableLock", ReentrantLock.class);
  78         exceptionTable = (Object[]) exceptionTableHandle.get();
  79         exceptionTableLock = (ReentrantLock) exceptionTableLockHandle.get();
  80 
  81         NEXT = lookup.findVarHandle(nodeClass, "next", nodeClass);
  82         EX = lookup.findVarHandle(nodeClass, "ex", Throwable.class);
  83     }
  84 
  85     static <T> Class<T[]> arrayClass(Class<T> klazz) {
  86         try {
  87             return (Class<T[]>) Class.forName("[L" + klazz.getName() + ";");
  88         } catch (ReflectiveOperationException ex) {
  89             throw new Error(ex);
  90         }
  91     }
  92 
  93     Object next(Object node) { return NEXT.get(node); }
  94     Throwable ex(Object node) { return (Throwable) EX.get(node); }
  95 
  96     static class FailingTaskException extends RuntimeException {}
  97     static class FailingTask extends RecursiveAction {
  98         public void compute() { throw new FailingTaskException(); }
  99     }
 100 
 101     /** Counts all FailingTaskExceptions still recorded in exceptionTable. */
 102     int retainedExceptions() {
 103         exceptionTableLock.lock();
 104         try {
 105             int count = 0;
 106             for (Object node : exceptionTable)
 107                 for (; node != null; node = next(node))
 108                     if (ex(node) instanceof FailingTaskException)
 109                         count++;
 110             return count;
 111         } finally {
 112             exceptionTableLock.unlock();
 113         }
 114     }
 115 
 116     @Test
 117     public void exceptionTableCleanup() throws Exception {
 118         ArrayList<FailingTask> failedTasks = failedTasks();
 119 
 120         // Retain a strong ref to one last failing task
 121         FailingTask lastTask = failedTasks.get(rnd.nextInt(failedTasks.size()));
 122 
 123         // Clear all other strong refs, making exception table cleanable
 124         failedTasks.clear();
 125 
 126         BooleanSupplier exceptionTableIsClean = () -> {
 127             try {
 128                 // Trigger exception table expunging as side effect
 129                 lastTask.join();
 130                 throw new AssertionError("should throw");
 131             } catch (FailingTaskException expected) {}
 132             int count = retainedExceptions();
 133             if (count == 0)
 134                 throw new AssertionError("expected to find last task");
 135             return count == 1;
 136         };
 137         gcAwait(exceptionTableIsClean);
 138     }
 139 
 140     /** Sequestered into a separate method to inhibit GC retention. */
 141     ArrayList<FailingTask> failedTasks()
 142         throws Exception {
 143         final ForkJoinPool pool = new ForkJoinPool(rnd.nextInt(1, 4));
 144 
 145         assertEquals(0, retainedExceptions());
 146 
 147         final ArrayList<FailingTask> tasks = new ArrayList<>();
 148 
 149         for (int i = exceptionTable.length; i--> 0; ) {
 150             FailingTask task = new FailingTask();
 151             pool.execute(task);
 152             tasks.add(task); // retain strong refs to all tasks, for now
 153             task = null;     // excessive GC retention paranoia
 154         }
 155         for (FailingTask task : tasks) {
 156             try {
 157                 task.join();
 158                 throw new AssertionError("should throw");
 159             } catch (FailingTaskException success) {}
 160             task = null;     // excessive GC retention paranoia
 161         }
 162 
 163         if (rnd.nextBoolean())
 164             gcAwait(() -> retainedExceptions() == tasks.size());
 165 
 166         return tasks;
 167     }
 168 
 169     // --------------- GC finalization infrastructure ---------------
 170 
 171     /** No guarantees, but effective in practice. */
 172     static void forceFullGc() {
 173         long timeoutMillis = 1000L;
 174         CountDownLatch finalized = new CountDownLatch(1);
 175         ReferenceQueue<Object> queue = new ReferenceQueue<>();
 176         WeakReference<Object> ref = new WeakReference<>(
 177             new Object() { protected void finalize() { finalized.countDown(); }},
 178             queue);
 179         try {
 180             for (int tries = 3; tries--> 0; ) {
 181                 System.gc();
 182                 if (finalized.await(timeoutMillis, MILLISECONDS)
 183                     && queue.remove(timeoutMillis) != null
 184                     && ref.get() == null) {
 185                     System.runFinalization(); // try to pick up stragglers
 186                     return;
 187                 }
 188                 timeoutMillis *= 4;
 189             }
 190         } catch (InterruptedException unexpected) {
 191             throw new AssertionError("unexpected InterruptedException");
 192         }
 193         throw new AssertionError("failed to do a \"full\" gc");
 194     }
 195 
 196     static void gcAwait(BooleanSupplier s) {
 197         for (int i = 0; i < 10; i++) {
 198             if (s.getAsBoolean())
 199                 return;
 200             forceFullGc();
 201         }
 202         throw new AssertionError("failed to satisfy condition");
 203     }
 204 }