Skip to content

Commit f1710a7

Browse files
committed
More advanced check for self-suppression during the final exception building in Job with enabled stacktrace recovery
* Efficiently prevent cycles for recovered1(original).addSuppressed(recovered2(original)) * Disable virtual time output in regular tests
1 parent 26a14ee commit f1710a7

File tree

3 files changed

+40
-13
lines changed

3 files changed

+40
-13
lines changed

kotlinx-coroutines-core/common/src/JobSupport.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,16 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
250250
private fun addSuppressedExceptions(rootCause: Throwable, exceptions: List<Throwable>) {
251251
if (exceptions.size <= 1) return // nothing more to do here
252252
val seenExceptions = identitySet<Throwable>(exceptions.size)
253+
/*
254+
* Note that root cause may be a recovered exception as well.
255+
* To avoid cycles we unwrap the root cause and check for self-suppression against unwrapped cause,
256+
* but add suppressed exceptions to the recovered root cause (as it is our final exception)
257+
*/
258+
val unwrappedCause = unwrap(rootCause)
253259
for (exception in exceptions) {
254260
val unwrapped = unwrap(exception)
255-
if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(unwrapped)) {
261+
if (unwrapped !== rootCause && unwrapped !== unwrappedCause &&
262+
unwrapped !is CancellationException && seenExceptions.add(unwrapped)) {
256263
rootCause.addSuppressedThrowable(unwrapped)
257264
}
258265
}

kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import java.util.concurrent.locks.*
1010

1111
private const val SHUTDOWN_TIMEOUT = 1000L
1212

13-
internal inline fun withVirtualTimeSource(log: PrintStream = System.`out`, block: () -> Unit) {
13+
internal inline fun withVirtualTimeSource(log: PrintStream? = null, block: () -> Unit) {
1414
DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) // shutdown execution with old time source (in case it was working)
1515
val testTimeSource = VirtualTimeSource(log)
1616
timeSource = testTimeSource
@@ -41,7 +41,7 @@ private const val REAL_PARK_NANOS = 10_000_000L // 10 ms -- park for a little to
4141

4242
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
4343
internal class VirtualTimeSource(
44-
private val log: PrintStream
44+
private val log: PrintStream?
4545
) : TimeSource {
4646
private val mainThread: Thread = Thread.currentThread()
4747
private var checkpointNanos: Long = System.nanoTime()
@@ -138,7 +138,7 @@ internal class VirtualTimeSource(
138138
}
139139

140140
private fun logTime(s: String) {
141-
log.println("[$s: Time = ${TimeUnit.NANOSECONDS.toMillis(time)} ms]")
141+
log?.println("[$s: Time = ${TimeUnit.NANOSECONDS.toMillis(time)} ms]")
142142
}
143143

144144
private fun minParkedTill(): Long =

kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class StackTraceRecoveryTest : TestBase() {
2020
fun testAsync() = runTest {
2121
fun createDeferred(depth: Int): Deferred<*> {
2222
return if (depth == 0) {
23-
async(coroutineContext + NonCancellable) {
23+
async<Unit>(coroutineContext + NonCancellable) {
2424
throw ExecutionException(null)
2525
}
2626
} else {
@@ -47,7 +47,7 @@ class StackTraceRecoveryTest : TestBase() {
4747

4848
@Test
4949
fun testCompletedAsync() = runTest {
50-
val deferred = async(coroutineContext + NonCancellable) {
50+
val deferred = async<Unit>(coroutineContext + NonCancellable) {
5151
throw ExecutionException(null)
5252
}
5353

@@ -133,7 +133,7 @@ class StackTraceRecoveryTest : TestBase() {
133133

134134
@Test
135135
fun testWithContext() = runTest {
136-
val deferred = async(NonCancellable, start = CoroutineStart.LAZY) {
136+
val deferred = async<Unit>(NonCancellable, start = CoroutineStart.LAZY) {
137137
throw RecoverableTestException()
138138
}
139139

@@ -152,25 +152,26 @@ class StackTraceRecoveryTest : TestBase() {
152152
deferred.join()
153153
}
154154

155-
private suspend fun outerMethod(deferred: Deferred<Nothing>, vararg traces: String) {
155+
private suspend fun outerMethod(deferred: Deferred<*>, vararg traces: String) {
156156
withContext(Dispatchers.IO) {
157157
innerMethod(deferred, *traces)
158158
}
159159

160160
assertTrue(true)
161161
}
162162

163-
private suspend fun innerMethod(deferred: Deferred<Nothing>, vararg traces: String) {
163+
private suspend fun innerMethod(deferred: Deferred<*>, vararg traces: String) {
164164
try {
165165
deferred.await()
166+
expectUnreached()
166167
} catch (e: RecoverableTestException) {
167168
verifyStackTrace(e, *traces)
168169
}
169170
}
170171

171172
@Test
172173
fun testCoroutineScope() = runTest {
173-
val deferred = async(NonCancellable, start = CoroutineStart.LAZY) {
174+
val deferred = async<Unit>(NonCancellable, start = CoroutineStart.LAZY) {
174175
throw RecoverableTestException()
175176
}
176177

@@ -203,7 +204,7 @@ class StackTraceRecoveryTest : TestBase() {
203204

204205
@Test
205206
fun testThrowingInitCause() = runTest {
206-
val deferred = async(NonCancellable) {
207+
val deferred = async<Unit>(NonCancellable) {
207208
expect(2)
208209
throw TrickyException()
209210
}
@@ -217,7 +218,7 @@ class StackTraceRecoveryTest : TestBase() {
217218
}
218219
}
219220

220-
private suspend fun outerScopedMethod(deferred: Deferred<Nothing>, vararg traces: String) = coroutineScope {
221+
private suspend fun outerScopedMethod(deferred: Deferred<*>, vararg traces: String) = coroutineScope {
221222
supervisorScope {
222223
innerMethod(deferred, *traces)
223224
assertTrue(true)
@@ -228,7 +229,7 @@ class StackTraceRecoveryTest : TestBase() {
228229
@Test
229230
fun testSelect() = runTest {
230231
expect(1)
231-
val result = kotlin.runCatching { doSelect() }
232+
val result = runCatching { doSelect() }
232233
expect(3)
233234
verifyStackTrace(result.exceptionOrNull()!!,
234235
"kotlinx.coroutines.RecoverableTestException\n" +
@@ -251,4 +252,23 @@ class StackTraceRecoveryTest : TestBase() {
251252
}
252253
}
253254
}
255+
256+
@Test
257+
fun testSelfSuppression() = runTest {
258+
try {
259+
runBlocking {
260+
val job = launch {
261+
coroutineScope<Unit> {
262+
throw RecoverableTestException()
263+
}
264+
}
265+
266+
job.join()
267+
expectUnreached()
268+
}
269+
expectUnreached()
270+
} catch (e: RecoverableTestException) {
271+
checkCycles(e)
272+
}
273+
}
254274
}

0 commit comments

Comments
 (0)