Skip to content

Commit e32aa78

Browse files
committed
Ensure that all coroutines throwables in core are serializable
Fixes #3328
1 parent 2ee99e2 commit e32aa78

File tree

5 files changed

+108
-4
lines changed

5 files changed

+108
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.guava
6+
7+
import com.google.common.reflect.*
8+
import kotlinx.coroutines.*
9+
import org.junit.Test
10+
import kotlin.test.*
11+
12+
class ListAllCoroutineThrowableSubclassesTest : TestBase() {
13+
14+
/*
15+
* These are all known throwables in kotlinx.coroutines.
16+
* If you have added one, this test will fail to make
17+
* you ensure your exception type is java.io.Serializable.
18+
*
19+
* We do not have means to check it automatically, so checks are delegated to humans.
20+
* Also, this test meant to be in kotlinx-coroutines-core, but properly scanning classpath
21+
* requires guava which is toxic dependency that we'd like to avoid even in tests.
22+
*
23+
* See #3328 for serialization rationale.
24+
*/
25+
private val knownThrowables = setOf(
26+
"kotlinx.coroutines.TimeoutCancellationException",
27+
"kotlinx.coroutines.JobCancellationException",
28+
"kotlinx.coroutines.internal.UndeliveredElementException",
29+
"kotlinx.coroutines.CompletionHandlerException",
30+
"kotlinx.coroutines.DiagnosticCoroutineContextException",
31+
"kotlinx.coroutines.CoroutinesInternalError",
32+
"kotlinx.coroutines.channels.ClosedSendChannelException",
33+
"kotlinx.coroutines.channels.ClosedReceiveChannelException",
34+
"kotlinx.coroutines.flow.internal.ChildCancelledException",
35+
"kotlinx.coroutines.flow.internal.AbortFlowException",
36+
37+
)
38+
39+
@Test
40+
fun testThrowableSubclassesAreSerializable() {
41+
var throwables = 0
42+
val classes = ClassPath.from(this.javaClass.classLoader)
43+
.getTopLevelClassesRecursive("kotlinx.coroutines");
44+
classes.forEach {
45+
try {
46+
if (Throwable::class.java.isAssignableFrom(it.load())) {
47+
// Skip classes from test sources
48+
if (it.load().protectionDomain.codeSource.location.toString().contains("/test/")) {
49+
return@forEach
50+
}
51+
++throwables
52+
// println(""""$it",""")
53+
assertTrue(knownThrowables.contains(it.toString()))
54+
}
55+
} catch (e: Throwable) {
56+
// Ignore unloadable classes
57+
}
58+
}
59+
60+
assertEquals(knownThrowables.size, throwables)
61+
}
62+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ private class TimeoutCoroutine<U, in T: U>(
163163
*/
164164
public class TimeoutCancellationException internal constructor(
165165
message: String,
166-
@JvmField internal val coroutine: Job?
166+
@JvmField @Transient internal val coroutine: Job?
167167
) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> {
168168
/**
169169
* Creates a timeout exception with the given message.
@@ -173,7 +173,7 @@ public class TimeoutCancellationException internal constructor(
173173
internal constructor(message: String) : this(message, null)
174174

175175
// message is never null in fact
176-
override fun createCopy(): TimeoutCancellationException? =
176+
override fun createCopy(): TimeoutCancellationException =
177177
TimeoutCancellationException(message ?: "", coroutine).also { it.initCause(this) }
178178
}
179179

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public actual fun CancellationException(message: String?, cause: Throwable?) : C
2929
internal actual class JobCancellationException public actual constructor(
3030
message: String,
3131
cause: Throwable?,
32-
@JvmField internal actual val job: Job
32+
@JvmField @Transient internal actual val job: Job
3333
) : CancellationException(message), CopyableThrowable<JobCancellationException> {
3434

3535
init {

kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import kotlinx.coroutines.*
88
import kotlinx.coroutines.flow.*
99

1010
internal actual class AbortFlowException actual constructor(
11-
actual val owner: FlowCollector<*>
11+
@JvmField @Transient actual val owner: FlowCollector<*>
1212
) : CancellationException("Flow was aborted, no more elements needed") {
1313

1414
override fun fillInStackTrace(): Throwable {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import org.junit.*
8+
import java.io.*
9+
10+
11+
@Suppress("BlockingMethodInNonBlockingContext")
12+
class JobCancellationExceptionSerializerTest : TestBase() {
13+
14+
@Test
15+
fun testSerialization() = runTest {
16+
try {
17+
coroutineScope {
18+
expect(1)
19+
20+
launch {
21+
expect(2)
22+
try {
23+
hang {}
24+
} catch (e: CancellationException) {
25+
throw RuntimeException("RE2", e)
26+
}
27+
}
28+
29+
launch {
30+
expect(3)
31+
throw RuntimeException("RE1")
32+
}
33+
}
34+
} catch (e: Throwable) {
35+
// Should not fail
36+
ObjectOutputStream(ByteArrayOutputStream()).use {
37+
it.writeObject(e)
38+
}
39+
finish(4)
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)