8324868: debug agent does not properly handle interrupts of a virtual thread

Reviewed-by: sspitsyn, amenkov
This commit is contained in:
Chris Plummer 2024-03-12 20:54:18 +00:00
parent 22f10e045b
commit 966a42f9b3
3 changed files with 245 additions and 130 deletions

View File

@ -1549,7 +1549,7 @@ threadControl_resumeAll(void)
* resume any vthread with a suspendCount == 1, and we want to ignore
* vthreads with a suspendCount > 0. Therefore we don't want
* ResumeAllVirtualThreads resuming these vthreads. We must first
* build a list of them to pass to as the exclude list.
* build a list of them to pass as the exclude list.
*/
enumerateOverThreadList(env, &runningVThreads, excludeCountHelper,
&excludeCnt);
@ -2117,28 +2117,25 @@ threadControl_onEventHandlerEntry(jbyte sessionID, EventInfo *evinfo, jobject cu
}
static void
doPendingTasks(JNIEnv *env, ThreadNode *node)
doPendingTasks(JNIEnv *env, jthread thread, int pendingInterrupt, jobject pendingStop)
{
/*
* Take care of any pending interrupts/stops, and clear out
* info on pending interrupts/stops.
* Take care of any pending interrupts/stops.
*/
if (node->pendingInterrupt) {
if (pendingInterrupt) {
JVMTI_FUNC_PTR(gdata->jvmti,InterruptThread)
(gdata->jvmti, node->thread);
(gdata->jvmti, thread);
/*
* TO DO: Log error
*/
node->pendingInterrupt = JNI_FALSE;
}
if (node->pendingStop != NULL) {
if (pendingStop != NULL) {
JVMTI_FUNC_PTR(gdata->jvmti,StopThread)
(gdata->jvmti, node->thread, node->pendingStop);
(gdata->jvmti, thread, pendingStop);
/*
* TO DO: Log error
*/
tossGlobalRef(env, &(node->pendingStop));
}
}
@ -2147,35 +2144,44 @@ threadControl_onEventHandlerExit(EventIndex ei, jthread thread,
struct bag *eventBag)
{
ThreadNode *node;
JNIEnv *env = getEnv();
log_debugee_location("threadControl_onEventHandlerExit()", thread, NULL, 0);
if (ei == EI_THREAD_END) {
eventHandler_lock(); /* for proper lock order */
}
debugMonitorEnter(threadLock);
node = findRunningThread(thread);
if (node == NULL) {
EXIT_ERROR(AGENT_ERROR_NULL_POINTER,"thread list corrupted");
} else {
JNIEnv *env;
env = getEnv();
if (ei == EI_THREAD_END) {
removeThread(env, node);
node = NULL; /* has been freed */
} else {
/* No point in doing this if the thread is about to die.*/
doPendingTasks(env, node);
node->eventBag = eventBag;
node->current_ei = 0;
eventHandler_lock(); /* for proper lock order - see removeThread() call below */
debugMonitorEnter(threadLock);
node = findRunningThread(thread);
if (node == NULL) {
EXIT_ERROR(AGENT_ERROR_NULL_POINTER,"thread list corrupted");
}
}
debugMonitorExit(threadLock);
if (ei == EI_THREAD_END) {
removeThread(env, node); // Grabs handlerLock, thus the reason for first grabbing it above.
node = NULL; // We exiting the threadLock. No longer safe to access.
debugMonitorExit(threadLock);
eventHandler_unlock();
} else {
debugMonitorEnter(threadLock);
node = findRunningThread(thread);
if (node == NULL) {
EXIT_ERROR(AGENT_ERROR_NULL_POINTER,"thread list corrupted");
}
/* No need to do the following if the thread is about to die.*/
int pendingInterrupt = node->pendingInterrupt;
jobject pendingStop = node->pendingStop;
jthread thread = node->thread;
node->pendingInterrupt = JNI_FALSE;
node->pendingStop = NULL;
node->eventBag = eventBag;
node->current_ei = 0;
node = NULL; // We exiting the threadLock. No longer safe to access.
// doPendingTasks() may do an upcall to java, and we don't want to hold any
// locks when doing that. Thus we got all our node updates done first
// and can now exit the threadLock.
debugMonitorExit(threadLock);
doPendingTasks(env, thread, pendingInterrupt, pendingStop);
if (pendingStop != NULL) {
tossGlobalRef(env, &pendingStop);
}
}
}
@ -2219,29 +2225,9 @@ threadControl_applicationThreadStatus(jthread thread,
jvmtiError
threadControl_interrupt(jthread thread)
{
ThreadNode *node;
jvmtiError error;
error = JVMTI_ERROR_NONE;
log_debugee_location("threadControl_interrupt()", thread, NULL, 0);
debugMonitorEnter(threadLock);
node = findThread(&runningThreads, thread);
if ((node == NULL) || !HANDLING_EVENT(node)) {
error = JVMTI_FUNC_PTR(gdata->jvmti,InterruptThread)
(gdata->jvmti, thread);
} else {
/*
* Hold any interrupts until after the event is processed.
*/
node->pendingInterrupt = JNI_TRUE;
}
debugMonitorExit(threadLock);
return error;
return JVMTI_FUNC_PTR(gdata->jvmti,InterruptThread)(gdata->jvmti, thread);
}
void
@ -2308,26 +2294,20 @@ threadControl_saveCLEInfo(JNIEnv *env, jthread thread, EventIndex ei,
debugMonitorExit(threadLock);
}
/*
* Support for getting an interrupt in an application thread while doing
* a debugMonitorWait().
*/
void
threadControl_setPendingInterrupt(jthread thread)
{
ThreadNode *node;
/*
* vmTestbase/nsk/jdb/kill/kill001/kill001.java seems to be the only code
* that triggers this function. Is uses ThreadReference.Stop.
*
* Since ThreadReference.Stop is not supported for vthreads, we should never
* get here with a vthread.
*/
JDI_ASSERT(!isVThread(thread));
debugMonitorEnter(threadLock);
node = findThread(&runningThreads, thread);
if (node != NULL) {
node->pendingInterrupt = JNI_TRUE;
}
node = findRunningThread(thread);
JDI_ASSERT(node != NULL);
node->pendingInterrupt = JNI_TRUE;
debugMonitorExit(threadLock);
}
@ -2512,23 +2492,13 @@ threadControl_setEventMode(jvmtiEventMode mode, EventIndex ei, jthread thread)
}
/*
* Returns the current thread, if the thread has generated at least
* one event, and has not generated a thread end event.
* Returns the current thread.
*/
jthread
threadControl_currentThread(void)
{
jthread thread;
debugMonitorEnter(threadLock);
{
ThreadNode *node;
node = findThread(&runningThreads, NULL);
thread = (node == NULL) ? NULL : node->thread;
}
debugMonitorExit(threadLock);
jthread thread = NULL;
jvmtiError error = JVMTI_FUNC_PTR(gdata->jvmti,GetCurrentThread)(gdata->jvmti, &thread);
return thread;
}
@ -2670,6 +2640,7 @@ dumpThread(ThreadNode *node) {
tty_message("\tthreadState: 0x%x", getThreadState(node));
tty_message("\ttoBeResumed: %d", node->toBeResumed);
tty_message("\tis_vthread: %d", node->is_vthread);
tty_message("\tpendingInterrupt: %d", node->pendingInterrupt);
tty_message("\tcurrentInvoke.pending: %d", node->currentInvoke.pending);
tty_message("\tcurrentInvoke.started: %d", node->currentInvoke.started);
tty_message("\tcurrentInvoke.available: %d", node->currentInvoke.available);

View File

@ -27,5 +27,6 @@
#
#############################################################################
java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all
java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all
java/lang/management/MemoryMXBean/CollectionUsageThreshold.java 8318668 generic-all
com/sun/jdi/InterruptHangTest.java 8306679,8043571 generic-all

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2006, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2006, 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
@ -28,76 +28,175 @@ import com.sun.jdi.request.*;
/**
* @test
* @bug 6459476
* @summary Debuggee is blocked, looks like running
* @summary Test interrupting debuggee with single stepping enable
* @author jjh
*
* @run build TestScaffold VMConnection TargetListener TargetAdapter
* @run compile -g InterruptHangTest.java
* @run driver InterruptHangTest
* @run driver InterruptHangTest precise
* @run driver InterruptHangTest aggressive
* @run driver InterruptHangTest remote
*/
/**
* Debuggee has two threads. Debugger keeps stepping in
* the first thread. The second thread keeps interrupting the first
* thread. If a long time goes by with the debugger not getting
* a step event, the test fails.
* The debuggee loops in the main thread while the debugger has single stepping
* enabled for the debuggee's main thread. The main thread is repeatedly
* interrupted while it is looping. If a long time goes by with the debugger
* not getting a single step event, the test fails.
*
* Interrupts are generated in 3 difference modes:
* precise - The debuggee creates a 2nd thread that repeatedly calls
* Thread.interrupt() on the main thread, but does so in a controlled
* fashion that allows to test to verify that every interrupt is
* received and handled.
* aggressive - The debuggee creates a 2nd thread that repeatedly calls
* Thread.interrupt() on the main thread, but does so at a high pace
* and without any coordination with the main thread. Because of
* this it is not possible to account for all the interrupts since some
* might be done before the previous interrupt is handled.
* remote - Much like the "aggressive" mode, except interrupts are remotely
* generated by the debugger using ThreadReference.interrupt(). There
* is no "precise" mode for remotely generated interrupts since it is
* difficult to coordinate between the debugger and debuggee in a
* reasonable way.
*/
class InterruptHangTarg {
public static String sync = "sync";
public static void main(String[] args){
int answer = 0;
System.out.println("Howdy!");
Thread interruptorThread = DebuggeeWrapper.newThread(new Interruptor(Thread.currentThread()));
synchronized(sync) {
class InterruptHangTarg {
static String sync = "sync";
static int interruptsSent;
static boolean remoteMode;
static boolean preciseMode;
static boolean aggressiveMode;
private final static int INTERRUPTS_EXPECTED = 200;
public static void main(String[] args){
int answer = 0; // number of interrupts answered
System.out.println("Howdy!");
remoteMode = "remote".equals(args[0]);
preciseMode = "precise".equals(args[0]);
aggressiveMode = "aggressive".equals(args[0]);
if (!remoteMode && !preciseMode && !aggressiveMode) {
throw new RuntimeException("Invalid first arg for debuggee: " + args[0]);
}
// Create the debuggee interruptor thread if needed.
Thread interruptorThread;
if (preciseMode) {
interruptorThread =
DebuggeeWrapper.newThread(new PreciseInterruptor(Thread.currentThread()));
interruptorThread.start();
try {
sync.wait();
} catch (InterruptedException ee) {
System.out.println("Debuggee interruptee: interrupted before starting loop");
}
} else if (aggressiveMode) {
interruptorThread =
DebuggeeWrapper.newThread(new AggressiveInterruptor(Thread.currentThread()));
interruptorThread.start();
} else {
interruptorThread = null; // we are in "remote" interrupt mode
}
// Debugger will keep stepping thru this loop
for (int ii = 0; ii < 200; ii++) {
answer++;
for (int ii = 0; ii < INTERRUPTS_EXPECTED; ii++) {
boolean wasInterrupted = false;
try {
// Give other thread a chance to run
// Give other thread a chance to interrupt
Thread.sleep(100);
} catch (InterruptedException ee) {
System.out.println("Debuggee interruptee: interrupted at iteration: "
+ ii);
answer++;
wasInterrupted = true;
boolean isInterrupted = Thread.currentThread().isInterrupted();
System.out.println("Debuggee interruptee(" + ii + "): isInterrupted(" + isInterrupted + ")");
if (preciseMode) {
// When Thread.sleep() is interrupted, the interrupted status of the thread
// should remain cleared (since InterruptedException was thrown), and the
// the next interrupt won't come in until after the sync.notify() below.
// Note this is not true for the aggressive and remote modes since
// interrupts can come in at any time, even while we are handling
// an intrrupt.
if (isInterrupted) {
throw new RuntimeException("Thread should not have interrupted status set.");
}
synchronized(InterruptHangTarg.sync) {
// Let the interruptor thread know it can start interrupting again
sync.notify();
}
}
}
// Every Thread.sleep() call should get interrupted
if (!wasInterrupted) {
throw new RuntimeException("Thread was never interrupted during sleep: " + ii);
}
}
// Kill the interrupter thread
interruptorThread.interrupt();
if (interruptorThread != null) {
synchronized(InterruptHangTarg.sync) {
// Kill the interrupter thread
interruptorThread.interrupt();
}
}
// Print how many times an interrupt was sent. When in remote mode, the RemoteInterruptor
// is in a different process, so interruptsSent is not updated, therefore we don't
// print it here. The RemoteInterruptor thread keeps its own count and prints
// it before it exits.
if (!remoteMode) {
System.out.println("interrupts sent: " + interruptsSent);
}
// When in precise mode, make sure that every interrupt sent resulted in
// an InterruptedException. Note the interruptor always ends up sending
// one extra interrupt at the end.
if (preciseMode && interruptsSent != answer + 1) {
throw new RuntimeException("Too many interrupts sent. Sent: " + interruptsSent +
". Expected to send: " + answer + 1);
}
System.out.println("Goodbye from InterruptHangTarg!");
}
}
class Interruptor implements Runnable {
Thread interruptee;
Interruptor(Thread interruptee) {
this.interruptee = interruptee;
}
public void run() {
synchronized(InterruptHangTarg.sync) {
InterruptHangTarg.sync.notify();
static class AggressiveInterruptor implements Runnable {
Thread interruptee;
AggressiveInterruptor(Thread interruptee) {
this.interruptee = interruptee;
}
int ii = 0;
while(true) {
ii++;
interruptee.interrupt();
try {
Thread.sleep(10);
} catch (InterruptedException ee) {
System.out.println("Debuggee Interruptor: finished after " +
ii + " iterrupts");
break;
public void run() {
while (true) {
InterruptHangTarg.interruptsSent++;
interruptee.interrupt();
try {
Thread.sleep(5);
} catch (InterruptedException ee) {
System.out.println("Debuggee Interruptor: finished after " +
InterruptHangTarg.interruptsSent + " iterrupts");
break;
}
}
}
}
static class PreciseInterruptor implements Runnable {
Thread interruptee;
PreciseInterruptor(Thread interruptee) {
this.interruptee = interruptee;
}
public void run() {
synchronized(InterruptHangTarg.sync) {
while (true) {
InterruptHangTarg.interruptsSent++;
interruptee.interrupt();
try {
// Wait until the interruptee has handled the interrupt
InterruptHangTarg.sync.wait();
} catch (InterruptedException ee) {
System.out.println("Debuggee Interruptor: finished after " +
InterruptHangTarg.interruptsSent + " iterrupts");
break;
}
}
}
}
}
}
@ -105,16 +204,48 @@ class Interruptor implements Runnable {
/********** test program **********/
public class InterruptHangTest extends TestScaffold {
class RemoteInterruptor extends Thread {
static int interruptsSent;
ThreadReference interruptee;
RemoteInterruptor(ThreadReference interruptee) {
this.interruptee = interruptee;
}
public void run() {
try {
while (true) {
interruptee.interrupt();
interruptsSent++;
Thread.sleep(5);
}
} catch (InterruptedException ee) {
println("RemoteInterruptor thread: Unexpected Interrupt");
throw new RuntimeException(ee);
} catch (IllegalThreadStateException | VMDisconnectedException ee) {
println("RemoteInterruptor thread: Got expected " + ee.getClass().getSimpleName()
+ " after " + interruptsSent + " interrupts sent. Exiting.");
} catch (Throwable ee) {
println("RemoteInterruptor thread: Got unexpected exception after "
+ interruptsSent + " interrupts sent. Exiting with exception.");
ee.printStackTrace(System.out);
throw new RuntimeException(ee);
}
}
}
ThreadReference mainThread;
Thread timerThread;
String sync = "sync";
static int nSteps = 0;
static boolean remoteMode;
InterruptHangTest (String args[]) {
super(args);
}
public static void main(String[] args) throws Exception {
remoteMode = "remote".equals(args[0]);
new InterruptHangTest(args).startTests();
}
@ -174,11 +305,23 @@ public class InterruptHangTest extends TestScaffold {
}
};
Thread remoteInterruptorThread = null;
if (remoteMode) {
// Create a thread to call ThreadReference.interrupt() on the
// debuggee main thread.
remoteInterruptorThread = new RemoteInterruptor(mainThread);
remoteInterruptorThread.setName("RemoteInterruptor");
remoteInterruptorThread.setDaemon(true);
remoteInterruptorThread.start();
}
/*
* resume the target listening for events
*/
listenUntilVMDisconnect();
if (remoteInterruptorThread != null) {
remoteInterruptorThread.join();
}
timerThread.interrupt();
/*
@ -186,9 +329,9 @@ public class InterruptHangTest extends TestScaffold {
* if anything has called failure("foo") testFailed will be true
*/
if (!testFailed) {
println("InterruptHangTest: passed");
println("InterruptHangTest("+ args[0] + "): passed");
} else {
throw new Exception("InterruptHangTest: failed");
throw new Exception("InterruptHangTest("+ args[0] + "): failed");
}
}
}