Skip to content

Commit a0cd925

Browse files
committed
~: rename Exception.kt to StackTraceRecovery.kt. Recover exception in scoped coroutine and its fast-path, skip common prefix during exception merging
1 parent c99c93a commit a0cd925

File tree

10 files changed

+145
-23
lines changed

10 files changed

+145
-23
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
243243
var suppressed = false
244244
for (exception in exceptions) {
245245
val unwrapped = unwrap(exception)
246-
if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(exception)) {
246+
if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(unwrapped)) {
247247
rootCause.addSuppressedThrowable(unwrapped)
248248
suppressed = true
249249
}

common/kotlinx-coroutines-core-common/src/internal/Scopes.kt

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ internal open class ScopeCoroutine<in T>(
2121

2222
@Suppress("UNCHECKED_CAST")
2323
internal override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) {
24-
if (state is CompletedExceptionally)
25-
uCont.resumeUninterceptedWithExceptionMode(state.cause, mode)
26-
else
24+
if (state is CompletedExceptionally) {
25+
val exception = if (mode == MODE_IGNORE) state.cause else recoverStackTrace(state.cause, uCont)
26+
uCont.resumeUninterceptedWithExceptionMode(exception, mode)
27+
} else {
2728
uCont.resumeUninterceptedMode(state as T, mode)
29+
}
2830
}
2931
}
3032

33+
internal fun AbstractCoroutine<*>.tryRecover(exception: Throwable): Throwable {
34+
val cont = (this as? ScopeCoroutine<*>)?.uCont ?: return exception
35+
return recoverStackTrace(exception, cont)
36+
}
37+
3138
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
3239
override val coroutineContext: CoroutineContext = context
3340
}

common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.coroutines.intrinsics
66

77
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.internal.*
89
import kotlin.coroutines.*
910
import kotlin.coroutines.intrinsics.*
1011

@@ -126,8 +127,8 @@ private inline fun <T> AbstractCoroutine<T>.undispatchedResult(
126127
val state = state
127128
if (state is CompletedExceptionally) {
128129
when {
129-
shouldThrow(state.cause) -> throw state.cause
130-
result is CompletedExceptionally -> throw result.cause
130+
shouldThrow(state.cause) -> throw tryRecover(state.cause)
131+
result is CompletedExceptionally -> throw tryRecover(result.cause)
131132
else -> result
132133
}
133134
} else {

core/kotlinx-coroutines-core/src/internal/Exceptions.kt renamed to core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt

+18-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ private fun <E : Throwable> E.sanitizeStackTrace(): E {
2424
val stackTrace = stackTrace
2525
val size = stackTrace.size
2626

27-
val lastIntrinsic = stackTrace.frameIndex("kotlinx.coroutines.internal.ExceptionsKt")
27+
val lastIntrinsic = stackTrace.frameIndex("kotlinx.coroutines.internal.StackTraceRecoveryKt")
2828
val startIndex = lastIntrinsic + 1
2929
val endIndex = stackTrace.frameIndex("kotlin.coroutines.jvm.internal.BaseContinuationImpl")
3030
val adjustment = if (endIndex == -1) 0 else size - endIndex
3131
val trace = Array(size - lastIntrinsic - adjustment) {
3232
if (it == 0) {
33-
artificialFrame("Current coroutine stacktrace")
33+
artificialFrame("Coroutine boundary")
3434
} else {
3535
stackTrace[startIndex + it - 1]
3636
}
@@ -82,7 +82,7 @@ private fun <E : Throwable> recoverFromStackFrame(exception: E, continuation: Co
8282
* caused by "IllegalStateException" (original one)
8383
*/
8484
// TODO optimizable allocations and passes
85-
stacktrace.addFirst(artificialFrame("Current coroutine stacktrace"))
85+
stacktrace.addFirst(artificialFrame("Coroutine boundary"))
8686
val copied = meaningfulActualStackTrace(cause)
8787
newException.stackTrace = (copied + stacktrace).toTypedArray()
8888
return newException
@@ -105,8 +105,14 @@ private fun <E : Throwable> E.causeAndStacktrace(): Pair<E, Array<StackTraceElem
105105
}
106106

107107
private fun mergeRecoveredTraces(recoveredStacktrace: Array<StackTraceElement>, result: ArrayDeque<StackTraceElement>) {
108+
// Merge two stacktraces and trim common prefix
108109
val startIndex = recoveredStacktrace.indexOfFirst { it.isArtificial() } + 1
109-
for (i in (recoveredStacktrace.size - 1) downTo startIndex) {
110+
val lastFrameIndex = recoveredStacktrace.size - 1
111+
for (i in lastFrameIndex downTo startIndex) {
112+
val element = recoveredStacktrace[i]
113+
if (element.elementWiseEquals(result.last)) {
114+
result.removeLast()
115+
}
110116
result.addFirst(recoveredStacktrace[i])
111117
}
112118
}
@@ -184,6 +190,14 @@ internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\
184190
internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b")
185191
private fun Array<StackTraceElement>.frameIndex(methodName: String) = indexOfFirst { methodName == it.className }
186192

193+
private fun StackTraceElement.elementWiseEquals(e: StackTraceElement): Boolean {
194+
/*
195+
* By default, STE.equals compares class loads as well, which may lead to an interesting duplicates
196+
* in stack traces.
197+
*/
198+
return lineNumber == e.lineNumber && methodName == e.methodName && fileName == e.fileName
199+
}
200+
187201
@Suppress("ACTUAL_WITHOUT_EXPECT")
188202
actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame
189203

core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt

+8-7
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,21 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() {
3838
} catch (e: Exception) {
3939
verifyStackTrace(e,
4040
"kotlinx.coroutines.RecoverableTestException\n" +
41-
"\t(Current coroutine stacktrace)\n" +
41+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" +
42+
"\t(Coroutine boundary)\n" +
4243
"\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" +
4344
"\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" +
4445
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" +
4546
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" +
4647
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invoke(StackTraceRecoveryNestedChannelsTest.kt)\n" +
47-
"\tat kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:84)\n" +
48+
"\tat kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:85)\n" +
4849
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:146)\n" +
4950
"\tat kotlinx.coroutines.BuildersKt.withContext(Unknown Source)\n" +
5051
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendWithContext(StackTraceRecoveryNestedChannelsTest.kt:18)\n" +
51-
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:36)\n" +
52+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:37)\n" +
5253
"Caused by: kotlinx.coroutines.RecoverableTestException\n" +
5354
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:33)\n" +
54-
"\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)")
55+
"\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n")
5556
}
5657
}
5758

