1 /*
  2  * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 import jdk.test.lib.Asserts;
 24 import jdk.test.lib.Utils;
 25 import jdk.test.lib.process.ProcessTools;
 26 import jdk.test.lib.process.OutputAnalyzer;
 27 
 28 import java.lang.reflect.Constructor;
 29 import java.lang.reflect.InvocationTargetException;
 30 import java.util.List;
 31 import java.util.concurrent.atomic.AtomicReference;
 32 import java.util.concurrent.ExecutorService;
 33 import java.util.concurrent.Executor;
 34 import java.util.concurrent.Executors;
 35 import java.util.concurrent.ThreadFactory;
 36 import java.util.regex.Matcher;
 37 import java.util.regex.Pattern;
 38 
 39 /*
 40  * Tests that JNI monitors work correctly with virtual threads,
 41  * There are multiple test scenarios that we check using unified logging output
 42  * (both positive and negative tests). Each test case is handled by its own @-test
 43  * definition so that we can run each sub-test independently.
 44  *
 45  * The original bug was only discovered because the ForkJoinPool worker thread terminated
 46  * and trigerred an assertion failure. So we use a custom scheduler to give us control.
 47  */
 48 
 49 /**
 50  * @test id=normal
 51  * @bug 8327743
 52  * @summary Normal lock then unlock
 53  * @library /test/lib
 54  * @modules java.base/java.lang:+open
 55  * @requires vm.continuations
 56  * @run driver JNIMonitor Normal
 57  */
 58 
 59 /**
 60  * @test id=multiNormal
 61  * @bug 8327743
 62  * @summary Normal lock then unlock by multiple threads
 63  * @library /test/lib
 64  * @modules java.base/java.lang:+open
 65  * @requires vm.continuations
 66  * @run driver JNIMonitor MultiNormal
 67  */
 68 
 69 /**
 70  * @test id=missingUnlock
 71  * @bug 8327743
 72  * @summary Don't do the unlock and exit normally
 73  * @library /test/lib
 74  * @modules java.base/java.lang:+open
 75  * @requires vm.continuations
 76  * @run driver JNIMonitor MissingUnlock
 77  */
 78 
 79 /**
 80  * @test id=multiMissingUnlock
 81  * @bug 8327743
 82  * @summary Don't do the unlock and exit normally, by multiple threads
 83  * @library /test/lib
 84  * @modules java.base/java.lang:+open
 85  * @requires vm.continuations
 86  * @run driver JNIMonitor MultiMissingUnlock
 87  */
 88 
 89 /**
 90  * @test id=missingUnlockWithThrow
 91  * @bug 8327743
 92  * @summary Don't do the unlock and exit by throwing
 93  * @library /test/lib
 94  * @modules java.base/java.lang:+open
 95  * @requires vm.continuations
 96  * @run driver JNIMonitor MissingUnlockWithThrow
 97  */
 98 
 99 /**
100  * @test id=multiMissingUnlockWithThrow
101  * @bug 8327743
102  * @summary Don't do the unlock and exit by throwing, by multiple threads
103  * @library /test/lib
104  * @modules java.base/java.lang:+open
105  * @requires vm.continuations
106  * @run driver JNIMonitor MultiMissingUnlockWithThrow
107  */
108 
109 public class JNIMonitor {
110 
111     public static void main(String[] args) throws Exception {
112         String test = args[0];
113         String[] cmdArgs = new String[] {
114             "-Djava.library.path=" + Utils.TEST_NATIVE_PATH,
115             // Grant access to ThreadBuilders$VirtualThreadBuilder
116             "--add-opens=java.base/java.lang=ALL-UNNAMED",
117             // Enable the JNI warning
118             "-Xcheck:jni",
119             "-Xlog:jni=debug",
120             // Enable thread termination logging as a visual cross-check
121             "-Xlog:thread+os=info",
122             "JNIMonitor$" + test,
123         };
124         OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs);
125         oa.shouldHaveExitValue(0);
126         oa.stdoutShouldMatch(terminated);
127 
128         switch(test) {
129             case "Normal":
130             case "MultiNormal":
131                 oa.stdoutShouldNotMatch(stillLocked);
132                 break;
133             case "MissingUnlock":
134                 oa.stdoutShouldMatch(stillLocked);
135                 break;
136             case "MultiMissingUnlock":
137                 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT);
138                 break;
139             case "MissingUnlockWithThrow":
140                 oa.stdoutShouldMatch(stillLocked);
141                 oa.stderrShouldContain(throwMsg);
142                 break;
143             case "MultiMissingUnlockWithThrow":
144                 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT);
145                 parseOutputForPattern(oa.stderrAsLines(), throwMsg, MULTI_THREAD_COUNT);
146                 break;
147 
148             default: throw new Error("Unknown arg: " + args[0]);
149         }
150         oa.reportDiagnosticSummary();
151     }
152 
153     // The number of threads for a multi tests. Arbitrarily chosen to be > 1 but small
154     // enough to not waste too much time.
155     static final int MULTI_THREAD_COUNT = 5;
156 
157     // The logging message for leaving a monitor JNI locked has the form
158     //   [0.187s][debug][jni] VirtualThread (tid: 28, carrier id: 29) exiting with Objects still locked by JNI MonitorEnter.
159     // but if the test is run with other logging options then whitespace may get introduced in the
160     // log decorator sections, so ignore those.
161     static final String stillLocked = "VirtualThread \\(tid:.*exiting with Objects still locked by JNI MonitorEnter";
162     // The carrier thread termination logging has the form:
163     // [1.394s][info][os,thread] JavaThread exiting (name: "pool-1-thread-1", tid: 3090592).
164     static final String terminated = "JavaThread exiting \\(name: \"pool-1-thread-1\"";
165 
166     static final String throwMsg = "Terminating via exception as requested";
167 
168     // Check the process logging output for the given pattern to see if the expected number of
169     // lines are found.
170     private static void parseOutputForPattern(List<String> lines, String pattern, int expected) {
171         Pattern p = Pattern.compile(pattern);
172         int found = 0;
173         for (String line : lines) {
174             Matcher m = p.matcher(line);
175             if (m.find()) {
176                 found++;
177             }
178         }
179         if (found != expected) {
180             throw new RuntimeException("Checking for pattern \"" + pattern + "\": expected "
181                                        + expected + " but found " + found);
182         }
183     }
184 
185 
186     // straight-forward interface to JNI monitor functions
187     static native int monitorEnter(Object o);
188     static native int monitorExit(Object o);
189 
190     // Isolate the native library loading to the actual test cases, not the class that
191     // jtreg Driver will load and execute.
192     static class TestBase {
193 
194         static {
195             System.loadLibrary("JNIMonitor");
196         }
197 
198         // This gives us a way to control the scheduler used for our virtual threads. The test
199         // only works as intended when the virtual threads run on the same carrier thread (as
200         // that carrier maintains ownership of the monitor if the virtual thread fails to unlock it).
201         // The original issue was also only discovered due to the carrier thread terminating
202         // unexpectedly, so we can force that condition too by shutting down our custom scheduler.
203         private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) {
204             Thread.Builder.OfVirtual builder = Thread.ofVirtual();
205             try {
206                 Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder");
207                 Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class);
208                 ctor.setAccessible(true);
209                 return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler);
210             } catch (InvocationTargetException e) {
211                 Throwable cause = e.getCause();
212                 if (cause instanceof RuntimeException re) {
213                     throw re;
214                 }
215                 throw new RuntimeException(e);
216             } catch (Exception e) {
217                 throw new RuntimeException(e);
218             }
219         }
220 
221         static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable {
222             final Object monitor = new Object();
223             final AtomicReference<Throwable> exception = new AtomicReference();
224             // Ensure all our VT's operate of the same carrier, sequentially.
225             ExecutorService scheduler = Executors.newSingleThreadExecutor();
226             ThreadFactory factory = virtualThreadBuilder(scheduler).factory();
227             for (int i = 0 ; i < nThreads; i++) {
228                 Thread th = factory.newThread(() -> {
229                         try {
230                             int res = monitorEnter(monitor);
231                             Asserts.assertTrue(res == 0, "monitorEnter should return 0.");
232                             Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned");
233                             Thread.yield();
234                             if (!skipUnlock) {
235                                 res = monitorExit(monitor);
236                                 Asserts.assertTrue(res == 0, "monitorExit should return 0.");
237                                 Asserts.assertFalse(Thread.holdsLock(monitor), "monitor should be unowned");
238                             }
239                         } catch (Throwable t) {
240                             exception.set(t);
241                         }
242                         if (throwOnExit) {
243                             throw new RuntimeException(throwMsg);
244                         }
245                     });
246                 th.start();
247                 th.join();
248                 if (exception.get() != null) {
249                     throw exception.get();
250                 }
251             }
252             // Now force carrier thread to shutdown.
253             scheduler.shutdown();
254         }
255     }
256 
257     // These are the actual test case classes that get exec'd.
258 
259     static class Normal extends TestBase {
260         public static void main(String[] args) throws Throwable {
261             runTest(1, false, false);
262         }
263     }
264 
265     static class MultiNormal extends TestBase {
266         public static void main(String[] args) throws Throwable {
267             runTest(MULTI_THREAD_COUNT, false, false);
268         }
269     }
270 
271     static class MissingUnlock extends TestBase  {
272         public static void main(String[] args) throws Throwable {
273             runTest(1, true, false);
274         }
275     }
276 
277     static class MultiMissingUnlock extends TestBase {
278         public static void main(String[] args) throws Throwable {
279             runTest(MULTI_THREAD_COUNT, true, false);
280         }
281     }
282 
283     static class MissingUnlockWithThrow extends TestBase {
284         public static void main(String[] args) throws Throwable {
285             runTest(1, true, true);
286         }
287     }
288 
289     static class MultiMissingUnlockWithThrow extends TestBase {
290         public static void main(String[] args) throws Throwable {
291             runTest(MULTI_THREAD_COUNT, true, true);
292         }
293     }
294 
295 }