Skip to content

Commit d293639

Browse files
committed
Make JobCancellationException copyable in debug mode
This simplifies trouble-shooting of "what caused cancellation", as you can print out exception and see its recovered stack trace.
1 parent 0aad8f1 commit d293639

File tree

10 files changed

+58
-27
lines changed

10 files changed

+58
-27
lines changed

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ internal expect class JobCancellationException(
2525

2626
internal expect class DispatchException(message: String, cause: Throwable) : RuntimeException
2727

28-
internal expect fun Throwable.addSuppressedThrowable(other: Throwable)
28+
internal expect fun Throwable.addSuppressedThrowable(other: Throwable)
29+
30+
// For use in tests
31+
internal expect val RECOVER_STACK_TRACES: Boolean

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,14 @@ class CoroutineScopeTest : TestBase() {
120120
try {
121121
callJobScoped()
122122
expectUnreached()
123-
} catch (e: CancellationException) {
123+
} catch (e: JobCancellationException) {
124124
expect(5)
125-
assertNull(e.cause)
125+
if (RECOVER_STACK_TRACES) {
126+
val cause = e.cause as JobCancellationException // shall be recovered JCE
127+
assertNull(cause.cause)
128+
} else {
129+
assertNull(e.cause)
130+
}
126131
}
127132
}
128133
repeat(3) { yield() } // let everything to start properly

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

+14-4
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ class NonCancellableTest : TestBase() {
2929
try {
3030
job.await()
3131
expectUnreached()
32-
} catch (e: CancellationException) {
33-
assertNull(e.cause)
32+
} catch (e: JobCancellationException) {
33+
if (RECOVER_STACK_TRACES) {
34+
val cause = e.cause as JobCancellationException // shall be recovered JCE
35+
assertNull(cause.cause)
36+
} else {
37+
assertNull(e.cause)
38+
}
3439
finish(6)
3540
}
3641
}
@@ -119,8 +124,13 @@ class NonCancellableTest : TestBase() {
119124
try {
120125
job.await()
121126
expectUnreached()
122-
} catch (e: CancellationException) {
123-
assertNull(e.cause)
127+
} catch (e: JobCancellationException) {
128+
if (RECOVER_STACK_TRACES) {
129+
val cause = e.cause as JobCancellationException // shall be recovered JCE
130+
assertNull(cause.cause)
131+
} else {
132+
assertNull(e.cause)
133+
}
124134
finish(7)
125135
}
126136
}

kotlinx-coroutines-core/js/src/Exceptions.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,7 @@ private fun String?.withCause(cause: Throwable?) =
5858
if (cause == null) this else "$this; caused by $cause"
5959

6060
@Suppress("NOTHING_TO_INLINE")
61-
internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ }
61+
internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ }
62+
63+
// For use in tests
64+
internal actual val RECOVER_STACK_TRACES: Boolean = false

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ public interface CopyableThrowable<T> where T : Throwable, T : CopyableThrowable
4848
* Creates a copy of the current instance.
4949
* For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one.
5050
* Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call.
51+
* An exception can opt-out of copying by returning `null` from this function.
5152
*/
52-
public fun createCopy(): T
53+
public fun createCopy(): T?
5354
}
5455

5556
/**
@@ -77,8 +78,9 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value ->
7778
}
7879
}
7980

81+
// Note: stack-trace recovery is enabled only in debug mode
8082
@JvmField
81-
internal val RECOVER_STACKTRACES = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
83+
internal actual val RECOVER_STACK_TRACES = DEBUG && systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
8284

8385
// internal debugging tools
8486

kotlinx-coroutines-core/jvm/src/Exceptions.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ internal actual class JobCancellationException public actual constructor(
4141
message: String,
4242
cause: Throwable?,
4343
@JvmField internal actual val job: Job
44-
) : CancellationException(message) {
44+
) : CancellationException(message), CopyableThrowable<JobCancellationException> {
4545

4646
init {
4747
if (cause != null) initCause(cause)
@@ -60,6 +60,17 @@ internal actual class JobCancellationException public actual constructor(
6060
return this
6161
}
6262

63+
override fun createCopy(): JobCancellationException? {
64+
if (DEBUG) {
65+
return JobCancellationException(message!!, this, job)
66+
}
67+
68+
/*
69+
* In non-debug mode we don't copy JCE for speed as it does not have the stack trace anyway.
70+
*/
71+
return null
72+
}
73+
6374
override fun toString(): String = "${super.toString()}; job=$job"
6475

