Skip to content

Commit 4451d72

Browse files
committed
Make all invocations of withContext cancellable
* Immediately check is the new context is cancelled and throw CancellationException as needed * Properly explain cancellation behavior in documentation Fixes #962 Fixes #785
1 parent 7dd86b6 commit 4451d72

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,17 @@ private class LazyDeferredCoroutine<T>(
121121
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns
122122
* the result.
123123
*
124-
* This function immediately applies dispatcher from the new context, shifting execution of the block into the
125-
* different thread inside the block, and back when it completes.
126-
* The specified [context] is added onto the current coroutine context for the execution of the block.
124+
* The resulting context for the [block] is derived by merging the current [coroutineContext] with the
125+
* specified [context] using `coroutineContext + context` (see [CoroutineContext.plus]).
126+
* This suspending function is cancellable. It immediately checks for cancellation of
127+
* the resulting context and throws [CancellationException] if it is not [active][CoroutineContext.isActive].
128+
*
129+
* This function uses dispatcher from the new context, shifting execution of the [block] into the
130+
* different thread if a new dispatcher is specified, and back to the original dispatcher
131+
* when it completes. Note, that the result of `withContext` invocation is
132+
* dispatched into the original context in a cancellable way, which means that if the original [coroutineContext],
133+
* in which `withContext` was invoked, is cancelled by the time its dispatcher starts to execute the code,
134+
* it discards the result of `withContext` and throws [CancellationException].
127135
*/
128136
public suspend fun <T> withContext(
129137
context: CoroutineContext,
@@ -132,6 +140,8 @@ public suspend fun <T> withContext(
132140
// compute new context
133141
val oldContext = uCont.context
134142
val newContext = oldContext + context
143+
// always check for cancellation of new context
144+
newContext.checkCompletion()
135145
// FAST PATH #1 -- new context is the same as the old one
136146
if (newContext === oldContext) {
137147
val coroutine = ScopeCoroutine(newContext, uCont) // MODE_DIRECT

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

+26
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,32 @@ class WithContextTest : TestBase() {
325325
assertFalse(ctxJob.isCancelled)
326326
}
327327

328+
@Test
329+
fun testWithContextCancelledJob() = runTest {
330+
expect(1)
331+
val job = Job()
332+
job.cancel()
333+
try {
334+
withContext(job) {
335+
expectUnreached()
336+
}
337+
} catch (e: CancellationException) {
338+
expect(2)
339+
}
340+
finish(3)
341+
}
342+
343+
@Test
344+
fun testWithContextCancelledThisJob() = runTest(
345+
expected = { it is CancellationException }
346+
) {
347+
coroutineContext.cancel()
348+
withContext(wrapperDispatcher(coroutineContext)) {
349+
expectUnreached()
350+
}
351+
expectUnreached()
352+
}
353+
328354
private class Wrapper(val value: String) : Incomplete {
329355
override val isActive: Boolean
330356
get() = error("")

0 commit comments

Comments
 (0)