Skip to content

Commit 06451df

Browse files
authored
Supress internal ThreadAbortedError in user threads (#526)
1 parent 1de201b commit 06451df

File tree

14 files changed

+179
-74
lines changed

14 files changed

+179
-74
lines changed

bootstrap/src/sun/nio/ch/lincheck/EventTracker.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public interface EventTracker {
2323
void beforeThreadStart();
2424
void afterThreadFinish();
2525
void threadJoin(Thread thread, boolean withTimeout);
26+
void onThreadRunException(Throwable exception);
2627

2728
void beforeLock(int codeLocation);
2829
void lock(Object monitor);

bootstrap/src/sun/nio/ch/lincheck/Injections.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,21 @@ public static void afterThreadFinish() {
177177
tracker.afterThreadFinish();
178178
}
179179

180+
/**
181+
* Called from thread's {@code run} method failed with an exception.
182+
*
183+
* @param exception the exception that was thrown in the thread.
184+
*/
185+
public static void onThreadRunException(Throwable exception) {
186+
Thread thread = Thread.currentThread();
187+
// TestThread is handled separately
188+
if (thread instanceof TestThread) return;
189+
ThreadDescriptor descriptor = ThreadDescriptor.getCurrentThreadDescriptor();
190+
if (descriptor == null) return;
191+
EventTracker tracker = descriptor.getEventTracker();
192+
tracker.onThreadRunException(exception);
193+
}
194+
180195
/**
181196
* Called from instrumented code instead of {@code thread.join()}.
182197
*/

src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ internal fun executeActor(
5757
throw invE
5858
// Exception thrown not during the method invocation should contain underlying exception
5959
return ExceptionResult.create(
60-
invE.cause?.takeIf { exceptionCanBeValidExecutionResult(it) }
60+
invE.cause?.takeIf { !isInternalException(it) }
6161
?: throw invE
6262
)
6363
} catch (e: Exception) {
@@ -187,12 +187,6 @@ internal fun collectThreadDump(runner: Runner) = Thread.getAllStackTraces().filt
187187

188188
internal val String.canonicalClassName get() = this.replace('/', '.')
189189

190-
@Suppress("DEPRECATION") // ThreadDeath
191-
internal fun exceptionCanBeValidExecutionResult(exception: Throwable): Boolean {
192-
return exception !is ThreadDeath && // is used to stop thread in `FixedActiveThreadsExecutor` via `thread.stop()`
193-
exception !is ThreadAbortedError // is used to abort thread in `ManagedStrategy`
194-
}
195-
196190
internal val Throwable.text: String get() {
197191
val writer = StringWriter()
198192
printStackTrace(PrintWriter(writer))

src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,8 +487,6 @@ internal open class ParallelThreadsRunner(
487487
override fun isCurrentRunnerThread(thread: Thread): Boolean = executor.threads.any { it === thread }
488488

489489
override fun onThreadFinish(iThread: Int) {}
490-
491-
override fun onThreadFailure(iThread: Int, e: Throwable) {}
492490
}
493491

494492
internal enum class UseClocks { ALWAYS, RANDOM }

src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.lincheck.*
1313
import org.jetbrains.kotlinx.lincheck.annotations.*
1414
import org.jetbrains.kotlinx.lincheck.execution.*
1515
import org.jetbrains.kotlinx.lincheck.strategy.*
16+
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
1617
import java.io.*
1718
import java.lang.reflect.*
1819
import java.util.concurrent.atomic.*
@@ -59,12 +60,6 @@ abstract class Runner protected constructor(
5960
*/
6061
abstract fun onThreadFinish(iThread: Int)
6162

62-
/**
63-
* This method is invoked by the corresponding test thread
64-
* when an unexpected exception is thrown.
65-
*/
66-
abstract fun onThreadFailure(iThread: Int, e: Throwable)
67-
6863
/**
6964
* This method is invoked by the corresponding test thread
7065
* when the current coroutine suspends.
@@ -99,13 +94,30 @@ abstract class Runner protected constructor(
9994
}
10095

10196
/**
102-
* Is invoked after each actor execution from the specified thread, even if a legal exception was thrown.
97+
* Is invoked after each actor execution from the specified thread.
10398
* The invocations are inserted into the generated code.
10499
*/
105100
fun onActorFinish() {
106101
strategy.onActorFinish()
107102
}
108103

104+
/**
105+
* Is invoked if an actor execution has thrown an exception.
106+
*
107+
* Default implementation checks if the failure
108+
* was caused by an internal exception (see [isInternalException]) and re-throw in this case,
109+
* otherwise it treats the exception as a normal actor result.
110+
*
111+
* @param iThread the number of the invoking thread where the failure occurred.
112+
* @param throwable the exception that caused the actor failure.
113+
*/
114+
// used in byte-code generation
115+
open fun onActorFailure(iThread: Int, throwable: Throwable) {
116+
if (isInternalException(throwable)) {
117+
throw throwable
118+
}
119+
}
120+
109121
fun beforePart(part: ExecutionPart) {
110122
completedOrSuspendedThreads.set(0)
111123
currentExecutionPart = part
@@ -133,3 +145,15 @@ abstract class Runner protected constructor(
133145
enum class ExecutionPart {
134146
INIT, PARALLEL, POST, VALIDATION
135147
}
148+
149+
/**
150+
* Checks if the provided exception is considered an internal exception.
151+
* Internal exceptions are those used by the Lincheck itself
152+
* to control execution of the analyzed code.
153+
*/
154+
@Suppress("DEPRECATION") // ThreadDeath
155+
internal fun isInternalException(exception: Throwable): Boolean =
156+
// is used to stop thread in `FixedActiveThreadsExecutor` via `thread.stop()`
157+
exception is ThreadDeath ||
158+
// is used to abort thread in `ManagedStrategy`
159+
exception is LincheckAnalysisAbortedError

src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
import org.jetbrains.kotlinx.lincheck.Result;
1313

14-
import static org.jetbrains.kotlinx.lincheck.UtilsKt.*;
15-
1614

1715
/**
1816
* Instance of this class represents the test execution for ONE thread. Several instances should be run in parallel.
@@ -53,12 +51,4 @@ public void incClock() {
5351
curClock++;
5452
}
5553

56-
// used in byte-code generation
57-
public void failOnExceptionIsUnexpected(int iThread, Throwable e) throws Throwable {
58-
if (!exceptionCanBeValidExecutionResult(e)) {
59-
runner.onThreadFailure(iThread, e);
60-
throw e;
61-
}
62-
}
63-
6454
}

src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecutionGenerator.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public class TestThreadExecutionGenerator {
7070
private static final Method PARALLEL_THREADS_RUNNER_PROCESS_INVOCATION_RESULT_METHOD = new Method("processInvocationResult", RESULT_TYPE, new Type[]{ OBJECT_TYPE, INT_TYPE, INT_TYPE });
7171
private static final Method RUNNER_IS_PARALLEL_EXECUTION_COMPLETED_METHOD = new Method("isParallelExecutionCompleted", BOOLEAN_TYPE, new Type[]{});
7272

73-
private static final Method TEST_THREAD_EXECUTION_FAIL_ON_EXCEPTION_IF_UNEXPECTED = new Method("failOnExceptionIsUnexpected", VOID_TYPE, new Type[]{INT_TYPE, THROWABLE_TYPE});
73+
private static final Method RUNNER_ON_ACTOR_FAILURE_METHOD = new Method("onActorFailure", VOID_TYPE, new Type[]{INT_TYPE, THROWABLE_TYPE});
74+
7475
private static int generatedClassNumber = 0;
7576

7677
static {
@@ -149,6 +150,14 @@ private static void generateRun(ClassVisitor cv, Type testType, int iThread, Lis
149150
mv.getField(TEST_THREAD_EXECUTION_TYPE, "runner", RUNNER_TYPE);
150151
mv.push(iThread);
151152
mv.invokeVirtual(RUNNER_TYPE, RUNNER_ON_THREAD_START_METHOD);
153+
154+
// wrap actor's running loop in try-finally
155+
Label actorsRunningLoopBlockStart = mv.newLabel();
156+
Label actorsRunningLoopBlockEnd = mv.newLabel();
157+
Label actorsRunningLoopBlockFinally = mv.newLabel();
158+
mv.visitTryCatchBlock(actorsRunningLoopBlockStart, actorsRunningLoopBlockEnd, actorsRunningLoopBlockFinally, null);
159+
160+
mv.visitLabel(actorsRunningLoopBlockStart);
152161
// Number of current operation (starts with 0)
153162
int iLocal = mv.newLocal(INT_TYPE);
154163
mv.push(0);
@@ -232,11 +241,14 @@ private static void generateRun(ClassVisitor cv, Type testType, int iThread, Lis
232241
int eLocal = mv.newLocal(THROWABLE_TYPE);
233242
mv.storeLocal(eLocal);
234243

244+
// push the runner on stack to call its method
235245
mv.loadThis();
246+
mv.getField(TEST_THREAD_EXECUTION_TYPE, "runner", RUNNER_TYPE);
247+
// push iThread and exception on stack
236248
mv.push(iThread);
237249
mv.loadLocal(eLocal);
238-
// Fail if this exception is not a valid execution result
239-
mv.invokeVirtual(TEST_THREAD_EXECUTION_TYPE, TEST_THREAD_EXECUTION_FAIL_ON_EXCEPTION_IF_UNEXPECTED);
250+
// Fail if this exception is an internal exception
251+
mv.invokeVirtual(RUNNER_TYPE, RUNNER_ON_ACTOR_FAILURE_METHOD);
240252

241253
mv.loadLocal(eLocal);
242254

@@ -270,13 +282,28 @@ private static void generateRun(ClassVisitor cv, Type testType, int iThread, Lis
270282
mv.iinc(iLocal, 1);
271283
mv.visitLabel(launchNextActor);
272284
}
285+
mv.visitInsn(ACONST_NULL); // push null exception value indicating normal method's termination
286+
mv.goTo(actorsRunningLoopBlockFinally);
287+
mv.visitLabel(actorsRunningLoopBlockEnd);
288+
289+
mv.visitLabel(actorsRunningLoopBlockFinally);
273290
// Call runner's onThreadFinish(iThread) method
274291
mv.loadThis();
275292
mv.getField(TEST_THREAD_EXECUTION_TYPE, "runner", RUNNER_TYPE);
276293
mv.push(iThread);
277294
mv.invokeVirtual(RUNNER_TYPE, RUNNER_ON_THREAD_FINISH_METHOD);
295+
296+
// Check if an exception was thrown in the actors' running loop and re-throw it
297+
Label methodReturnLabel = mv.newLabel();
298+
mv.dup();
299+
mv.ifNull(methodReturnLabel);
300+
mv.throwException(); // re-throw exception
301+
278302
// Complete the method
303+
mv.visitLabel(methodReturnLabel);
304+
mv.pop(); // pop null exception value indicating normal method's termination
279305
mv.visitInsn(RETURN);
306+
280307
mv.visitMaxs(3, 4);
281308
mv.visitEnd();
282309
}

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/LoopDetector.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import java.util.ArrayList
5858
* - For instance, if the [currentInterleavingHistory] is [0: 2], [1: 3], [0: 3], [1: 3], [0: 3], ..., [1: 3], [0: 3] and a deadlock is detected,
5959
* the cycle is identified as [1: 3], [0: 3].
6060
* This means 2 executions in thread 0 and 3 executions in both threads 1 and 0 will be allowed.
61-
* - Execution is halted after the last execution in thread 0 using [ThreadAbortedError].
61+
* - Execution is halted after the last execution in thread 0 using [LincheckAnalysisAbortedError].
6262
* - The logic for tracking executions and switches in replay mode is implemented in [ReplayModeLoopDetectorHelper].
6363
*
6464
* Note: An example of this behavior is detailed in the comments of the code itself.

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,28 @@ abstract class ManagedStrategy(
523523
onThreadFinish(currentThreadId)
524524
}
525525

526+
/**
527+
* Handles exceptions that occur in a specific thread.
528+
* This method is called when a thread finishes with an exception.
529+
*
530+
* @param exception The exception that was thrown within the thread.
531+
*/
532+
override fun onThreadRunException(exception: Throwable) = runInIgnoredSection {
533+
val currentThreadId = threadScheduler.getCurrentThreadId()
534+
// do not track unregistered threads
535+
if (currentThreadId < 0) return
536+
// scenario threads are handled separately by the runner itself
537+
if (currentThreadId < scenario.nThreads) return
538+
// check if the exception is internal
539+
if (isInternalException(exception)) {
540+
onInternalException(currentThreadId, exception)
541+
} else {
542+
// re-throw any non-internal exception,
543+
// so it will be treated as the final result of `Thread::run`.
544+
throw exception
545+
}
546+
}
547+
526548
override fun threadJoin(thread: Thread?, withTimeout: Boolean) = runInIgnoredSection {
527549
if (withTimeout) return // timeouts occur instantly
528550
val currentThreadId = threadScheduler.getCurrentThreadId()
@@ -593,46 +615,51 @@ abstract class ManagedStrategy(
593615

594616
/**
595617
* This method is executed as the first thread action.
596-
* @param iThread the number of the executed thread according to the [scenario][ExecutionScenario].
618+
*
619+
* @param threadId the thread id of the started thread.
597620
*/
598-
open fun onThreadStart(iThread: Int) {
599-
threadScheduler.awaitTurn(iThread)
600-
threadScheduler.startThread(iThread)
621+
open fun onThreadStart(threadId: ThreadId) {
622+
threadScheduler.awaitTurn(threadId)
623+
threadScheduler.startThread(threadId)
601624
}
602625

603626
/**
604-
* This method is executed as the last thread action if no exception has been thrown.
605-
* @param iThread the number of the executed thread according to the [scenario][ExecutionScenario].
627+
* This method is executed as the last thread action.
628+
*
629+
* @param threadId the thread id of the finished thread.
606630
*/
607-
open fun onThreadFinish(iThread: Int) {
608-
threadScheduler.awaitTurn(iThread)
609-
threadScheduler.finishThread(iThread)
610-
loopDetector.onThreadFinish(iThread)
631+
open fun onThreadFinish(threadId: ThreadId) {
632+
threadScheduler.awaitTurn(threadId)
633+
threadScheduler.finishThread(threadId)
634+
loopDetector.onThreadFinish(threadId)
611635
traceCollector?.onThreadFinish()
612-
unblockJoiningThreads(iThread)
613-
val nextThread = chooseThreadSwitch(iThread, true)
636+
unblockJoiningThreads(threadId)
637+
val nextThread = chooseThreadSwitch(threadId, true)
614638
setCurrentThread(nextThread)
615639
}
616640

617641
/**
618-
* This method is executed if an illegal exception has been thrown (see [exceptionCanBeValidExecutionResult]).
619-
* @param iThread the number of the executed thread according to the [scenario][ExecutionScenario].
620-
* @param exception the exception that was thrown
642+
* This method is executed if an internal exception has been thrown (see [isInternalException]).
643+
*
644+
* @param threadId the thread id of the thread where exception was thrown.
645+
* @param exception the exception that was thrown.
621646
*/
622-
open fun onThreadFailure(iThread: Int, exception: Throwable) {
623-
// This method is called only if exception can't be treated as a normal operation result,
647+
open fun onInternalException(threadId: Int, exception: Throwable) {
648+
check(isInternalException(exception))
649+
// This method is called only if the exception cannot be treated as a normal result,
624650
// so we exit testing code to avoid trace collection resume or some bizarre bugs
625651
leaveTestingCode()
626-
// skip abort exception
627-
if (exception !== ThreadAbortedError) {
628-
// Despite the fact that the corresponding failure will be detected by the runner,
629-
// the managed strategy can construct a trace to reproduce this failure.
630-
// Let's then store the corresponding failing result and construct the trace.
631-
suddenInvocationResult = UnexpectedExceptionInvocationResult(exception, runner.collectExecutionResults())
632-
threadScheduler.abortAllThreads()
633-
}
634-
// notify the scheduler that the thread is going to be finished
635-
threadScheduler.finishThread(iThread)
652+
// suppress `ThreadAbortedError`
653+
if (exception is LincheckAnalysisAbortedError) return
654+
// Though the corresponding failure will be detected by the runner,
655+
// the managed strategy can construct a trace to reproduce this failure.
656+
// Let's then store the corresponding failing result and construct the trace.
657+
suddenInvocationResult = UnexpectedExceptionInvocationResult(
658+
exception,
659+
runner.collectExecutionResults()
660+
)
661+
threadScheduler.abortAllThreads()
662+
throw exception
636663
}
637664

638665
override fun onActorStart(iThread: Int) {
@@ -1996,8 +2023,10 @@ internal class ManagedStrategyRunner(
19962023
managedStrategy.onThreadFinish(iThread)
19972024
}
19982025

1999-
override fun onThreadFailure(iThread: Int, e: Throwable) = runInIgnoredSection {
2000-
managedStrategy.onThreadFailure(iThread, e)
2026+
override fun onActorFailure(iThread: Int, throwable: Throwable) = runInIgnoredSection {
2027+
if (isInternalException(throwable)) {
2028+
managedStrategy.onInternalException(iThread, throwable)
2029+
}
20012030
}
20022031

20032032
override fun afterCoroutineSuspended(iThread: Int) = runInIgnoredSection {

0 commit comments

Comments
 (0)