Skip to content

Commit 4b85eb6

Browse files
Introduce ClassValue-based exception constructor cache to stracktrace recovery (#2997)
* Use ClassValue-based cache for exception constructors with stacktrace recovery * It eliminates the classloader leak in containerized environments * Insignificantly improves the performance of exception copying * Creates a precedent of guarded use of non-Android-compliant API Co-authored-by: dkhalanskyjb <[email protected]>
1 parent 0427205 commit 4b85eb6

File tree

3 files changed

+54
-34
lines changed

3 files changed

+54
-34
lines changed

kotlinx-coroutines-core/build.gradle

-7
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,6 @@ task checkJdk16() {
201201
}
202202
}
203203

204-
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
205-
kotlinOptions.jdkHome = System.env.JDK_16
206-
// only fail when actually trying to compile, not during project setup phase
207-
dependsOn(checkJdk16)
208-
}
209-
210204
jvmTest {
211205
minHeapSize = '1g'
212206
maxHeapSize = '1g'
@@ -283,7 +277,6 @@ task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) {
283277
classpath = files { jvmTest.classpath }
284278
testClassesDirs = files { jvmTest.testClassesDirs }
285279
executable = "$System.env.JDK_16/bin/java"
286-
exclude '**/*LFStressTest.*' // lock-freedom tests use LockFreedomTestEnvironment which needs JDK8
287280
exclude '**/*LincheckTest.*' // Lincheck tests use LinChecker which needs JDK8
288281
exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8
289282
exclude '**/ExceptionsGuideTest.*'

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

+54-26
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,42 @@ import java.util.concurrent.locks.*
1111
import kotlin.concurrent.*
1212

1313
private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1)
14-
private val cacheLock = ReentrantReadWriteLock()
1514
private typealias Ctor = (Throwable) -> Throwable?
16-
// Replace it with ClassValue when Java 6 support is over
17-
private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMap()
15+
16+
private val ctorCache = try {
17+
if (ANDROID_DETECTED) WeakMapCtorCache
18+
else ClassValueCtorCache
19+
} catch (e: Throwable) {
20+
// Fallback on Java 6 or exotic setups
21+
WeakMapCtorCache
22+
}
1823

1924
@Suppress("UNCHECKED_CAST")
2025
internal fun <E : Throwable> tryCopyException(exception: E): E? {
2126
// Fast path for CopyableThrowable
2227
if (exception is CopyableThrowable<*>) {
2328
return runCatching { exception.createCopy() as E? }.getOrNull()
2429
}
25-
// Use cached ctor if found
26-
cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor ->
27-
return cachedCtor(exception) as E?
28-
}
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 { exceptionCtors[exception.javaClass] = { null } }
34-
return null
35-
}
30+
return ctorCache.get(exception.javaClass).invoke(exception) as E?
31+
}
32+
33+
private fun <E : Throwable> createConstructor(clz: Class<E>): Ctor {
34+
val nullResult: Ctor = { null } // Pre-cache class
35+
// Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors)
36+
if (throwableFields != clz.fieldsCountOrDefault(0)) return nullResult
3637
/*
37-
* Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message).
38-
* Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
39-
*/
40-
var ctor: Ctor? = null
41-
val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size }
38+
* Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message).
39+
* Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
40+
*/
41+
val constructors = clz.constructors.sortedByDescending { it.parameterTypes.size }
4242
for (constructor in constructors) {
43-
ctor = createConstructor(constructor)
44-
if (ctor != null) break
43+
val result = createSafeConstructor(constructor)
44+
if (result != null) return result
4545
}
46-
// Store the resulting ctor to cache
47-
cacheLock.write { exceptionCtors[exception.javaClass] = ctor ?: { null } }
48-
return ctor?.invoke(exception) as E?
46+
return nullResult
4947
}
5048

51-
private fun createConstructor(constructor: Constructor<*>): Ctor? {
49+
private fun createSafeConstructor(constructor: Constructor<*>): Ctor? {
5250
val p = constructor.parameterTypes
5351
return when (p.size) {
5452
2 -> when {
@@ -71,11 +69,41 @@ private fun createConstructor(constructor: Constructor<*>): Ctor? {
7169
private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor =
7270
{ e -> runCatching { block(e) }.getOrNull() }
7371

74-
private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
72+
private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) =
73+
kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
7574

7675
private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {
7776
val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) }
7877
val totalFields = accumulator + fieldsCount
7978
val superClass = superclass ?: return totalFields
8079
return superClass.fieldsCount(totalFields)
8180
}
81+
82+
internal abstract class CtorCache {
83+
abstract fun get(key: Class<out Throwable>): Ctor
84+
}
85+
86+
private object WeakMapCtorCache : CtorCache() {
87+
private val cacheLock = ReentrantReadWriteLock()
88+
private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMap()
89+
90+
override fun get(key: Class<out Throwable>): Ctor {
91+
cacheLock.read { exceptionCtors[key]?.let { return it } }
92+
cacheLock.write {
93+
exceptionCtors[key]?.let { return it }
94+
return createConstructor(key).also { exceptionCtors[key] = it }
95+
}
96+
}
97+
}
98+
99+
@IgnoreJreRequirement
100+
private object ClassValueCtorCache : CtorCache() {
101+
private val cache = object : ClassValue<Ctor>() {
102+
override fun computeValue(type: Class<*>?): Ctor {
103+
@Suppress("UNCHECKED_CAST")
104+
return createConstructor(type as Class<out Throwable>)
105+
}
106+
}
107+
108+
override fun get(key: Class<out Throwable>): Ctor = cache.get(key)
109+
}

ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import java.io.*
1111
import java.util.stream.*
1212
import kotlin.test.*
1313

14-
@Ignore
1514
class R8ServiceLoaderOptimizationTest : TestBase() {
1615
private val r8Dex = File(System.getProperty("dexPath")!!).asDexFile()
1716
private val r8DexNoOptim = File(System.getProperty("noOptimDexPath")!!).asDexFile()

0 commit comments

Comments
 (0)