jdk-24/test/hotspot/jtreg/runtime/vthread/JNIMonitor/JNIMonitor.java
Patricio Chilano Mateo 78b80150e0 8338383: Implement JEP 491: Synchronize Virtual Threads without Pinning
Co-authored-by: Patricio Chilano Mateo <pchilanomate@openjdk.org>
Co-authored-by: Alan Bateman <alanb@openjdk.org>
Co-authored-by: Andrew Haley <aph@openjdk.org>
Co-authored-by: Fei Yang <fyang@openjdk.org>
Co-authored-by: Coleen Phillimore <coleenp@openjdk.org>
Co-authored-by: Richard Reingruber <rrich@openjdk.org>
Co-authored-by: Martin Doerr <mdoerr@openjdk.org>
Reviewed-by: aboldtch, dholmes, coleenp, fbredberg, dlong, sspitsyn
2024-11-12 15:23:48 +00:00

305 lines
12 KiB
Java

/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.Utils;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.process.OutputAnalyzer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*
* Tests that JNI monitors work correctly with virtual threads,
* There are multiple test scenarios that we check using unified logging output
* (both positive and negative tests). Each test case is handled by its own @-test
* definition so that we can run each sub-test independently.
*
* The original bug was only discovered because the ForkJoinPool worker thread terminated
* and trigerred an assertion failure. So we use a custom scheduler to give us control.
*/
/**
* @test id=normal
* @bug 8327743
* @summary Normal lock then unlock
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor Normal
*/
/**
* @test id=multiNormal
* @bug 8327743
* @summary Normal lock then unlock by multiple threads
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor MultiNormal
*/
/**
* @test id=missingUnlock
* @bug 8327743
* @summary Don't do the unlock and exit normally
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor MissingUnlock
*/
/**
* @test id=multiMissingUnlock
* @bug 8327743
* @summary Don't do the unlock and exit normally, by multiple threads
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor MultiMissingUnlock
*/
/**
* @test id=missingUnlockWithThrow
* @bug 8327743
* @summary Don't do the unlock and exit by throwing
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor MissingUnlockWithThrow
*/
/**
* @test id=multiMissingUnlockWithThrow
* @bug 8327743
* @summary Don't do the unlock and exit by throwing, by multiple threads
* @library /test/lib
* @modules java.base/java.lang:+open
* @requires vm.continuations
* @run driver JNIMonitor MultiMissingUnlockWithThrow
*/
public class JNIMonitor {
public static void main(String[] args) throws Exception {
String test = args[0];
String[] cmdArgs = new String[] {
"-Djava.library.path=" + Utils.TEST_NATIVE_PATH,
// Grant access to ThreadBuilders$VirtualThreadBuilder
"--add-opens=java.base/java.lang=ALL-UNNAMED",
// Enable the JNI warning
"-Xcheck:jni",
"-Xlog:jni=debug",
// Enable thread termination logging as a visual cross-check
"-Xlog:thread+os=info",
// We only count monitors in LM_LEGACY mode
"-XX:LockingMode=1",
// Disable compact headers since that switches locking mode to LM_LIGHTWEIGHT
"-XX:+UnlockExperimentalVMOptions",
"-XX:-UseCompactObjectHeaders",
"JNIMonitor$" + test,
};
OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs);
oa.shouldHaveExitValue(0);
oa.stdoutShouldMatch(terminated);
switch(test) {
case "Normal":
case "MultiNormal":
oa.stdoutShouldNotMatch(stillLocked);
break;
case "MissingUnlock":
oa.stdoutShouldMatch(stillLocked);
break;
case "MultiMissingUnlock":
parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT);
break;
case "MissingUnlockWithThrow":
oa.stdoutShouldMatch(stillLocked);
oa.stderrShouldContain(throwMsg);
break;
case "MultiMissingUnlockWithThrow":
parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT);
parseOutputForPattern(oa.stderrAsLines(), throwMsg, MULTI_THREAD_COUNT);
break;
default: throw new Error("Unknown arg: " + args[0]);
}
oa.reportDiagnosticSummary();
}
// The number of threads for a multi tests. Arbitrarily chosen to be > 1 but small
// enough to not waste too much time.
static final int MULTI_THREAD_COUNT = 5;
// The logging message for leaving a monitor JNI locked has the form
// [0.187s][debug][jni] VirtualThread (tid: 28, carrier id: 29) exiting with Objects still locked by JNI MonitorEnter.
// but if the test is run with other logging options then whitespace may get introduced in the
// log decorator sections, so ignore those.
static final String stillLocked = "VirtualThread \\(tid:.*exiting with Objects still locked by JNI MonitorEnter";
// The carrier thread termination logging has the form:
// [1.394s][info][os,thread] JavaThread exiting (name: "pool-1-thread-1", tid: 3090592).
static final String terminated = "JavaThread exiting \\(name: \"pool-1-thread-1\"";
static final String throwMsg = "Terminating via exception as requested";
// Check the process logging output for the given pattern to see if the expected number of
// lines are found.
private static void parseOutputForPattern(List<String> lines, String pattern, int expected) {
Pattern p = Pattern.compile(pattern);
int found = 0;
for (String line : lines) {
Matcher m = p.matcher(line);
if (m.find()) {
found++;
}
}
if (found != expected) {
throw new RuntimeException("Checking for pattern \"" + pattern + "\": expected "
+ expected + " but found " + found);
}
}
// straight-forward interface to JNI monitor functions
static native int monitorEnter(Object o);
static native int monitorExit(Object o);
// Isolate the native library loading to the actual test cases, not the class that
// jtreg Driver will load and execute.
static class TestBase {
static {
System.loadLibrary("JNIMonitor");
}
// This gives us a way to control the scheduler used for our virtual threads. The test
// only works as intended when the virtual threads run on the same carrier thread (as
// that carrier maintains ownership of the monitor if the virtual thread fails to unlock it).
// The original issue was also only discovered due to the carrier thread terminating
// unexpectedly, so we can force that condition too by shutting down our custom scheduler.
private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) {
Thread.Builder.OfVirtual builder = Thread.ofVirtual();
try {
Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder");
Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class);
ctor.setAccessible(true);
return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException re) {
throw re;
}
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable {
final Object[] monitors = new Object[nThreads];
for (int i = 0; i < nThreads; i++) {
monitors[i] = new Object();
}
final AtomicReference<Throwable> exception = new AtomicReference();
// Ensure all our VT's operate of the same carrier, sequentially.
ExecutorService scheduler = Executors.newSingleThreadExecutor();
ThreadFactory factory = virtualThreadBuilder(scheduler).factory();
for (int i = 0 ; i < nThreads; i++) {
Object monitor = skipUnlock ? monitors[i] : monitors[0];
Thread th = factory.newThread(() -> {
try {
int res = monitorEnter(monitor);
Asserts.assertTrue(res == 0, "monitorEnter should return 0.");
Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned");
Thread.yield();
if (!skipUnlock) {
res = monitorExit(monitor);
Asserts.assertTrue(res == 0, "monitorExit should return 0.");
Asserts.assertFalse(Thread.holdsLock(monitor), "monitor should be unowned");
}
} catch (Throwable t) {
exception.set(t);
}
if (throwOnExit) {
throw new RuntimeException(throwMsg);
}
});
th.start();
th.join();
if (exception.get() != null) {
throw exception.get();
}
}
// Now force carrier thread to shutdown.
scheduler.shutdown();
}
}
// These are the actual test case classes that get exec'd.
static class Normal extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(1, false, false);
}
}
static class MultiNormal extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(MULTI_THREAD_COUNT, false, false);
}
}
static class MissingUnlock extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(1, true, false);
}
}
static class MultiMissingUnlock extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(MULTI_THREAD_COUNT, true, false);
}
}
static class MissingUnlockWithThrow extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(1, true, true);
}
}
static class MultiMissingUnlockWithThrow extends TestBase {
public static void main(String[] args) throws Throwable {
runTest(MULTI_THREAD_COUNT, true, true);
}
}
}