6576
@Suppress("DEPRECATION")

kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMa
2020
internal fun <E : Throwable> tryCopyException(exception: E): E? {
2121
// Fast path for CopyableThrowable
2222
if (exception is CopyableThrowable<*>) {
23-
return runCatching { exception.createCopy() as E }.getOrNull()
23+
return runCatching { exception.createCopy() as E? }.getOrNull()
2424
}
2525
// Use cached ctor if found
2626
cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor ->

kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt

+6-14
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import kotlin.coroutines.*
1212
import kotlin.coroutines.intrinsics.*
1313

1414
internal actual fun <E : Throwable> recoverStackTrace(exception: E): E {
15-
if (recoveryDisabled(exception)) {
16-
return exception
17-
}
15+
if (recoveryDisabled(exception)) return exception
1816
// No unwrapping on continuation-less path: exception is not reported multiple times via slow paths
1917
val copy = tryCopyException(exception) ?: return exception
2018
return copy.sanitizeStackTrace()
@@ -41,10 +39,7 @@ private fun <E : Throwable> E.sanitizeStackTrace(): E {
4139
}
4240

4341
internal actual fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
44-
if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) {
45-
return exception
46-
}
47-
42+
if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) return exception
4843
return recoverFromStackFrame(exception, continuation)
4944
}
5045

@@ -146,26 +141,23 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin
146141
}
147142

148143
internal actual fun <E : Throwable> unwrap(exception: E): E {
149-
if (recoveryDisabled(exception)) {
150-
return exception
151-
}
152-
144+
if (recoveryDisabled(exception)) return exception
153145
val cause = exception.cause
154146
// Fast-path to avoid array cloning
155147
if (cause == null || cause.javaClass != exception.javaClass) {
156148
return exception
157149
}
158-
150+
// Slow path looks for artificial frames in a stack-trace
159151
if (exception.stackTrace.any { it.isArtificial() }) {
160152
@Suppress("UNCHECKED_CAST")
161-
return exception.cause as? E ?: exception
153+
return cause as E
162154
} else {
163155
return exception
164156
}
165157
}
166158

167159
private fun <E : Throwable> recoveryDisabled(exception: E) =
168-
!RECOVER_STACKTRACES || !DEBUG || exception is NonRecoverableThrowable
160+
!RECOVER_STACK_TRACES || exception is NonRecoverableThrowable
169161

170162
private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque<StackTraceElement> {
171163
val stack = ArrayDeque<StackTraceElement>()

kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,10 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() {
171171
@Test
172172
fun testCancel() = runTest {
173173
runOnlyCancellation(null) { e ->
174-
assertNull(e.cause)
174+
val cause = e.cause as JobCancellationException // shall be recovered JCE
175+
assertNull(cause.cause)
175176
assertTrue(e.suppressed.isEmpty())
177+
assertTrue(cause.suppressed.isEmpty())
176178
}
177179
}
178180

kotlinx-coroutines-core/native/src/Exceptions.kt

+3
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ private fun String?.withCause(cause: Throwable?) =
5959

6060
@Suppress("NOTHING_TO_INLINE")
6161
internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ }
62+
63+
// For use in tests
64+
internal actual val RECOVER_STACK_TRACES: Boolean = false

0 commit comments

Comments
 (0)