Skip to content

Improve coroutine exception handling logic #1199

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 2 commits into from
May 24, 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
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,11 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin
public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
public final fun getCancellationException ()Ljava/util/concurrent/CancellationException;
protected fun getCancelsParent ()Z
public fun getChildJobCancellationCause ()Ljava/lang/Throwable;
public fun getChildJobCancellationCause ()Ljava/util/concurrent/CancellationException;
public final fun getChildren ()Lkotlin/sequences/Sequence;
protected final fun getCompletionCause ()Ljava/lang/Throwable;
protected final fun getCompletionCauseHandled ()Z
public final fun getCompletionExceptionOrNull ()Ljava/lang/Throwable;
protected fun getHandlesException ()Z
public final fun getKey ()Lkotlin/coroutines/CoroutineContext$Key;
public final fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0;
protected fun handleJobException (Ljava/lang/Throwable;)Z
Expand Down Expand Up @@ -447,7 +446,7 @@ public abstract interface annotation class kotlinx/coroutines/ObsoleteCoroutines
}

public abstract interface class kotlinx/coroutines/ParentJob : kotlinx/coroutines/Job {
public abstract fun getChildJobCancellationCause ()Ljava/lang/Throwable;
public abstract fun getChildJobCancellationCause ()Ljava/util/concurrent/CancellationException;
}

public final class kotlinx/coroutines/ParentJob$DefaultImpls {
Expand Down
1 change: 1 addition & 0 deletions kotlinx-coroutines-core/common/src/CompletableJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public interface CompletableJob : Job {
/**
* Completes this job exceptionally with a given [exception]. The result is `true` if this job was
* completed as a result of this invocation and `false` otherwise (if it was already completed).
* [exception] parameter is used as an additional debug information that is not handled by any exception handlers.
*
* Subsequent invocations of this function have no effect and always produce `false`.
*
Expand Down
5 changes: 4 additions & 1 deletion kotlinx-coroutines-core/common/src/Job.kt
Original file line number Diff line number Diff line change
Expand Up @@ -422,10 +422,13 @@ public interface ParentJob : Job {
* Child job is using this method to learn its cancellation cause when the parent cancels it with [ChildJob.parentCancelled].
* This method is invoked only if the child was not already being cancelled.
*
* Note that [CancellationException] is the method's return type: if child is cancelled by its parent,
* then the original exception is **already** handled by either the parent or the original source of failure.
*
* @suppress **This is unstable API and it is subject to change.**
*/
@InternalCoroutinesApi
public fun getChildJobCancellationCause(): Throwable
public fun getChildJobCancellationCause(): CancellationException
}

/**
Expand Down
50 changes: 31 additions & 19 deletions kotlinx-coroutines-core/common/src/JobSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.js.*
import kotlin.jvm.*

/**
Expand Down Expand Up @@ -127,7 +128,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
private val _state = atomic<Any?>(if (active) EMPTY_ACTIVE else EMPTY_NEW)

@Volatile
private var parentHandle: ChildHandle? = null
@JvmField
internal var parentHandle: ChildHandle? = null

// ------------ initialization ------------

Expand Down Expand Up @@ -635,29 +637,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
private fun createJobCancellationException() =
JobCancellationException("Job was cancelled", null, this)

override fun getChildJobCancellationCause(): Throwable {
override fun getChildJobCancellationCause(): CancellationException {
// determine root cancellation cause of this job (why is it cancelling its children?)
val state = this.state
val rootCause = when (state) {
is Finishing -> state.rootCause
is Incomplete -> error("Cannot be cancelling child in this state: $state")
is CompletedExceptionally -> state.cause
is Incomplete -> error("Cannot be cancelling child in this state: $state")
else -> null // create exception with the below code on normal completion
}
/*
* If this parent job handles exceptions, then wrap cause into JobCancellationException, because we
* don't want the child to handle this exception on more time. Otherwise, pass our original rootCause
* to the child for cancellation.
*/
return if (rootCause == null || handlesException && rootCause !is CancellationException) {
JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
} else {
rootCause
}
return (rootCause as? CancellationException) ?: JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
}

// cause is Throwable or ParentJob when cancelChild was invoked
private fun createCauseException(cause: Any?): Throwable = when(cause) {
private fun createCauseException(cause: Any?): Throwable = when (cause) {
is Throwable? -> cause ?: createJobCancellationException()
else -> (cause as ParentJob).getChildJobCancellationCause()
}
Expand Down Expand Up @@ -924,13 +917,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
protected open val cancelsParent: Boolean get() = true

/**
* Returns `true` for jobs that handle their exceptions or integrate them
* into the job's result via [onCompletionInternal]. The only instance of the [Job] that does not
* handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl].
* Returns `true` for jobs that handle their exceptions or integrate them into the job's result via [onCompletionInternal].
* A valid implementation of this getter should recursively check parent as well before returning `false`.
*
* The only instance of the [Job] that does not handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl].
* @suppress **This is unstable API and it is subject to change.*
*/
protected open val handlesException: Boolean get() = true
internal open val handlesException: Boolean get() = true

/**
* Handles the final job [exception] that was not handled by the parent coroutine.
Expand Down Expand Up @@ -1231,10 +1224,29 @@ private class Empty(override val isActive: Boolean) : Incomplete {
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
init { initParentJobInternal(parent) }
override val onCancelComplete get() = true
override val handlesException: Boolean get() = false
/*
* Check whether parent is able to handle exceptions as well.
* With this check, an exception in that pattern will be handled once:
* ```
* launch {
* val child = Job(coroutineContext[Job])
* launch(child) { throw ... }
* }
* ```
*/
override val handlesException: Boolean = handlesException()
override fun complete() = makeCompleting(Unit)
override fun completeExceptionally(exception: Throwable): Boolean =
makeCompleting(CompletedExceptionally(exception))

@JsName("handlesExceptionF")
private fun handlesException(): Boolean {
var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false
while (true) {
if (parentJob.handlesException) return true
parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false
}
}
}

// -------- invokeOnCompletion nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlin.test.*
class ParentCancellationTest : TestBase() {
@Test
fun testJobChild() = runTest {
testParentCancellation(expectUnhandled = true) { fail ->
testParentCancellation(expectUnhandled = false) { fail ->
val child = Job(coroutineContext[Job])
CoroutineScope(coroutineContext + child).fail()
}
Expand Down
4 changes: 3 additions & 1 deletion kotlinx-coroutines-core/common/test/SupervisorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ class SupervisorTest : TestBase() {
try {
deferred.await()
expectUnreached()
} catch (e: TestException1) {
} catch (e: CancellationException) {
val cause = if (RECOVER_STACK_TRACES) e.cause?.cause!! else e.cause
assertTrue(cause is TestException1)
finish(3)
}
}
Expand Down
4 changes: 2 additions & 2 deletions kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CapturingHandler : AbstractCoroutineContextElement(CoroutineExceptionHandl
}
}

internal fun runBlock(
internal fun captureExceptionsRun(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
): Throwable {
Expand All @@ -63,7 +63,7 @@ internal fun runBlock(
return handler.getException()
}

internal fun runBlockForMultipleExceptions(
internal fun captureMultipleExceptionsRun(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
): List<Throwable> {
Expand Down
Loading