78b80150e0
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
305 lines
12 KiB
Java
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);
|
|
}
|
|
}
|
|
|
|
}
|