diff --git a/docs/debugging.md b/docs/debugging.md index fc8570126d..e2c7ec1e07 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -45,7 +45,7 @@ It is easy to demonstrate with actual stacktraces of the same program that await The only downside of this approach is losing referential transparency of the exception. -### Stacktrace recovery machinery +### Stacktrace recovery machinery This section explains the inner mechanism of stacktrace recovery and can be skipped. @@ -56,6 +56,7 @@ and then throws the resulting exception instead of the original one. Exception copy logic is straightforward: 1) If the exception class implements [CopyableThrowable], [CopyableThrowable.createCopy] is used. + `null` can be returned from `createCopy` to opt-out specific exception from being recovered. 2) If the exception class has class-specific fields not inherited from Throwable, the exception is not copied. 3) Otherwise, one of the public exception's constructor is invoked reflectively with an optional `initCause` call. diff --git a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt index 8ce0fcd261..8599143e95 100644 --- a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt @@ -42,9 +42,3 @@ internal expect interface CoroutineStackFrame { public val callerFrame: CoroutineStackFrame? public fun getStackTraceElement(): StackTraceElement? } - -/** - * Marker that indicates that stacktrace of the exception should not be recovered. - * Currently internal, but may become public in the future - */ -internal interface NonRecoverableThrowable diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 50df19a6b1..ad7b8b1508 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -2,11 +2,12 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("unused") + package kotlinx.coroutines import kotlinx.coroutines.flow.* import kotlin.coroutines.* -import kotlinx.coroutines.internal.* import kotlin.test.* public expect open class TestBase constructor() { @@ -56,12 +57,14 @@ public suspend inline fun assertFailsWith(flow: Flow<*>) public suspend fun Flow.sum() = fold(0) { acc, value -> acc + value } public suspend fun Flow.longSum() = fold(0L) { acc, value -> acc + value } -public class TestException(message: String? = null) : Throwable(message), NonRecoverableThrowable -public class TestException1(message: String? = null) : Throwable(message), NonRecoverableThrowable -public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable -public class TestException3(message: String? = null) : Throwable(message), NonRecoverableThrowable -public class TestCancellationException(message: String? = null) : CancellationException(message), NonRecoverableThrowable -public class TestRuntimeException(message: String? = null) : RuntimeException(message), NonRecoverableThrowable + +// data is added to avoid stacktrace recovery because CopyableThrowable is not accessible from common modules +public class TestException(message: String? = null, private val data: Any? = null) : Throwable(message) +public class TestException1(message: String? = null, private val data: Any? = null) : Throwable(message) +public class TestException2(message: String? = null, private val data: Any? = null) : Throwable(message) +public class TestException3(message: String? = null, private val data: Any? = null) : Throwable(message) +public class TestCancellationException(message: String? = null, private val data: Any? = null) : CancellationException(message) +public class TestRuntimeException(message: String? = null, private val data: Any? = null) : RuntimeException(message) public class RecoverableTestException(message: String? = null) : RuntimeException(message) public class RecoverableTestCancellationException(message: String? = null) : CancellationException(message) diff --git a/kotlinx-coroutines-core/jvm/src/Debug.kt b/kotlinx-coroutines-core/jvm/src/Debug.kt index e818bfd940..98a1c1ea7d 100644 --- a/kotlinx-coroutines-core/jvm/src/Debug.kt +++ b/kotlinx-coroutines-core/jvm/src/Debug.kt @@ -42,7 +42,6 @@ public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug" * Stacktrace recovery mode wraps every exception into the exception of the same type with original exception * as cause, but with stacktrace of the current coroutine. * Exception is instantiated using reflection by using no-arg, cause or cause and message constructor. - * Stacktrace is not recovered if exception is an instance of [CancellationException] or [NonRecoverableThrowable]. * * This mechanism is currently supported for channels, [async], [launch], [coroutineScope], [supervisorScope] * and [withContext] builders. diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index ed2861a80d..2d7ed7a3d1 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -27,7 +27,7 @@ private val stackTraceRecoveryClassName = runCatching { }.getOrElse { stackTraceRecoveryClass } internal actual fun recoverStackTrace(exception: E): E { - if (recoveryDisabled(exception)) return exception + if (!RECOVER_STACK_TRACES) return exception // No unwrapping on continuation-less path: exception is not reported multiple times via slow paths val copy = tryCopyException(exception) ?: return exception return copy.sanitizeStackTrace() @@ -53,7 +53,7 @@ private fun E.sanitizeStackTrace(): E { } internal actual fun recoverStackTrace(exception: E, continuation: Continuation<*>): E { - if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) return exception + if (!RECOVER_STACK_TRACES || continuation !is CoroutineStackFrame) return exception return recoverFromStackFrame(exception, continuation) } @@ -147,7 +147,7 @@ private fun mergeRecoveredTraces(recoveredStacktrace: Array, @Suppress("NOTHING_TO_INLINE") internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing { - if (recoveryDisabled(exception)) throw exception + if (!RECOVER_STACK_TRACES) throw exception suspendCoroutineUninterceptedOrReturn { if (it !is CoroutineStackFrame) throw exception throw recoverFromStackFrame(exception, it) @@ -155,7 +155,7 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin } internal actual fun unwrap(exception: E): E { - if (recoveryDisabled(exception)) return exception + if (!RECOVER_STACK_TRACES) return exception val cause = exception.cause // Fast-path to avoid array cloning if (cause == null || cause.javaClass != exception.javaClass) { @@ -170,9 +170,6 @@ internal actual fun unwrap(exception: E): E { } } -private fun recoveryDisabled(exception: E) = - !RECOVER_STACK_TRACES || exception is NonRecoverableThrowable - private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque { val stack = ArrayDeque() continuation.getStackTraceElement()?.let { stack.add(it) }