Skip to content

Commit 4380dd9

Browse files
committed
Basic exception stacktrace recovery mechanism in JobSupport (~all builders) and channels
* Implement CoroutineStackFrame in CancellableContinuationImpl, DispatchedContinuation and withContext * On coroutine resumption try to reflectively instantiate exception instance of the same type, but with augmented stacktrace * Augment stacktrace by walking over CoroutineStackFrame * Augment stacktrace on fast-path exceptions without CoroutineStackFrame walking to provide more context to an exception Fixes #493
1 parent 28b073a commit 4380dd9

File tree

13 files changed

+431
-30
lines changed

13 files changed

+431
-30
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ private class RunCompletion<in T>(
248248
override val context: CoroutineContext,
249249
delegate: Continuation<T>,
250250
resumeMode: Int
251-
) : AbstractContinuation<T>(delegate, resumeMode) {
252-
251+
) : AbstractContinuation<T>(delegate, resumeMode) , CoroutineStackFrame {
252+
override val callerFrame: CoroutineStackFrame? = delegate as CoroutineStackFrame?
253+
override fun getStackTraceElement(): StackTraceElement? = null
253254
override val useCancellingState: Boolean get() = true
254255
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,15 @@ private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHand
261261
internal class CancellableContinuationImpl<in T>(
262262
delegate: Continuation<T>,
263263
resumeMode: Int
264-
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable {
264+
) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable, CoroutineStackFrame {
265265

266266
public override val context: CoroutineContext = delegate.context
267267

268+
override val callerFrame: CoroutineStackFrame?
269+
get() = delegate as? CoroutineStackFrame
270+
271+
override fun getStackTraceElement(): StackTraceElement? = null
272+
268273
override fun initCancellability() {
269274
initParentJobInternal(delegate.context[Job])
270275
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ private val UNDEFINED = Symbol("UNDEFINED")
1313
internal class DispatchedContinuation<in T>(
1414
@JvmField val dispatcher: CoroutineDispatcher,
1515
@JvmField val continuation: Continuation<T>
16-
) : Continuation<T> by continuation, DispatchedTask<T> {
16+
) : Continuation<T> by continuation, DispatchedTask<T>, CoroutineStackFrame {
1717
private var _state: Any? = UNDEFINED
1818
public override var resumeMode: Int = 0
19+
override val callerFrame: CoroutineStackFrame? = continuation as? CoroutineStackFrame
20+
override fun getStackTraceElement(): StackTraceElement? = null
1921

2022
override fun takeState(): Any? {
2123
val state = _state
@@ -44,8 +46,9 @@ internal class DispatchedContinuation<in T>(
4446
_state = value
4547
resumeMode = MODE_CANCELLABLE
4648
dispatcher.dispatch(context, this)
47-
} else
49+
} else {
4850
resumeUndispatched(value)
51+
}
4952
}
5053

5154
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
956956
val state = this.state
957957
if (state !is Incomplete) {
958958
// already complete -- just return result
959-
if (state is CompletedExceptionally) throw state.cause
959+
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause)
960960
return state
961961

962962
}
@@ -971,7 +971,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
971971
val state = this.state
972972
check(state !is Incomplete)
973973
if (state is CompletedExceptionally)
974-
cont.resumeWithException(state.cause)
974+
cont.resumeWithStackTrace(state.cause)
975975
else
976976
cont.resume(state)
977977
})

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

+18-18
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ public abstract class AbstractSendChannel<E> : SendChannel<E> {
174174
result === OFFER_SUCCESS -> true
175175
// We should check for closed token on offer as well, otherwise offer won't be linearizable
176176
// in the face of concurrent close()
177-
result === OFFER_FAILED -> throw closedForSend?.sendException ?: return false
178-
result is Closed<*> -> throw result.sendException
177+
result === OFFER_FAILED -> throw closedForSend?.sendException?.also { recoverStackTrace(it) } ?: return false
178+
result is Closed<*> -> throw recoverStackTrace(result.sendException)
179179
else -> error("offerInternal returned $result")
180180
}
181181
}
@@ -192,7 +192,7 @@ public abstract class AbstractSendChannel<E> : SendChannel<E> {
192192
}
193193
is Closed<*> -> {
194194
helpClose(enqueueResult)
195-
cont.resumeWithException(enqueueResult.sendException)
195+
cont.resumeWithStackTrace(enqueueResult.sendException)
196196
return@sc
197197
}
198198
}
@@ -206,7 +206,7 @@ public abstract class AbstractSendChannel<E> : SendChannel<E> {
206206
offerResult === OFFER_FAILED -> continue@loop
207207
offerResult is Closed<*> -> {
208208
helpClose(offerResult)
209-
cont.resumeWithException(offerResult.sendException)
209+
cont.resumeWithStackTrace(offerResult.sendException)
210210
return@sc
211211
}
212212
else -> error("offerInternal returned $offerResult")
@@ -405,7 +405,7 @@ public abstract class AbstractSendChannel<E> : SendChannel<E> {
405405
when {
406406
enqueueResult === ALREADY_SELECTED -> return
407407
enqueueResult === ENQUEUE_FAILED -> {} // retry
408-
enqueueResult is Closed<*> -> throw enqueueResult.sendException
408+
enqueueResult is Closed<*> -> throw recoverStackTrace(enqueueResult.sendException)
409409
else -> error("performAtomicIfNotSelected(TryEnqueueSendDesc) returned $enqueueResult")
410410
}
411411
} else {
@@ -417,7 +417,7 @@ public abstract class AbstractSendChannel<E> : SendChannel<E> {
417417
block.startCoroutineUnintercepted(receiver = this, completion = select.completion)
418418
return
419419
}
420-
offerResult is Closed<*> -> throw offerResult.sendException
420+
offerResult is Closed<*> -> throw recoverStackTrace(offerResult.sendException)
421421
else -> error("offerSelectInternal returned $offerResult")
422422
}
423423
}
@@ -571,7 +571,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
571571

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

