Skip to content

Commit 08bda30

Browse files
committed
Ignore resume on cancelled continuation
There is a race between cancellation of outer job and invocation of resume on a cancellable continuation and this race cannot be worked around by the code that uses suspendCancellableCoroutine function. Thus resume attempt on a cancelled continuation shall be simply ignored. Fixes #450
1 parent 52a0ec0 commit 08bda30

File tree

3 files changed

+127
-52
lines changed

3 files changed

+127
-52
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,17 @@ internal abstract class AbstractContinuation<in T>(
272272
}
273273
}
274274
}
275-
276275
is NotCompleted -> {
277276
if (updateStateToFinal(state, proposedUpdate, resumeMode)) return
278277
}
279278
is CancelledContinuation -> {
280-
if (proposedUpdate is NotCompleted || proposedUpdate is CompletedExceptionally) {
281-
error("Unexpected update, state: $state, update: $proposedUpdate")
282-
}
283-
// Coroutine is dispatched normally (e.g.via `delay()`) after cancellation
279+
/*
280+
* If continuation was cancelled, then all further updates (resumes or exceptions) must be
281+
* ignored, because cancellation is asynchronous and may race with resume/resumeWithException.
282+
* This race is normal.
283+
*
284+
* :todo: we should somehow remember the attempt to invoke resume and fail on the second attempt.
285+
*/
284286
return
285287
}
286288
else -> error("Already resumed, but proposed with update $proposedUpdate")
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
package kotlinx.coroutines.experimental
6+
7+
import kotlin.coroutines.experimental.*
8+
import kotlin.test.*
9+
10+
class CancellableContinuationTest : TestBase() {
11+
@Test
12+
fun testResumeWithExceptionAndResumeWithException() = runTest {
13+
var continuation: Continuation<Unit>? = null
14+
val job = launch(coroutineContext) {
15+
try {
16+
expect(2)
17+
suspendCancellableCoroutine<Unit> { c ->
18+
continuation = c
19+
}
20+
} catch (e: TestException) {
21+
expect(3)
22+
}
23+
}
24+
expect(1)
25+
yield()
26+
continuation!!.resumeWithException(TestException())
27+
yield()
28+
assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(TestException()) }
29+
job.join()
30+
finish(4)
31+
}
32+
33+
@Test
34+
fun testResumeAndResumeWithException() = runTest {
35+
var continuation: Continuation<Unit>? = null
36+
val job = launch(coroutineContext) {
37+
expect(2)
38+
suspendCancellableCoroutine<Unit> { c ->
39+
continuation = c
40+
}
41+
expect(3)
42+
}
43+
expect(1)
44+
yield()
45+
continuation!!.resume(Unit)
46+
job.join()
47+
assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(TestException()) }
48+
finish(4)
49+
}
50+
51+
@Test
52+
fun testResumeAndResume() = runTest {
53+
var continuation: Continuation<Unit>? = null
54+
val job = launch(coroutineContext) {
55+
expect(2)
56+
suspendCancellableCoroutine<Unit> { c ->
57+
continuation = c
58+
}
59+
expect(3)
60+
}
61+
expect(1)
62+
yield()
63+
continuation!!.resume(Unit)
64+
job.join()
65+
assertFailsWith<IllegalStateException> { continuation!!.resume(Unit) }
66+
finish(4)
67+
}
68+
69+
/**
70+
* Cancelling outer job may, in practise, race with attempt to resume continuation and resumes
71+
* should be ignored. Here suspended coroutine is cancelled but then resumed with exception.
72+
*/
73+
@Test
74+
fun testCancelAndResumeWithException() = runTest {
75+
var continuation: Continuation<Unit>? = null
76+
val job = launch(coroutineContext) {
77+
try {
78+
expect(2)
79+
suspendCancellableCoroutine<Unit> { c ->
80+
continuation = c
81+
}
82+
} catch (e: JobCancellationException) {
83+
expect(3)
84+
}
85+
}
86+
expect(1)
87+
yield()
88+
job.cancel() // Cancel job
89+
yield()
90+
continuation!!.resumeWithException(TestException()) // Should not fail
91+
finish(4)
92+
}
93+
94+
/**
95+
* Cancelling outer job may, in practise, race with attempt to resume continuation and resumes
96+
* should be ignored. Here suspended coroutine is cancelled but then resumed with exception.
97+
*/
98+
@Test
99+
fun testCancelAndResume() = runTest {
100+
var continuation: Continuation<Unit>? = null
101+
val job = launch(coroutineContext) {
102+
try {
103+
expect(2)
104+
suspendCancellableCoroutine<Unit> { c ->
105+
continuation = c
106+
}
107+
} catch (e: JobCancellationException) {
108+
expect(3)
109+
}
110+
}
111+
expect(1)
112+
yield()
113+
job.cancel() // Cancel job
114+
yield()
115+
continuation!!.resume(Unit) // Should not fail
116+
finish(4)
117+
}
118+
119+
private class TestException : Exception()
120+
}

core/kotlinx-coroutines-core/test/CancellableContinuationExceptionHandlingTest.kt

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -215,53 +215,6 @@ class CancellableContinuationExceptionHandlingTest : TestBase() {
215215
}
216216
}
217217

218-
@Test
219-
fun testMultipleCancellations() = runTest {
220-
var continuation: Continuation<Unit>? = null
221-
222-
val job = launch(coroutineContext) {
223-
try {
224-
expect(2)
225-
suspendCancellableCoroutine<Unit> { c ->
226-
continuation = c
227-
}
228-
} catch (e: IOException) {
229-
expect(3)
230-
}
231-
}
232-
233-
expect(1)
234-
yield()
235-
continuation!!.resumeWithException(IOException())
236-
yield()
237-
assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(ClosedChannelException()) }
238-
try {
239-
job.join()
240-
} finally {
241-
finish(4)
242-
}
243-
}
244-
245-
@Test
246-
fun testResumeAndCancel() = runTest {
247-
var continuation: Continuation<Unit>? = null
248-
249-
val job = launch(coroutineContext) {
250-
expect(2)
251-
suspendCancellableCoroutine<Unit> { c ->
252-
continuation = c
253-
}
254-
expect(3)
255-
}
256-
257-
expect(1)
258-
yield()
259-
continuation!!.resume(Unit)
260-
job.join()
261-
assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(ClosedChannelException()) }
262-
finish(4)
263-
}
264-
265218
private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
266219
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
267220
return object : CoroutineDispatcher() {

0 commit comments

Comments
 (0)