Skip to content

Commit 675c30c

Browse files
committed
Basic exception stacktrace recovery mechanism
* Implement CoroutineStackFrame in CancellableContinuationImpl, DispatchedContinuation and ScopeCoroutine * On coroutine resumption try to reflectively instantiate exception instance of the same type, but with augmented stacktrace * Recover stacktrace by walking over CoroutineStackFrame * Recover stacktrace on fast-path exceptions without CoroutineStackFrame walking to provide more context to an exception * Unwrap exceptions when doing aggregation in JobSupport * Add kill-switch to disable stacktrace recovery, introduce method to recover stacktrace on the exceptional fast-path * Add `suspendCoroutineOrReturn` on exceptional fast-path in await in order to provide "real" stacktrace Design rationale: All recovery of *suspended* continuations takes place in Dispatched.kt file, the only place where all calls to "resume*" ends up, so we don't have to remember about stacktrace recovery in every primitive we are implementing. But to provide more accurate stacktraces we *have to* recover it on every fast-path for better debuggability. Fixes #493
1 parent 461473e commit 675c30c

File tree

17 files changed

+686
-76
lines changed

17 files changed

+686
-76
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ public final class kotlinx/coroutines/CancellableContinuation$DefaultImpls {
5050
public static synthetic fun tryResume$default (Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;
5151
}
5252

53-
public class kotlinx/coroutines/CancellableContinuationImpl : java/lang/Runnable, kotlinx/coroutines/CancellableContinuation {
53+
public class kotlinx/coroutines/CancellableContinuationImpl : java/lang/Runnable, kotlin/coroutines/jvm/internal/CoroutineStackFrame, kotlinx/coroutines/CancellableContinuation {
5454
public fun <init> (Lkotlin/coroutines/Continuation;I)V
5555
public fun completeResume (Ljava/lang/Object;)V
56+
public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;
5657
public fun getContext ()Lkotlin/coroutines/CoroutineContext;
58+
public fun getStackTraceElement ()Ljava/lang/StackTraceElement;
5759
public fun getSuccessfulResult (Ljava/lang/Object;)Ljava/lang/Object;
5860
public fun initCancellability ()V
5961
protected fun nameString ()Ljava/lang/String;

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,15 @@ private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHand
218218
internal open class CancellableContinuationImpl<in T>(
219219
delegate: Continuation<T>,
220220
resumeMode: Int
221-
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable {
221+
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable, CoroutineStackFrame {
222222

223223
public override val context: CoroutineContext = delegate.context
224224

225+
override val callerFrame: CoroutineStackFrame?
226+
get() = delegate as? CoroutineStackFrame
227+
228+
override fun getStackTraceElement(): StackTraceElement? = null
229+
225230
override fun initCancellability() {
226231
initParentJobInternal(delegate.context[Job])
227232
}

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

+12-6
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ internal object UndispatchedEventLoop {
8181
internal class DispatchedContinuation<in T>(
8282
@JvmField val dispatcher: CoroutineDispatcher,
8383
@JvmField val continuation: Continuation<T>
84-
) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), Continuation<T> by continuation {
84+
) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation<T> by continuation {
8585
@JvmField
8686
@Suppress("PropertyName")
8787
internal var _state: Any? = UNDEFINED
88+
override val callerFrame: CoroutineStackFrame? = continuation as? CoroutineStackFrame
89+
override fun getStackTraceElement(): StackTraceElement? = null
8890
@JvmField // pre-cached value to avoid ctx.fold on every resumption
8991
internal val countOrElement = threadContextElements(context)
9092

@@ -167,7 +169,7 @@ internal class DispatchedContinuation<in T>(
167169
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
168170
inline fun resumeUndispatchedWithException(exception: Throwable) {
169171
withCoroutineContext(context, countOrElement) {
170-
continuation.resumeWithException(exception)
172+
continuation.resumeWithStackTrace(exception)
171173
}
172174
}
173175

@@ -190,7 +192,7 @@ internal fun <T> Continuation<T>.resumeCancellable(value: T) = when (this) {
190192

191193
internal fun <T> Continuation<T>.resumeCancellableWithException(exception: Throwable) = when (this) {
192194
is DispatchedContinuation -> resumeCancellableWithException(exception)
193-
else -> resumeWithException(exception)
195+
else -> resumeWithStackTrace(exception)
194196
}
195197

196198
internal fun <T> Continuation<T>.resumeDirect(value: T) = when (this) {
@@ -199,8 +201,8 @@ internal fun <T> Continuation<T>.resumeDirect(value: T) = when (this) {
199201
}
200202

201203
internal fun <T> Continuation<T>.resumeDirectWithException(exception: Throwable) = when (this) {
202-
is DispatchedContinuation -> continuation.resumeWithException(exception)
203-
else -> resumeWithException(exception)
204+
is DispatchedContinuation -> continuation.resumeWithStackTrace(exception)
205+
else -> resumeWithStackTrace(exception)
204206
}
205207

206208
internal abstract class DispatchedTask<in T>(
@@ -231,7 +233,7 @@ internal abstract class DispatchedTask<in T>(
231233
else {
232234
val exception = getExceptionalResult(state)
233235
if (exception != null)
234-
continuation.resumeWithException(exception)
236+
continuation.resumeWithStackTrace(exception)
235237
else
236238
continuation.resume(getSuccessfulResult(state))
237239
}
@@ -275,3 +277,7 @@ internal fun <T> DispatchedTask<T>.resume(delegate: Continuation<T>, useMode: In
275277
delegate.resumeMode(getSuccessfulResult(state), useMode)
276278
}
277279
}
280+
281+
282+
@Suppress("NOTHING_TO_INLINE")
283+
private inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this)))

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
242242
val seenExceptions = identitySet<Throwable>(exceptions.size)
243243
var suppressed = false
244244
for (exception in exceptions) {
245-
if (exception !== rootCause && exception !is CancellationException && seenExceptions.add(exception)) {
246-
rootCause.addSuppressedThrowable(exception)
245+
val unwrapped = unwrap(exception)
246+
if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(exception)) {
247+
rootCause.addSuppressedThrowable(unwrapped)
247248
suppressed = true
248249
}
249250
}
@@ -1078,7 +1079,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
10781079
val state = this.state
10791080
if (state !is Incomplete) {
10801081
// already complete -- just return result
1081-
if (state is CompletedExceptionally) throw state.cause
1082+
if (state is CompletedExceptionally) { // Slow path to recover stacktrace
1083+
suspendCoroutineUninterceptedOrReturn<Unit> {
1084+
throw recoverStackTrace(state.cause, it)
1085+
}
1086+
}
10821087
return state
10831088

10841089
}

common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt

+16-14
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
176176
result === OFFER_SUCCESS -> true
177177
// We should check for closed token on offer as well, otherwise offer won't be linearizable
178178
// in the face of concurrent close()
179-
result === OFFER_FAILED -> throw closedForSend?.sendException ?: return false
180-
result is Closed<*> -> throw result.sendException
179+
result === OFFER_FAILED -> throw closedForSend?.sendException?.let { recoverStackTrace(it) } ?: return false
180+
result is Closed<*> -> throw recoverStackTrace(result.sendException)
181181
else -> error("offerInternal returned $result")
182182
}
183183
}
@@ -408,7 +408,7 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
408408
when {
409409
enqueueResult === ALREADY_SELECTED -> return
410410
enqueueResult === ENQUEUE_FAILED -> {} // retry
411-
enqueueResult is Closed<*> -> throw enqueueResult.sendException
411+
enqueueResult is Closed<*> -> throw recoverStackTrace(enqueueResult.sendException)
412412
else -> error("performAtomicIfNotSelected(TryEnqueueSendDesc) returned $enqueueResult")
413413
}
414414
} else {
@@ -420,7 +420,7 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
420420
block.startCoroutineUnintercepted(receiver = this, completion = select.completion)
421421
return
422422
}
423-
offerResult is Closed<*> -> throw offerResult.sendException
423+
offerResult is Closed<*> -> throw recoverStackTrace(offerResult.sendException)
424424
else -> error("offerSelectInternal returned $offerResult")
425425
}
426426
}
@@ -574,7 +574,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
574574

575575
@Suppress("UNCHECKED_CAST")
576576
private fun receiveResult(result: Any?): E {
577-
if (result is Closed<*>) throw result.receiveException
577+
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
578578
return result as E
579579
}
580580

@@ -620,7 +620,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
620620
@Suppress("UNCHECKED_CAST")
621621
private fun receiveOrNullResult(result: Any?): E? {
622622
if (result is Closed<*>) {
623-
if (result.closeCause != null) throw result.closeCause
623+
if (result.closeCause != null) throw recoverStackTrace(result.closeCause)
624624
return null
625625
}
626626
return result as E
@@ -759,7 +759,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
759759
when {
760760
pollResult === ALREADY_SELECTED -> return
761761
pollResult === POLL_FAILED -> {} // retry
762-
pollResult is Closed<*> -> throw pollResult.receiveException
762+
pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException)
763763
else -> {
764764
block.startCoroutineUnintercepted(pollResult as E, select.completion)
765765
return
@@ -798,8 +798,9 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
798798
if (select.trySelect(null))
799799
block.startCoroutineUnintercepted(null, select.completion)
800800
return
801-
} else
802-
throw pollResult.closeCause
801+
} else {
802+
throw recoverStackTrace(pollResult.closeCause)
803+
}
803804
}
804805
else -> {
805806
// selected successfully
@@ -858,7 +859,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
858859

859860
private fun hasNextResult(result: Any?): Boolean {
860861
if (result is Closed<*>) {
861-
if (result.closeCause != null) throw result.receiveException
862+
if (result.closeCause != null) recoverStackTrace(throw result.receiveException)
862863
return false
863864
}
864865
return true
@@ -892,7 +893,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
892893
@Suppress("UNCHECKED_CAST")
893894
override suspend fun next(): E {
894895
val result = this.result
895-
if (result is Closed<*>) throw result.receiveException
896+
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
896897
if (result !== POLL_FAILED) {
897898
this.result = POLL_FAILED
898899
return result as E
@@ -944,10 +945,11 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
944945
}
945946

946947
override fun resumeReceiveClosed(closed: Closed<*>) {
947-
val token = if (closed.closeCause == null)
948+
val token = if (closed.closeCause == null) {
948949
cont.tryResume(false)
949-
else
950-
cont.tryResumeWithException(closed.receiveException)
950+
} else {
951+
cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont))
952+
}
951953
if (token != null) {
952954
iterator.result = closed
953955
cont.completeResume(token)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.internal
6+
7+
import kotlin.coroutines.*
8+
9+
/**
10+
* Tries to recover stacktrace for given [exception] and [continuation].
11+
* Stacktrace recovery tries to restore [continuation] stack frames using its debug metadata with [CoroutineStackFrame] API
12+
* and then reflectively instantiate exception of given type with original exception as a cause and
13+
* sets new stacktrace for wrapping exception.
14+
* Some frames may be missing due to tail-call elimination.
15+
*
16+
* Works only on JVM with enabled debug-mode.
17+
*/
18+
internal expect fun <E: Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E
19+
20+
/**
21+
* Tries to recover stacktrace for given [exception]. Used in non-suspendable points of awaiting.
22+
* Stacktrace recovery tries to instantiate exception of given type with original exception as a cause.
23+
* Wrapping exception will have proper stacktrace as it's instantiated in the right context.
24+
*
25+
* Works only on JVM with enabled debug-mode.
26+
*/
27+
internal expect fun <E: Throwable> recoverStackTrace(exception: E): E
28+
29+
// Name conflict with recoverStackTrace
30+
@Suppress("NOTHING_TO_INLINE")
31+
internal expect suspend inline fun recoverAndThrow(exception: Throwable): Nothing
32+
33+
/**
34+
* The opposite of [recoverStackTrace].
35+
* It is guaranteed that `unwrap(recoverStackTrace(e)) === e`
36+
*/
37+
internal expect fun <E: Throwable> unwrap(exception: E): E
38+
39+
expect class StackTraceElement
40+
41+
internal expect interface CoroutineStackFrame {
42+
public val callerFrame: CoroutineStackFrame?
43+
public fun getStackTraceElement(): StackTraceElement?
44+
}
45+
46+
/**
47+
* Marker that indicates that stacktrace of the exception should not be recovered.
48+
* Currently internal, but may become public in the future
49+
*/
50+
internal interface NonRecoverableThrowable

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import kotlin.jvm.*
1414
internal open class ScopeCoroutine<in T>(
1515
context: CoroutineContext,
1616
@JvmField val uCont: Continuation<T> // unintercepted continuation
17-
) : AbstractCoroutine<T>(context, true) {
17+
) : AbstractCoroutine<T>(context, true), CoroutineStackFrame {
18+
final override val callerFrame: CoroutineStackFrame? get() = uCont as CoroutineStackFrame?
19+
final override fun getStackTraceElement(): StackTraceElement? = null
1820
override val defaultResumeMode: Int get() = MODE_DIRECT
1921

2022
@Suppress("UNCHECKED_CAST")

common/kotlinx-coroutines-core-common/test/TestBase.common.kt

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

77
import kotlin.coroutines.*
8+
import kotlinx.coroutines.internal.*
89

910
public expect open class TestBase constructor() {
1011
public val isStressTest: Boolean
@@ -23,13 +24,13 @@ public expect open class TestBase constructor() {
2324
)
2425
}
2526

26-
public class TestException(message: String? = null) : Throwable(message)
27-
public class TestException1(message: String? = null) : Throwable(message)
28-
public class TestException2(message: String? = null) : Throwable(message)
29-
public class TestException3(message: String? = null) : Throwable(message)
30-
public class TestRuntimeException(message: String? = null) : RuntimeException(message)
27+
public class TestException(message: String? = null) : Throwable(message), NonRecoverableThrowable
28+
public class TestException1(message: String? = null) : Throwable(message), NonRecoverableThrowable
29+
public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable
30+
public class TestException3(message: String? = null) : Throwable(message), NonRecoverableThrowable
31+
public class TestRuntimeException(message: String? = null) : RuntimeException(message), NonRecoverableThrowable
32+
public class RecoverableTestException(message: String? = null) : Throwable(message)
3133

32-
// Wrap context to avoid fast-paths on dispatcher comparison
3334
public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
3435
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
3536
return object : CoroutineDispatcher() {

core/kotlinx-coroutines-core/src/Debug.kt

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ import kotlinx.coroutines.internal.*
1212
*/
1313
public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug"
1414

15+
/**
16+
* Name of the boolean property that controls stacktrace recovery (enabled by default) on JVM.
17+
* Stacktrace recovery is enabled if both debug and stacktrace recovery modes are enabled.
18+
*
19+
* Stacktrace recovery mode wraps every exception into the exception of the same type with original exception
20+
* as cause, but with stacktrace of the current coroutine.
21+
* Exception is instantiated using reflection by using no-arg, cause or cause and message constructor.
22+
* Stacktrace is not recovered if exception is an instance of [CancellationException] or [NonRecoverableThrowable].
23+
*
24+
* This mechanism is currently supported for channels, [async], [launch], [coroutineScope], [supervisorScope]
25+
* and [withContext] builders.
26+
*/
27+
internal const val STACKTRACE_RECOVERY_PROPERTY_NAME = "kotlinx.coroutines.stacktrace.recovery"
28+
1529
/**
1630
* Automatic debug configuration value for [DEBUG_PROPERTY_NAME]. See [newCoroutineContext][CoroutineScope.newCoroutineContext].
1731
*/
@@ -36,6 +50,8 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value ->
3650
}
3751
}
3852

53+
internal val RECOVER_STACKTRACE = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
54+
3955
// internal debugging tools
4056

4157
internal actual val Any.hexAddress: String

0 commit comments

Comments
 (0)