@@ -587,7 +587,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
587587
// hm... something is not right. try to poll
588588
val result = pollInternal()
589589
if (result is Closed<*>) {
590-
cont.resumeWithException(result.receiveException)
590+
cont.resumeWithStackTrace(result.receiveException)
591591
return@sc
592592
}
593593
if (result !== POLL_FAILED) {
@@ -617,7 +617,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
617617
@Suppress("UNCHECKED_CAST")
618618
private fun receiveOrNullResult(result: Any?): E? {
619619
if (result is Closed<*>) {
620-
if (result.closeCause != null) throw result.closeCause
620+
if (result.closeCause != null) throw recoverStackTrace(result.closeCause)
621621
return null
622622
}
623623
return result as E
@@ -638,7 +638,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
638638
if (result.closeCause == null)
639639
cont.resume(null)
640640
else
641-
cont.resumeWithException(result.closeCause)
641+
cont.resumeWithStackTrace(result.closeCause)
642642
return@sc
643643
}
644644
if (result !== POLL_FAILED) {
@@ -751,7 +751,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
751751
when {
752752
pollResult === ALREADY_SELECTED -> return
753753
pollResult === POLL_FAILED -> {} // retry
754-
pollResult is Closed<*> -> throw pollResult.receiveException
754+
pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException)
755755
else -> {
756756
block.startCoroutineUnintercepted(pollResult as E, select.completion)
757757
return
@@ -791,7 +791,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
791791
block.startCoroutineUnintercepted(null, select.completion)
792792
return
793793
} else
794-
throw pollResult.closeCause
794+
throw recoverStackTrace(pollResult.closeCause)
795795
}
796796
else -> {
797797
// selected successfully
@@ -850,7 +850,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
850850

851851
private fun hasNextResult(result: Any?): Boolean {
852852
if (result is Closed<*>) {
853-
if (result.closeCause != null) throw result.receiveException
853+
if (result.closeCause != null) recoverStackTrace(throw result.receiveException)
854854
return false
855855
}
856856
return true
@@ -871,7 +871,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
871871
if (result.closeCause == null)
872872
cont.resume(false)
873873
else
874-
cont.resumeWithException(result.receiveException)
874+
cont.resumeWithStackTrace(result.receiveException)
875875
return@sc
876876
}
877877
if (result !== POLL_FAILED) {
@@ -884,7 +884,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
884884
@Suppress("UNCHECKED_CAST")
885885
override suspend fun next(): E {
886886
val result = this.result
887-
if (result is Closed<*>) throw result.receiveException
887+
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
888888
if (result !== POLL_FAILED) {
889889
this.result = POLL_FAILED
890890
return result as E
@@ -904,7 +904,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
904904
if (closed.closeCause == null && nullOnClose)
905905
cont.resume(null)
906906
else
907-
cont.resumeWithException(closed.receiveException)
907+
cont.resumeWithStackTrace(closed.receiveException)
908908
}
909909
override fun toString(): String = "ReceiveElement[$cont,nullOnClose=$nullOnClose]"
910910
}
@@ -939,7 +939,7 @@ public abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E>
939939
val token = if (closed.closeCause == null)
940940
cont.tryResume(false)
941941
else
942-
cont.tryResumeWithException(closed.receiveException)
942+
cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont))
943943
if (token != null) {
944944
iterator.result = closed
945945
cont.completeResume(token)
@@ -1052,7 +1052,7 @@ public class SendElement(
10521052
) : LockFreeLinkedListNode(), Send {
10531053
override fun tryResumeSend(idempotent: Any?): Any? = cont.tryResume(Unit, idempotent)
10541054
override fun completeResumeSend(token: Any) = cont.completeResume(token)
1055-
override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException)
1055+
override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithStackTrace(closed.sendException)
10561056
override fun toString(): String = "SendElement($pollResult)[$cont]"
10571057
}
10581058

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
@Suppress("NOTHING_TO_INLINE")
30+
internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this)))
31+
32+
expect class StackTraceElement
33+
34+
internal expect interface CoroutineStackFrame {
35+
public val callerFrame: CoroutineStackFrame?
36+
public fun getStackTraceElement(): StackTraceElement?
37+
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ public expect open class TestBase constructor() {
2020
block: suspend CoroutineScope.() -> Unit
2121
)
2222
}
23+
24+
// Specific exception which stacktrace cannot be recovered by our machinery
25+
class TestException(message: String) : Exception(message)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 kotlinx.coroutines.*
8+
import java.util.*
9+
import kotlin.coroutines.*
10+
11+
internal actual fun <E : Throwable> recoverStackTrace(exception: E): E {
12+
if (!DEBUG || exception is CancellationException) {
13+
return exception
14+
}
15+
16+
val copy = tryCopyException(exception) ?: return exception
17+
return copy.sanitizeStackTrace()
18+
}
19+
20+
private fun <E : Throwable> E.sanitizeStackTrace(): E {
21+
val size = stackTrace.size
22+
23+
var lastIntrinsic = -1
24+
for (i in 0 until size) {
25+
val name = stackTrace[i].className
26+
if ("kotlinx.coroutines.internal.ExceptionsKt" == name) {
27+
lastIntrinsic = i
28+
}
29+
}
30+
31+
val startIndex = lastIntrinsic + 1
32+
val trace = Array(size - lastIntrinsic) {
33+
if (it == 0) {
34+
artificialFrame("Current coroutine stacktrace")
35+
} else {
36+
stackTrace[startIndex + it - 1]
37+
}
38+
}
39+
40+
stackTrace = trace
41+
return this
42+
}
43+
44+
internal actual fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
45+
if (!DEBUG || exception is CancellationException || continuation !is CoroutineStackFrame) {
46+
return exception
47+
}
48+
49+
val stacktrace = createStackTrace(continuation)
50+
if (stacktrace.isEmpty()) return exception
51+
val newException = tryCopyException(exception) ?: return exception
52+
stacktrace.add(0, artificialFrame("Current coroutine stacktrace"))
53+
newException.stackTrace = stacktrace.toTypedArray()
54+
return newException
55+
}
56+
57+
@Suppress("UNCHECKED_CAST")
58+
private fun <E : Throwable> tryCopyException(exception: E): E? {
59+
/*
60+
* Try to reflectively find constructor(), constructor(message, cause) or constructor(cause).
61+
* First thing which comes in mind is why we should do it at all? Exceptions are share between coroutines,
62+
* so we can't safely modify their stacktraces, thus making copy is our only hope
63+
*/
64+
var newException: E? = null
65+
try {
66+
for (constructor in exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size }) {
67+
val parameters = constructor.parameterTypes
68+
if (parameters.size == 2 && parameters[0] == String::class.java && parameters[1] == Throwable::class.java) {
69+
newException = constructor.newInstance(exception.message, exception) as E
70+
} else if (parameters.size == 1 && parameters[0] == Throwable::class.java) {
71+
newException = constructor.newInstance(exception) as E
72+
} else if (parameters.isEmpty()) {
73+
newException = (constructor.newInstance() as E).also { it.initCause(exception) }
74+
}
75+
76+
if (newException != null) {
77+
break
78+
}
79+
}
80+
} catch (e: Exception) {
81+
// Do nothing
82+
}
83+
return newException
84+
}
85+
86+
private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList<StackTraceElement> {
87+
val stack = ArrayList<StackTraceElement>()
88+
continuation.getStackTraceElement()?.let { stack.add(it) }
89+
90+
var last = continuation
91+
while (true) {
92+
last = (last as? CoroutineStackFrame)?.callerFrame ?: break
93+
last.getStackTraceElement()?.let { stack.add(it) }
94+
}
95+
return stack
96+
}
97+
98+
99+
public fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
100+
101+
@Suppress("ACTUAL_WITHOUT_EXPECT")
102+
actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame
103+
104+
actual typealias StackTraceElement = java.lang.StackTraceElement

0 commit comments

Comments
 (0)