Skip to content

Commit 29493e1

Browse files
authored
Merge pull request #792 from Kotlin/stacktrace-recovery
Stacktrace recovery Fixes #493 Fixes #74
2 parents b733f7d + cd162d3 commit 29493e1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2721
-139
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ GlobalScope.launch {
3232
* [core](core/README.md) — Kotlin/JVM implementation of common coroutines with additional features:
3333
* `Dispatchers.IO` dispatcher for blocking coroutines;
3434
* `Executor.asCoroutineDispatcher()` extension, custom thread pools, and more.
35+
* [debug](core/README.md) — debug utilities for coroutines.
36+
* `DebugProbes` API to probe, keep track of, print and dump active coroutines.
3537
* [js](js/README.md) — Kotlin/JS implementation of common coroutines with `Promise` support.
3638
* [native](native/README.md) — Kotlin/Native implementation of common coroutines with `runBlocking` single-threaded event loop.
3739
* [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries:

RELEASE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ To release new `<version>` of `kotlinx-coroutines`:
1212
`git merge origin/master`
1313

1414
4. Search & replace `<old-version>` with `<version>` across the project files. Should replace in:
15-
* [`README.md`](README.md)
15+
* [`README.md`](README.md) (native, core, test, debug, modules)
1616
* [`coroutines-guide.md`](docs/coroutines-guide.md)
1717
* [`gradle.properties`](gradle.properties)
1818
* [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties)

binary-compatibility-validator/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
1414

1515
testArtifacts project(':kotlinx-coroutines-core')
16+
testArtifacts project(':kotlinx-coroutines-debug')
1617

1718
testArtifacts project(':kotlinx-coroutines-reactive')
1819
testArtifacts project(':kotlinx-coroutines-reactor')

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

+4-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;
@@ -349,6 +351,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin
349351
public fun plus (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job;
350352
public final fun registerSelectClause0 (Lkotlinx/coroutines/selects/SelectInstance;Lkotlin/jvm/functions/Function1;)V
351353
public final fun start ()Z
354+
public final fun toDebugString ()Ljava/lang/String;
352355
public fun toString ()Ljava/lang/String;
353356
}
354357

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
public final class kotlinx/coroutines/debug/CoroutineState {
2+
public final fun component1 ()Lkotlin/coroutines/Continuation;
3+
public final fun copy (Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineState;
4+
public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState;
5+
public fun equals (Ljava/lang/Object;)Z
6+
public final fun getContinuation ()Lkotlin/coroutines/Continuation;
7+
public final fun getCreationStackTrace ()Ljava/util/List;
8+
public final fun getJob ()Lkotlinx/coroutines/Job;
9+
public final fun getJobOrNull ()Lkotlinx/coroutines/Job;
10+
public final fun getState ()Lkotlinx/coroutines/debug/State;
11+
public fun hashCode ()I
12+
public final fun lastObservedStackTrace ()Ljava/util/List;
13+
public fun toString ()Ljava/lang/String;
14+
}
15+
16+
public final class kotlinx/coroutines/debug/DebugProbes {
17+
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
18+
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
19+
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
20+
public final fun dumpCoroutinesState ()Ljava/util/List;
21+
public final fun getSanitizeStackTraces ()Z
22+
public final fun hierarchyToString (Lkotlinx/coroutines/Job;)Ljava/lang/String;
23+
public final fun install ()V
24+
public final fun printHierarchy (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V
25+
public static synthetic fun printHierarchy$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V
26+
public final fun setSanitizeStackTraces (Z)V
27+
public final fun uninstall ()V
28+
public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V
29+
}
30+
31+
public final class kotlinx/coroutines/debug/State : java/lang/Enum {
32+
public static final field CREATED Lkotlinx/coroutines/debug/State;
33+
public static final field RUNNING Lkotlinx/coroutines/debug/State;
34+
public static final field SUSPENDED Lkotlinx/coroutines/debug/State;
35+
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State;
36+
public static fun values ()[Lkotlinx/coroutines/debug/State;
37+
}
38+

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ buildscript {
4040
classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version"
4141

4242
// JMH plugins
43-
classpath "com.github.jengelman.gradle.plugins:shadow:2.0.2"
43+
classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2"
4444
classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
4545
classpath "net.ltgt.gradle:gradle-apt-plugin:0.10"
4646
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ internal abstract class AbstractContinuation<in T>(
134134
if (trySuspend()) return COROUTINE_SUSPENDED
135135
// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
136136
val state = this.state
137-
if (state is CompletedExceptionally) throw state.cause
137+
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
138138
return getSuccessfulResult(state)
139139
}
140140

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ internal object UndispatchedEventLoop {
8282
internal class DispatchedContinuation<in T>(
8383
@JvmField val dispatcher: CoroutineDispatcher,
8484
@JvmField val continuation: Continuation<T>
85-
) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), Continuation<T> by continuation {
85+
) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation<T> by continuation {
8686
@JvmField
8787
@Suppress("PropertyName")
8888
internal var _state: Any? = UNDEFINED
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 abstract class DispatchedTask<in T>(
@@ -232,7 +234,7 @@ internal abstract class DispatchedTask<in T>(
232234
else {
233235
val exception = getExceptionalResult(state)
234236
if (exception != null)
235-
continuation.resumeWithException(exception)
237+
continuation.resumeWithStackTrace(exception)
236238
else
237239
continuation.resume(getSuccessfulResult(state))
238240
}
@@ -276,3 +278,9 @@ internal fun <T> DispatchedTask<T>.resume(delegate: Continuation<T>, useMode: In
276278
delegate.resumeMode(getSuccessfulResult(state), useMode)
277279
}
278280
}
281+
282+
283+
@Suppress("NOTHING_TO_INLINE")
284+
internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) {
285+
resumeWith(Result.failure(recoverStackTrace(exception, this)))
286+
}

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
247247
val seenExceptions = identitySet<Throwable>(exceptions.size)
248248
var suppressed = false
249249
for (exception in exceptions) {
250-
if (exception !== rootCause && exception !is CancellationException && seenExceptions.add(exception)) {
251-
rootCause.addSuppressedThrowable(exception)
250+
val unwrapped = unwrap(exception)
251+
if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(unwrapped)) {
252+
rootCause.addSuppressedThrowable(unwrapped)
252253
suppressed = true
253254
}
254255
}
@@ -929,7 +930,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
929930

930931
// for nicer debugging
931932
public override fun toString(): String =
932-
"${nameString()}{${stateString(state)}}@$hexAddress"
933+
"${toDebugString()}@$hexAddress"
934+
935+
@InternalCoroutinesApi
936+
public fun toDebugString(): String = "${nameString()}{${stateString(state)}}"
933937

934938
/**
935939
* @suppress **This is unstable API and it is subject to change.**
@@ -1083,7 +1087,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
10831087
val state = this.state
10841088
if (state !is Incomplete) {
10851089
// already complete -- just return result
1086-
if (state is CompletedExceptionally) throw state.cause
1090+
if (state is CompletedExceptionally) { // Slow path to recover stacktrace
1091+
recoverAndThrow(state.cause)
1092+
}
10871093
return state.unboxState()
10881094

10891095
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.coroutines
66

7+
import kotlinx.coroutines.internal.*
78
import kotlinx.coroutines.intrinsics.*
89
import kotlinx.coroutines.selects.*
910
import kotlin.coroutines.*
@@ -80,9 +81,10 @@ private fun <U, T: U> setupTimeout(
8081
private open class TimeoutCoroutine<U, in T: U>(
8182
@JvmField val time: Long,
8283
@JvmField val uCont: Continuation<U> // unintercepted continuation
83-
) : AbstractCoroutine<T>(uCont.context, active = true), Runnable, Continuation<T> {
84+
) : AbstractCoroutine<T>(uCont.context, active = true), Runnable, Continuation<T>, CoroutineStackFrame {
8485
override val defaultResumeMode: Int get() = MODE_DIRECT
85-
86+
override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame)?.callerFrame
87+
override fun getStackTraceElement(): StackTraceElement? = (uCont as? CoroutineStackFrame)?.getStackTraceElement()
8688
@Suppress("LeakingThis", "Deprecation")
8789
override fun run() {
8890
cancel(TimeoutCancellationException(time, this))

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

+16-14
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
182182
result === OFFER_SUCCESS -> true
183183
// We should check for closed token on offer as well, otherwise offer won't be linearizable
184184
// in the face of concurrent close()
185-
result === OFFER_FAILED -> throw closedForSend?.sendException ?: return false
186-
result is Closed<*> -> throw result.sendException
185+
result === OFFER_FAILED -> throw closedForSend?.sendException?.let { recoverStackTrace(it) } ?: return false
186+
result is Closed<*> -> throw recoverStackTrace(result.sendException)
187187
else -> error("offerInternal returned $result")
188188
}
189189
}
@@ -409,7 +409,7 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
409409
when {
410410
enqueueResult === ALREADY_SELECTED -> return
411411
enqueueResult === ENQUEUE_FAILED -> {} // retry
412-
enqueueResult is Closed<*> -> throw enqueueResult.sendException
412+
enqueueResult is Closed<*> -> throw recoverStackTrace(enqueueResult.sendException)
413413
else -> error("performAtomicIfNotSelected(TryEnqueueSendDesc) returned $enqueueResult")
414414
}
415415
} else {
@@ -421,7 +421,7 @@ internal abstract class AbstractSendChannel<E> : SendChannel<E> {
421421
block.startCoroutineUnintercepted(receiver = this, completion = select.completion)
422422
return
423423
}
424-
offerResult is Closed<*> -> throw offerResult.sendException
424+
offerResult is Closed<*> -> throw recoverStackTrace(offerResult.sendException)
425425
else -> error("offerSelectInternal returned $offerResult")
426426
}
427427
}
@@ -575,7 +575,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
575575

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

@@ -621,7 +621,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
621621
@Suppress("UNCHECKED_CAST")
622622
private fun receiveOrNullResult(result: Any?): E? {
623623
if (result is Closed<*>) {
624-
if (result.closeCause != null) throw result.closeCause
624+
if (result.closeCause != null) throw recoverStackTrace(result.closeCause)
625625
return null
626626
}
627627
return result as E
@@ -760,7 +760,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
760760
when {
761761
pollResult === ALREADY_SELECTED -> return
762762
pollResult === POLL_FAILED -> {} // retry
763-
pollResult is Closed<*> -> throw pollResult.receiveException
763+
pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException)
764764
else -> {
765765
block.startCoroutineUnintercepted(pollResult as E, select.completion)
766766
return
@@ -799,8 +799,9 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
799799
if (select.trySelect(null))
800800
block.startCoroutineUnintercepted(null, select.completion)
801801
return
802-
} else
803-
throw pollResult.closeCause
802+
} else {
803+
throw recoverStackTrace(pollResult.closeCause)
804+
}
804805
}
805806
else -> {
806807
// selected successfully
@@ -859,7 +860,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
859860

860861
private fun hasNextResult(result: Any?): Boolean {
861862
if (result is Closed<*>) {
862-
if (result.closeCause != null) throw result.receiveException
863+
if (result.closeCause != null) throw recoverStackTrace(result.receiveException)
863864
return false
864865
}
865866
return true
@@ -893,7 +894,7 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
893894
@Suppress("UNCHECKED_CAST")
894895
override suspend fun next(): E {
895896
val result = this.result
896-
if (result is Closed<*>) throw result.receiveException
897+
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
897898
if (result !== POLL_FAILED) {
898899
this.result = POLL_FAILED
899900
return result as E
@@ -945,10 +946,11 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
945946
}
946947

947948
override fun resumeReceiveClosed(closed: Closed<*>) {
948-
val token = if (closed.closeCause == null)
949+
val token = if (closed.closeCause == null) {
949950
cont.tryResume(false)
950-
else
951-
cont.tryResumeWithException(closed.receiveException)
951+
} else {
952+
cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont))
953+
}
952954
if (token != null) {
953955
iterator.result = closed
954956
cont.completeResume(token)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
internal expect inline fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T>

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

+13-4
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ 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")
2123
internal override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) {
22-
if (state is CompletedExceptionally)
23-
uCont.resumeUninterceptedWithExceptionMode(state.cause, mode)
24-
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 {
2528
uCont.resumeUninterceptedMode(state as T, mode)
29+
}
2630
}
2731
}
2832

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

0 commit comments

Comments
 (0)