Skip to content

Commit 887df0c

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 bafe7fb commit 887df0c

File tree

17 files changed

+687
-75
lines changed

17 files changed

+687
-75
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
@@ -216,10 +216,15 @@ private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHand
216216
internal open class CancellableContinuationImpl<in T>(
217217
delegate: Continuation<T>,
218218
resumeMode: Int
219-
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable {
219+
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable, CoroutineStackFrame {
220220

221221
public override val context: CoroutineContext = delegate.context
222222

223+
override val callerFrame: CoroutineStackFrame?
224+
get() = delegate as? CoroutineStackFrame
225+
226+
override fun getStackTraceElement(): StackTraceElement? = null
227+
223228
override fun initCancellability() {
224229
initParentJobInternal(delegate.context[Job])
225230
}

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

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

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

@@ -191,7 +193,7 @@ internal fun <T> Continuation<T>.resumeCancellable(value: T) = when (this) {
191193

192194
internal fun <T> Continuation<T>.resumeCancellableWithException(exception: Throwable) = when (this) {
193195
is DispatchedContinuation -> resumeCancellableWithException(exception)
194-
else -> resumeWithException(exception)
196+
else -> resumeWithStackTrace(exception)
195197
}
196198

197199
internal fun <T> Continuation<T>.resumeDirect(value: T) = when (this) {
@@ -200,8 +202,8 @@ internal fun <T> Continuation<T>.resumeDirect(value: T) = when (this) {
200202
}
201203

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

207209
internal interface DispatchedTask<in T> : Runnable {
@@ -230,7 +232,7 @@ internal interface DispatchedTask<in T> : Runnable {
230232
else {
231233
val exception = getExceptionalResult(state)
232234
if (exception != null)
233-
continuation.resumeWithException(exception)
235+
continuation.resumeWithStackTrace(exception)
234236
else
235237
continuation.resume(getSuccessfulResult(state))
236238
}
@@ -272,3 +274,7 @@ internal fun <T> DispatchedTask<T>.resume(delegate: Continuation<T>, useMode: In
272274
delegate.resumeMode(getSuccessfulResult(state), useMode)
273275
}
274276
}
277+
278+
279+
@Suppress("NOTHING_TO_INLINE")
280+
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
}
@@ -1077,7 +1078,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
10771078
val state = this.state
10781079
if (state !is Incomplete) {
10791080
// already complete -- just return result
1080-
if (state is CompletedExceptionally) throw state.cause
1081+
if (state is CompletedExceptionally) { // Slow path to recover stacktrace
1082+
suspendCoroutineUninterceptedOrReturn<Unit> {
1083+
throw recoverStackTrace(state.cause, it)
1084+
}
1085+
}
10811086
return state
10821087

10831088
}

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
@@ -758,7 +758,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
758758
when {
759759
pollResult === ALREADY_SELECTED -> return
760760
pollResult === POLL_FAILED -> {} // retry
761-
pollResult is Closed<*> -> throw pollResult.receiveException
761+
pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException)
762762
else -> {
763763
block.startCoroutineUnintercepted(pollResult as E, select.completion)
764764
return
@@ -797,8 +797,9 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
797797
if (select.trySelect(null))
798798
block.startCoroutineUnintercepted(null, select.completion)
799799
return
800-
} else
801-
throw pollResult.closeCause
800+
} else {
801+
throw recoverStackTrace(pollResult.closeCause)
802+
}
802803
}
803804
else -> {
804805
// selected successfully
@@ -857,7 +858,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
857858

858859
private fun hasNextResult(result: Any?): Boolean {
859860
if (result is Closed<*>) {
860-
if (result.closeCause != null) throw result.receiveException
861+
if (result.closeCause != null) recoverStackTrace(throw result.receiveException)
861862
return false
862863
}
863864
return true
@@ -891,7 +892,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
891892
@Suppress("UNCHECKED_CAST")
892893
override suspend fun next(): E {
893894
val result = this.result
894-
if (result is Closed<*>) throw result.receiveException
895+
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
895896
if (result !== POLL_FAILED) {
896897
this.result = POLL_FAILED
897898
return result as E
@@ -943,10 +944,11 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
943944
}
944945

945946
override fun resumeReceiveClosed(closed: Closed<*>) {
946-
val token = if (closed.closeCause == null)
947+
val token = if (closed.closeCause == null) {
947948
cont.tryResume(false)
948-
else
949-
cont.tryResumeWithException(closed.receiveException)
949+
} else {
950+
cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont))
951+
}
950952
if (token != null) {
951953
iterator.result = closed
952954
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

+8-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package kotlinx.coroutines
66

7+
import kotlinx.coroutines.internal.*
8+
79
public expect open class TestBase constructor() {
810
public val isStressTest: Boolean
911
public val stressTestMultiplier: Int
@@ -21,8 +23,9 @@ public expect open class TestBase constructor() {
2123
)
2224
}
2325

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

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)