Skip to content

Make withContext cancellable on return (instead of atomically cancell… #1192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions kotlinx-coroutines-core/common/src/Builders.common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public suspend fun <T> withContext(
}
}
// SLOW PATH -- use new dispatcher
val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_ATOMIC_DEFAULT
val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_CANCELLABLE
coroutine.initParentJob()
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
Expand Down Expand Up @@ -215,7 +215,7 @@ private class DispatchedCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override val defaultResumeMode: Int get() = MODE_ATOMIC_DEFAULT
override val defaultResumeMode: Int get() = MODE_CANCELLABLE

// this is copy-and-paste of a decision state machine inside AbstractionContinuation
// todo: we may some-how abstract it via inline class
Expand Down
23 changes: 13 additions & 10 deletions kotlinx-coroutines-core/common/src/Dispatched.kt
Original file line number Diff line number Diff line change
Expand Up @@ -218,32 +218,35 @@ internal abstract class DispatchedTask<in T>(

public final override fun run() {
val taskContext = this.taskContext
var exception: Throwable? = null
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
val context = continuation.context
val job = if (resumeMode.isCancellableMode) context[Job] else null
val state = takeState() // NOTE: Must take state in any case, even if cancelled
withCoroutineContext(context, delegate.countOrElement) {
if (job != null && !job.isActive) {
val exception = getExceptionalResult(state)
val job = if (resumeMode.isCancellableMode) context[Job] else null
/*
* Check whether continuation was originally resumed with an exception.
* If so, it dominates cancellation, otherwise the original exception
* will be silently lost.
*/
if (exception == null && job != null && !job.isActive) {
val cause = job.getCancellationException()
cancelResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
val exception = getExceptionalResult(state)
if (exception != null)
continuation.resumeWithStackTrace(exception)
else
continuation.resume(getSuccessfulResult(state))
if (exception != null) continuation.resumeWithStackTrace(exception)
else continuation.resume(getSuccessfulResult(state))
}
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
exception = e
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(exception, result.exceptionOrNull())
handleFatalException(fatalException, result.exceptionOrNull())
}
}

Expand Down
2 changes: 2 additions & 0 deletions kotlinx-coroutines-core/common/test/TestBase.common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
}
}

public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext)

30 changes: 25 additions & 5 deletions kotlinx-coroutines-core/common/test/WithContextTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class WithContextTest : TestBase() {
fun testThrowException() = runTest {
expect(1)
try {
withContext(coroutineContext) {
withContext<Unit>(coroutineContext) {
expect(2)
throw AssertionError()
}
Expand All @@ -31,7 +31,7 @@ class WithContextTest : TestBase() {
fun testThrowExceptionFromWrappedContext() = runTest {
expect(1)
try {
withContext(wrapperDispatcher(coroutineContext)) {
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
expect(2)
throw AssertionError()
}
Expand Down Expand Up @@ -151,7 +151,7 @@ class WithContextTest : TestBase() {
expect(2)
try {
// Same dispatcher, different context
withContext(CoroutineName("testRunCancellationUndispatchedVsException")) {
withContext<Unit>(CoroutineName("testRunCancellationUndispatchedVsException")) {
expect(3)
yield() // must suspend
expect(5)
Expand All @@ -176,7 +176,7 @@ class WithContextTest : TestBase() {
expect(2)
try {
// "Different" dispatcher (schedules execution back and forth)
withContext(wrapperDispatcher(coroutineContext)) {
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
expect(4)
yield() // must suspend
expect(6)
Expand Down Expand Up @@ -204,7 +204,7 @@ class WithContextTest : TestBase() {
job = launch(Job()) {
try {
expect(3)
withContext(wrapperDispatcher(coroutineContext)) {
withContext<Unit>(wrapperDispatcher(coroutineContext)) {
require(isActive)
expect(5)
job!!.cancel()
Expand Down Expand Up @@ -349,6 +349,26 @@ class WithContextTest : TestBase() {
expectUnreached()
}

@Test
fun testSequentialCancellation() = runTest {
val job = launch {
expect(1)
withContext(wrapperDispatcher()) {
expect(2)
}
expectUnreached()
}

yield()
val job2 = launch {
expect(3)
job.cancel()
}

joinAll(job, job2)
finish(4)
}

private class Wrapper(val value: String) : Incomplete {
override val isActive: Boolean
get() = error("")
Expand Down