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 
FJExceptionTableLeak()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 
arrayClass(Class<T> klazz)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 
next(Object node)93     Object next(Object node) { return NEXT.get(node); }
ex(Object node)94     Throwable ex(Object node) { return (Throwable) EX.get(node); }
95 
96     static class FailingTaskException extends RuntimeException {}
97     static class FailingTask extends RecursiveAction {
compute()98         public void compute() { throw new FailingTaskException(); }
99     }
100 
101     /** Counts all FailingTaskExceptions still recorded in exceptionTable. */
retainedExceptions()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
exceptionTableCleanup()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. */
failedTasks()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. */
forceFullGc()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 
gcAwait(BooleanSupplier s)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 }
205