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     VarHandle NEXT, EX;
  66     Object[] exceptionTable;
  67     ReentrantLock exceptionTableLock;
  68 
  69     FJExceptionTableLeak() {
  70         // initialize separately to allow to pass with FJ versions without table
  71     }
  72 
  73     void init() throws ReflectiveOperationException {
  74         MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
  75             ForkJoinTask.class, MethodHandles.lookup());
  76         Class<?> nodeClass = Class.forName(
  77             ForkJoinTask.class.getName() + "$ExceptionNode");
  78         VarHandle exceptionTableHandle = lookup.findStaticVarHandle(
  79             ForkJoinTask.class, "exceptionTable", arrayClass(nodeClass));
  80         VarHandle exceptionTableLockHandle = lookup.findStaticVarHandle(
  81             ForkJoinTask.class, "exceptionTableLock", ReentrantLock.class);
  82         exceptionTable = (Object[]) exceptionTableHandle.get();
  83         exceptionTableLock = (ReentrantLock) exceptionTableLockHandle.get();
  84 
  85         NEXT = lookup.findVarHandle(nodeClass, "next", nodeClass);
  86         EX = lookup.findVarHandle(nodeClass, "ex", Throwable.class);
  87     }
  88 
  89     static <T> Class<T[]> arrayClass(Class<T> klazz) {
  90         try {
  91             return (Class<T[]>) Class.forName("[L" + klazz.getName() + ";");
  92         } catch (ReflectiveOperationException ex) {
  93             throw new Error(ex);
  94         }
  95     }
  96 
  97     Object next(Object node) { return NEXT.get(node); }
  98     Throwable ex(Object node) { return (Throwable) EX.get(node); }
  99 
 100     static class FailingTaskException extends RuntimeException {}
 101     static class FailingTask extends RecursiveAction {
 102         public void compute() { throw new FailingTaskException(); }
 103     }
 104 
 105     /** Counts all FailingTaskExceptions still recorded in exceptionTable. */
 106     int retainedExceptions() {
 107         exceptionTableLock.lock();
 108         try {
 109             int count = 0;
 110             for (Object node : exceptionTable)
 111                 for (; node != null; node = next(node))
 112                     if (ex(node) instanceof FailingTaskException)
 113                         count++;
 114             return count;
 115         } finally {
 116             exceptionTableLock.unlock();
 117         }
 118     }
 119 
 120     @Test
 121     public void exceptionTableCleanup() throws Exception {
 122         try {
 123             init();
 124         } catch (ReflectiveOperationException ex) {
 125             return; // using FJ Version without Exception table
 126         }
 127         ArrayList<FailingTask> failedTasks = failedTasks();
 128 
 129         // Retain a strong ref to one last failing task
 130         FailingTask lastTask = failedTasks.get(rnd.nextInt(failedTasks.size()));
 131 
 132         // Clear all other strong refs, making exception table cleanable
 133         failedTasks.clear();
 134 
 135         BooleanSupplier exceptionTableIsClean = () -> {
 136             try {
 137                 // Trigger exception table expunging as side effect
 138                 lastTask.join();
 139                 throw new AssertionError("should throw");
 140             } catch (FailingTaskException expected) {}
 141             int count = retainedExceptions();
 142             if (count == 0)
 143                 throw new AssertionError("expected to find last task");
 144             return count == 1;
 145         };
 146         gcAwait(exceptionTableIsClean);
 147     }
 148 
 149     /** Sequestered into a separate method to inhibit GC retention. */
 150     ArrayList<FailingTask> failedTasks()
 151         throws Exception {
 152         final ForkJoinPool pool = new ForkJoinPool(rnd.nextInt(1, 4));
 153 
 154         assertEquals(0, retainedExceptions());
 155 
 156         final ArrayList<FailingTask> tasks = new ArrayList<>();
 157 
 158         for (int i = exceptionTable.length; i--> 0; ) {
 159             FailingTask task = new FailingTask();
 160             pool.execute(task);
 161             tasks.add(task); // retain strong refs to all tasks, for now
 162             task = null;     // excessive GC retention paranoia
 163         }
 164         for (FailingTask task : tasks) {
 165             try {
 166                 task.join();
 167                 throw new AssertionError("should throw");
 168             } catch (FailingTaskException success) {}
 169             task = null;     // excessive GC retention paranoia
 170         }
 171 
 172         if (rnd.nextBoolean())
 173             gcAwait(() -> retainedExceptions() == tasks.size());
 174 
 175         return tasks;
 176     }
 177 
 178     // --------------- GC finalization infrastructure ---------------
 179 
 180     /** No guarantees, but effective in practice. */
 181     static void forceFullGc() {
 182         long timeoutMillis = 1000L;
 183         CountDownLatch finalized = new CountDownLatch(1);
 184         ReferenceQueue<Object> queue = new ReferenceQueue<>();
 185         WeakReference<Object> ref = new WeakReference<>(
 186             new Object() { protected void finalize() { finalized.countDown(); }},
 187             queue);
 188         try {
 189             for (int tries = 3; tries--> 0; ) {
 190                 System.gc();
 191                 if (finalized.await(timeoutMillis, MILLISECONDS)
 192                     && queue.remove(timeoutMillis) != null
 193                     && ref.get() == null) {
 194                     System.runFinalization(); // try to pick up stragglers
 195                     return;
 196                 }
 197                 timeoutMillis *= 4;
 198             }
 199         } catch (InterruptedException unexpected) {
 200             throw new AssertionError("unexpected InterruptedException");
 201         }
 202         throw new AssertionError("failed to do a \"full\" gc");
 203     }
 204 
 205     static void gcAwait(BooleanSupplier s) {
 206         for (int i = 0; i < 10; i++) {
 207             if (s.getAsBoolean())
 208                 return;
 209             forceFullGc();
 210         }
 211         throw new AssertionError("failed to satisfy condition");
 212     }
 213 }