Skip to content

Commit 31b23e8

Browse files
committed
Fail-fast if coroutine start internal machinery has failed: treat any internal exception as coroutine result and invoke completion with such exception
Fixes #880
1 parent 29493e1 commit 31b23e8

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed

common/kotlinx-coroutines-core-common/src/intrinsics/Cancellable.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,29 @@ import kotlin.coroutines.intrinsics.*
1212
* Use this function to start coroutine in a cancellable way, so that it can be cancelled
1313
* while waiting to be dispatched.
1414
*/
15-
internal fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>) =
15+
internal fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>) = runSafely(completion) {
1616
createCoroutineUnintercepted(completion).intercepted().resumeCancellable(Unit)
17+
}
1718

1819
/**
1920
* Use this function to start coroutine in a cancellable way, so that it can be cancelled
2021
* while waiting to be dispatched.
2122
*/
2223
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
23-
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
24+
runSafely(completion) {
25+
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
26+
}
27+
28+
/**
29+
* Runs given block and completes completion with its exception if it occurs.
30+
* Rationale: [startCoroutineCancellable] is invoked when we are about to run coroutine asynchronously in its own dispatcher.
31+
* Thus if dispatcher throws an exception during coroutine start, coroutine never completes, so we should treat dispatcher exception
32+
* as its cause and resume completion.
33+
*/
34+
private inline fun runSafely(completion: Continuation<*>, block: () -> Unit) {
35+
try {
36+
block()
37+
} catch (e: Throwable) {
38+
completion.resumeWith(Result.failure(e))
39+
}
40+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("DeferredResultUnused")
6+
7+
package kotlinx.coroutines
8+
9+
import kotlinx.coroutines.channels.*
10+
import org.junit.*
11+
import org.junit.Test
12+
import org.junit.rules.*
13+
import kotlin.test.*
14+
15+
class FailFastOnStartTest : TestBase() {
16+
17+
@Rule
18+
@JvmField
19+
public val timeout: Timeout = Timeout.seconds(5)
20+
21+
@Test
22+
fun testLaunch() = runTest(expected = ::mainException) {
23+
launch(Dispatchers.Main) {}
24+
}
25+
26+
@Test
27+
fun testLaunchLazy() = runTest(expected = ::mainException) {
28+
val job = launch(Dispatchers.Main, start = CoroutineStart.LAZY) { fail() }
29+
job.join()
30+
}
31+
32+
@Test
33+
fun testLaunchUndispatched() = runTest(expected = ::mainException) {
34+
launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
35+
yield()
36+
fail()
37+
}
38+
}
39+
40+
@Test
41+
fun testAsync() = runTest(expected = ::mainException) {
42+
async(Dispatchers.Main) {}
43+
}
44+
45+
@Test
46+
fun testAsyncLazy() = runTest(expected = ::mainException) {
47+
val job = async(Dispatchers.Main, start = CoroutineStart.LAZY) { fail() }
48+
job.await()
49+
}
50+
51+
@Test
52+
fun testWithContext() = runTest(expected = ::mainException) {
53+
withContext(Dispatchers.Main) {
54+
fail()
55+
}
56+
}
57+
58+
@Test
59+
fun testProduce() = runTest(expected = ::mainException) {
60+
produce<Int>(Dispatchers.Main) { fail() }
61+
}
62+
63+
@Test
64+
fun testActor() = runTest(expected = ::mainException) {
65+
actor<Int>(Dispatchers.Main) { fail() }
66+
}
67+
68+
@Test
69+
fun testActorLazy() = runTest(expected = ::mainException) {
70+
val actor = actor<Int>(Dispatchers.Main, start = CoroutineStart.LAZY) { fail() }
71+
actor.send(1)
72+
}
73+
74+
private fun mainException(e: Throwable): Boolean {
75+
return e is IllegalStateException && e.message?.contains("Module with the Main dispatcher is missing") ?: false
76+
}
77+
}

0 commit comments

Comments
 (0)