Skip to content

Commit c022ab6

Browse files
committed
Make withContext cancellable on return (instead of atomically cancellable).
* Reasoning about cancellation is simplified in sequential scenarios, if 'cancel' was invoked before withContext return it will throw an exception, thus "isActive == false" cannot be observed in sequential scenarios after cancellation * withContext now complies its own documentation Fixes #1177
1 parent 71fa64f commit c022ab6

File tree

4 files changed

+42
-17
lines changed

4 files changed

+42
-17
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public suspend fun <T> withContext(
156156
}
157157
}
158158
// SLOW PATH -- use new dispatcher
159-
val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_ATOMIC_DEFAULT
159+
val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_CANCELLABLE
160160
coroutine.initParentJob()
161161
block.startCoroutineCancellable(coroutine, coroutine)
162162
coroutine.getResult()
@@ -215,7 +215,7 @@ private class DispatchedCoroutine<in T>(
215215
context: CoroutineContext,
216216
uCont: Continuation<T>
217217
) : ScopeCoroutine<T>(context, uCont) {
218-
override val defaultResumeMode: Int get() = MODE_ATOMIC_DEFAULT
218+
override val defaultResumeMode: Int get() = MODE_CANCELLABLE
219219

220220
// this is copy-and-paste of a decision state machine inside AbstractionContinuation
221221
// todo: we may some-how abstract it via inline class

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

+13-10
Original file line numberDiff line numberDiff line change
@@ -218,32 +218,35 @@ internal abstract class DispatchedTask<in T>(
218218

219219
public final override fun run() {
220220
val taskContext = this.taskContext
221-
var exception: Throwable? = null
221+
var fatalException: Throwable? = null
222222
try {
223223
val delegate = delegate as DispatchedContinuation<T>
224224
val continuation = delegate.continuation
225225
val context = continuation.context
226-
val job = if (resumeMode.isCancellableMode) context[Job] else null
227226
val state = takeState() // NOTE: Must take state in any case, even if cancelled
228227
withCoroutineContext(context, delegate.countOrElement) {
229-
if (job != null && !job.isActive) {
228+
val exception = getExceptionalResult(state)
229+
val job = if (resumeMode.isCancellableMode) context[Job] else null
230+
/*
231+
* Check whether continuation was originally resumed with an exception.
232+
* If so, it dominates cancellation, otherwise the original exception
233+
* will be silently lost.
234+
*/
235+
if (exception == null && job != null && !job.isActive) {
230236
val cause = job.getCancellationException()
231237
cancelResult(state, cause)
232238
continuation.resumeWithStackTrace(cause)
233239
} else {
234-
val exception = getExceptionalResult(state)
235-
if (exception != null)
236-
continuation.resumeWithStackTrace(exception)
237-
else
238-
continuation.resume(getSuccessfulResult(state))
240+
if (exception != null) continuation.resumeWithStackTrace(exception)
241+
else continuation.resume(getSuccessfulResult(state))
239242
}
240243
}
241244
} catch (e: Throwable) {
242245
// This instead of runCatching to have nicer stacktrace and debug experience
243-
exception = e
246+
fatalException = e
244247
} finally {
245248
val result = runCatching { taskContext.afterTask() }
246-
handleFatalException(exception, result.exceptionOrNull())
249+
handleFatalException(fatalException, result.exceptionOrNull())
247250
}
248251
}
249252

kotlinx-coroutines-core/common/test/TestBase.common.kt

+2
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
7474
}
7575
}
7676

77+
public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext)
78+

kotlinx-coroutines-core/common/test/WithContextTest.kt

+25-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class WithContextTest : TestBase() {
1515
fun testThrowException() = runTest {
1616
expect(1)
1717
try {
18-
withContext(coroutineContext) {
18+
withContext<Unit>(coroutineContext) {
1919
expect(2)
2020
throw AssertionError()
2121
}
@@ -31,7 +31,7 @@ class WithContextTest : TestBase() {
3131
fun testThrowExceptionFromWrappedContext() = runTest {
3232
expect(1)
3333
try {
34-
withContext(wrapperDispatcher(coroutineContext)) {
34+
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
3535
expect(2)
3636
throw AssertionError()
3737
}
@@ -151,7 +151,7 @@ class WithContextTest : TestBase() {
151151
expect(2)
152152
try {
153153
// Same dispatcher, different context
154-
withContext(CoroutineName("testRunCancellationUndispatchedVsException")) {
154+
withContext<Unit>(CoroutineName("testRunCancellationUndispatchedVsException")) {
155155
expect(3)
156156
yield() // must suspend
157157
expect(5)
@@ -176,7 +176,7 @@ class WithContextTest : TestBase() {
176176
expect(2)
177177
try {
178178
// "Different" dispatcher (schedules execution back and forth)
179-
withContext(wrapperDispatcher(coroutineContext)) {
179+
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
180180
expect(4)
181181
yield() // must suspend
182182
expect(6)
@@ -204,7 +204,7 @@ class WithContextTest : TestBase() {
204204
job = launch(Job()) {
205205
try {
206206
expect(3)
207-
withContext(wrapperDispatcher(coroutineContext)) {
207+
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
208208
require(isActive)
209209
expect(5)
210210
job!!.cancel()
@@ -349,6 +349,26 @@ class WithContextTest : TestBase() {
349349
expectUnreached()
350350
}
351351

352+
@Test
353+
fun testSequentialCancellation() = runTest {
354+
val job = launch {
355+
expect(1)
356+
withContext(wrapperDispatcher()) {
357+
expect(2)
358+
}
359+
expectUnreached()
360+
}
361+
362+
yield()
363+
val job2 = launch {
364+
expect(3)
365+
job.cancel()
366+
}
367+
368+
joinAll(job, job2)
369+
finish(4)
370+
}
371+
352372
private class Wrapper(val value: String) : Incomplete {
353373
override val isActive: Boolean
354374
get() = error("")

0 commit comments

Comments
 (0)