Check cancellation when starting unconfined coroutine
Fixes #621
diff --git a/common/kotlinx-coroutines-core-common/src/Dispatched.kt b/common/kotlinx-coroutines-core-common/src/Dispatched.kt
index ccb8d84..83eed65 100644
--- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt
+++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt
@@ -43,8 +43,9 @@
_state = CompletedExceptionally(exception)
resumeMode = MODE_ATOMIC_DEFAULT
dispatcher.dispatch(context, this)
- } else
+ } else {
resumeUndispatchedWithException(exception)
+ }
}
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
@@ -54,8 +55,11 @@
_state = value
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
- } else
- resumeUndispatched(value)
+ } else {
+ if (!resumeCancelled()) {
+ resumeUndispatched(value)
+ }
+ }
}
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
@@ -65,8 +69,22 @@
_state = CompletedExceptionally(exception)
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
- } else
- resumeUndispatchedWithException(exception)
+ } else {
+ if (!resumeCancelled()) {
+ resumeUndispatchedWithException(exception)
+ }
+ }
+ }
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline fun resumeCancelled(): Boolean {
+ val job = context[Job]
+ if (job != null && !job.isActive) {
+ resumeWithException(job.getCancellationException())
+ return true
+ }
+
+ return false
}
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
diff --git a/common/kotlinx-coroutines-core-common/test/ExperimentalDispatchModeTest.kt b/common/kotlinx-coroutines-core-common/test/ExperimentalDispatchModeTest.kt
new file mode 100644
index 0000000..fc93628
--- /dev/null
+++ b/common/kotlinx-coroutines-core-common/test/ExperimentalDispatchModeTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.experimental
+
+import kotlin.test.*
+
+class ExperimentalDispatchModeTest : TestBase() {
+ @Test
+ fun testUnconfinedCancellation() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ parent.cancel()
+ launch(Dispatchers.Unconfined) {
+ expectUnreached()
+ }
+
+ }.join()
+ finish(2)
+ }
+
+ @Test
+ fun testUnconfinedCancellationState() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ parent.cancel()
+ val job = launch(Dispatchers.Unconfined) {
+ expectUnreached()
+ }
+
+ assertTrue(job.isCancelled)
+ assertTrue(job.isCompleted)
+ assertFalse(job.isActive)
+ }.join()
+ finish(2)
+ }
+
+ @Test
+ fun testUnconfinedCancellationLazy() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ val job = launch(Dispatchers.Unconfined, start = CoroutineStart.LAZY) {
+ expectUnreached()
+ }
+ job.invokeOnCompletion { expect(2) }
+ assertFalse(job.isCompleted)
+
+ parent.cancel()
+ job.join()
+ }.join()
+ finish(3)
+ }
+
+ @Test
+ fun testUndispatchedCancellation() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ parent.cancel()
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ expect(2)
+ yield()
+ expectUnreached()
+ }
+
+ }.join()
+ finish(3)
+ }
+
+ @Test
+ fun testCancelledAtomicUnconfined() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ parent.cancel()
+ launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
+ expect(2)
+ yield()
+ expectUnreached()
+ }
+ }.join()
+ finish(3)
+ }
+
+
+ @Test
+ fun testCancelledWithContextUnconfined() = runTest {
+ val parent = Job()
+ launch(parent) {
+ expect(1)
+ parent.cancel()
+ withContext(Dispatchers.Unconfined) {
+ expectUnreached()
+ }
+ }.join()
+ finish(2)
+ }
+}
\ No newline at end of file