@@ -65,7 +66,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() {
6566
verifyStackTrace(e,
6667
"kotlinx.coroutines.RecoverableTestException\n" +
6768
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:59)\n" +
68-
"\t(Current coroutine stacktrace)\n" +
69+
"\t(Coroutine boundary)\n" +
6970
"\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" +
7071
"\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" +
7172
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" +
@@ -87,7 +88,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() {
8788
verifyStackTrace(e,
8889
"kotlinx.coroutines.RecoverableTestException\n" +
8990
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:81)\n" +
90-
"\t(Current coroutine stacktrace)\n" +
91+
"\t(Coroutine boundary)\n" +
9192
"\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" +
9293
"\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" +
9394
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" +
@@ -111,7 +112,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() {
111112
verifyStackTrace(e,
112113
"kotlinx.coroutines.RecoverableTestException\n" +
113114
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" +
114-
"\t(Current coroutine stacktrace)\n" +
115+
"\t(Coroutine boundary)\n" +
115116
"\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" +
116117
"\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" +
117118
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" +
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package kotlinx.coroutines.exceptions
2+
3+
import kotlinx.coroutines.*
4+
import org.junit.*
5+
import kotlin.coroutines.*
6+
7+
class StackTraceRecoveryNestedScopesTest : TestBase() {
8+
9+
private val TEST_MACROS = "TEST_NAME"
10+
11+
private val expectedTrace = "kotlinx.coroutines.RecoverableTestException\n" +
12+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:9)\n" +
13+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" +
14+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:12)\n" +
15+
"\t(Coroutine boundary)\n" +
16+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callWithTimeout\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:23)\n" +
17+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callCoroutineScope\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:29)\n" +
18+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$$TEST_MACROS\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:36)\n" +
19+
"Caused by: kotlinx.coroutines.RecoverableTestException\n" +
20+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:9)\n" +
21+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" +
22+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:12)\n" +
23+
"\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)"
24+
25+
private fun failure(): String = throw RecoverableTestException()
26+
27+
private fun CoroutineScope.createFailingAsync() = async {
28+
failure()
29+
}
30+
31+
private suspend fun callWithContext(doYield: Boolean) = withContext(wrapperDispatcher(coroutineContext)) {
32+
if (doYield) yield()
33+
createFailingAsync().await()
34+
yield()
35+
}
36+
37+
private suspend fun callWithTimeout(doYield: Boolean) = withTimeout(Long.MAX_VALUE) {
38+
if (doYield) yield()
39+
callWithContext(doYield)
40+
yield()
41+
}
42+
43+
private suspend fun callCoroutineScope(doYield: Boolean) = coroutineScope {
44+
if (doYield) yield()
45+
callWithTimeout(doYield)
46+
yield()
47+
}
48+
49+
@Test
50+
fun testNestedScopes() = runTest {
51+
try {
52+
callCoroutineScope(false)
53+
} catch (e: Exception) {
54+
verifyStackTrace(e, expectedTrace.replace(TEST_MACROS, "testNestedScopes"))
55+
}
56+
}
57+
58+
@Test
59+
fun testNestedScopesYield() = runTest {
60+
try {
61+
callCoroutineScope(true)
62+
} catch (e: Exception) {
63+
verifyStackTrace(e, expectedTrace.replace(TEST_MACROS, "testNestedScopesYield"))
64+
}
65+
}
66+
67+
@Test
68+
fun testAwaitNestedScopes() = runTest {
69+
val deferred = async(NonCancellable) {
70+
callCoroutineScope(false)
71+
}
72+
73+
verifyAwait(deferred)
74+
}
75+
76+
private suspend fun verifyAwait(deferred: Deferred<Unit>) {
77+
try {
78+
deferred.await()
79+
} catch (e: Exception) {
80+
verifyStackTrace(e,
81+
"kotlinx.coroutines.RecoverableTestException\n" +
82+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:23)\n" +
83+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" +
84+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:26)\n" +
85+
"\t(Coroutine boundary)\n" +
86+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callWithTimeout\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:37)\n" +
87+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callCoroutineScope\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:43)\n" +
88+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$testAwaitNestedScopes\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:68)\n" +
89+
"\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
90+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.verifyAwait(StackTraceRecoveryNestedScopesTest.kt:76)\n" +
91+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$testAwaitNestedScopes\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:71)\n" +
92+
"Caused by: kotlinx.coroutines.RecoverableTestException\n" +
93+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:23)\n" +
94+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" +
95+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:26)\n" +
96+
"\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)")
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)