Skip to content

Commit 4ba30cd

Browse files
committed
Stacktrace recovery improvements
* Introduce CopyableThrowable to provide a mechanism for flexible exception cloning * Do not reflectively clone exceptions with additional non-static fields
1 parent 3e4d808 commit 4ba30cd

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ public final class kotlinx/coroutines/CompletionHandlerException : java/lang/Run
121121
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
122122
}
123123

124+
public abstract interface class kotlinx/coroutines/CopyableThrowable {
125+
public abstract fun createCopy ()Ljava/lang/Throwable;
126+
}
127+
124128
public final class kotlinx/coroutines/CoroutineContextKt {
125129
public static final fun newCoroutineContext (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
126130
}

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

+27-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
package kotlinx.coroutines
66

7-
import kotlin.coroutines.Continuation
87
import kotlinx.coroutines.internal.*
8+
import kotlin.coroutines.*
99

1010
/**
1111
* Name of the property that controls coroutine debugging. See [newCoroutineContext][CoroutineScope.newCoroutineContext].
@@ -26,6 +26,32 @@ public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug"
2626
*/
2727
internal const val STACKTRACE_RECOVERY_PROPERTY_NAME = "kotlinx.coroutines.stacktrace.recovery"
2828

29+
/**
30+
* Throwable which can be cloned during stacktrace recovery in a class-specific way.
31+
* For additional information about stacktrace recovery see [STACKTRACE_RECOVERY_PROPERTY_NAME]
32+
*
33+
* Example of usage:
34+
* ```
35+
* class BadResponseCodeException(val responseCode: Int) : Exception(), CopyableThrowable<BadResponseCodeException> {
36+
*
37+
* override fun createCopy(): BadResponseCodeException {
38+
* val result = BadResponseCodeException(responseCode)
39+
* result.initCause(this)
40+
* return result
41+
* }
42+
* ```
43+
*/
44+
@ExperimentalCoroutinesApi
45+
public interface CopyableThrowable<T> where T : Throwable, T : CopyableThrowable<T> {
46+
47+
/**
48+
* Creates a copy of the current instance.
49+
* For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one.
50+
* Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call.
51+
*/
52+
public fun createCopy(): T
53+
}
54+
2955
/**
3056
* Automatic debug configuration value for [DEBUG_PROPERTY_NAME]. See [newCoroutineContext][CoroutineScope.newCoroutineContext].
3157
*/

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

+23
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@
44

55
package kotlinx.coroutines.internal
66

7+
import kotlinx.coroutines.*
8+
import java.lang.reflect.*
79
import java.util.*
810
import java.util.concurrent.locks.*
911
import kotlin.concurrent.*
1012

13+
private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1)
1114
private val cacheLock = ReentrantReadWriteLock()
1215
// Replace it with ClassValue when Java 6 support is over
1316
private val exceptionConstructors: WeakHashMap<Class<out Throwable>, (Throwable) -> Throwable?> = WeakHashMap()
1417

1518
@Suppress("UNCHECKED_CAST")
1619
internal fun <E : Throwable> tryCopyException(exception: E): E? {
20+
if (exception is CopyableThrowable<*>) {
21+
return runCatching { exception.createCopy() as E }.getOrNull()
22+
}
23+
1724
val cachedCtor = cacheLock.read {
1825
exceptionConstructors[exception.javaClass]
1926
}
2027

2128
if (cachedCtor != null) return cachedCtor(exception) as E?
29+
/*
30+
* Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors)
31+
*/
32+
if (throwableFields != exception.javaClass.fieldsCountOrDefault(0)) {
33+
cacheLock.write { exceptionConstructors[exception.javaClass] = { null } }
34+
return null
35+
}
2236

2337
/*
2438
* Try to reflectively find constructor(), constructor(message, cause) or constructor(cause).
@@ -43,3 +57,12 @@ internal fun <E : Throwable> tryCopyException(exception: E): E? {
4357
cacheLock.write { exceptionConstructors[exception.javaClass] = (ctor ?: { null }) }
4458
return ctor?.invoke(exception) as E?
4559
}
60+
61+
private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
62+
63+
private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {
64+
val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) }
65+
val totalFields = accumulator + fieldsCount
66+
val superClass = superclass ?: return totalFields
67+
return superClass.fieldsCount(totalFields)
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.exceptions
6+
7+
import kotlinx.coroutines.*
8+
import org.junit.Test
9+
import kotlin.test.*
10+
11+
@Suppress("UNREACHABLE_CODE", "UNUSED", "UNUSED_PARAMETER")
12+
class StackTraceRecoveryCustomExceptionsTest : TestBase() {
13+
14+
internal class NonCopyable(val customData: Int) : Throwable() {
15+
// Bait
16+
public constructor(cause: Throwable) : this(42)
17+
}
18+
19+
internal class Copyable(val customData: Int) : Throwable(), CopyableThrowable<Copyable> {
20+
// Bait
21+
public constructor(cause: Throwable) : this(42)
22+
23+
override fun createCopy(): Copyable {
24+
val copy = Copyable(customData)
25+
copy.initCause(this)
26+
return copy
27+
}
28+
}
29+
30+
@Test
31+
fun testStackTraceNotRecovered() = runTest {
32+
try {
33+
withContext(wrapperDispatcher(coroutineContext)) {
34+
throw NonCopyable(239)
35+
}
36+
expectUnreached()
37+
} catch (e: NonCopyable) {
38+
assertEquals(239, e.customData)
39+
assertNull(e.cause)
40+
}
41+
}
42+
43+
@Test
44+
fun testStackTraceRecovered() = runTest {
45+
try {
46+
withContext(wrapperDispatcher(coroutineContext)) {
47+
throw Copyable(239)
48+
}
49+
expectUnreached()
50+
} catch (e: Copyable) {
51+
assertEquals(239, e.customData)
52+
val cause = e.cause
53+
assertTrue(cause is Copyable)
54+
assertEquals(239, cause.customData)
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)