Skip to content

Handle exceptions fix #588

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 20 commits into from
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ecbc85c
Fix exception aggregation to ensure atomic handling of exceptions
elizarov Sep 14, 2018
7e23875
Fixed race between creation of a child & cancellation of parent job
elizarov Sep 20, 2018
6634ed7
Minor optimization in fast-path of JobSupport.tryMakeCompleting
elizarov Sep 20, 2018
ff0aab8
Document JobSupport state-transition & notification time-line
elizarov Sep 22, 2018
5d18d02
Introduced ChildHandle for attachChild
elizarov Sep 22, 2018
33e7ce4
Removed "cancel parent" from TestBase exception handlers
elizarov Sep 23, 2018
f157cec
Job machinery improvements:
qwwdfsad Sep 24, 2018
409ed26
Handle exception thrown by user-defined exception handler via global …
qwwdfsad Sep 24, 2018
f13cbae
Get rid of references to deprecated AbstractCoroutine.onCancellation
elizarov Sep 24, 2018
35f082e
Update API dump for protected onFailing
elizarov Sep 24, 2018
2d8cad0
Docs for context/coroutineContext in AbstractCoroutine are fixed
elizarov Sep 24, 2018
167c451
Removed private handleExceptionViaJob (not needed and bad name)
elizarov Sep 24, 2018
f45ec8d
Optimize Empty -> Failing state transition
elizarov Sep 24, 2018
ad84795
Slightly faster update to finishing state on completion
elizarov Sep 24, 2018
f7a6334
Child fails on parent failure (not cancelled)
elizarov Sep 24, 2018
6685fd0
Abolish distinction between cancelled and failed Job/Deferred
elizarov Sep 25, 2018
1e0a2f0
Merge branch 'develop' into handle-exception-fix
elizarov Sep 25, 2018
1b22af7
Fix lost exception during cancellation in produce, fix integration wi…
qwwdfsad Sep 25, 2018
852fae5
Reviewed and cleanup usage of "fail" word in comments and method names
elizarov Sep 25, 2018
541a9b6
Job unwraps all instances of CancellationException
elizarov Sep 25, 2018
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
@@ -1,4 +1,5 @@
public abstract class kotlinx/coroutines/experimental/AbstractCoroutine : kotlin/coroutines/experimental/Continuation, kotlinx/coroutines/experimental/CoroutineScope, kotlinx/coroutines/experimental/Job {
protected final field parentContext Lkotlin/coroutines/experimental/CoroutineContext;
public fun <init> (Lkotlin/coroutines/experimental/CoroutineContext;Z)V
public synthetic fun <init> (Lkotlin/coroutines/experimental/CoroutineContext;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getContext ()Lkotlin/coroutines/experimental/CoroutineContext;
Expand Down Expand Up @@ -89,6 +90,10 @@ public final class kotlinx/coroutines/experimental/CancelledContinuation : kotli
public fun <init> (Lkotlin/coroutines/experimental/Continuation;Ljava/lang/Throwable;)V
}

public abstract interface class kotlinx/coroutines/experimental/ChildHandle : kotlinx/coroutines/experimental/DisposableHandle {
public abstract fun childFailed (Ljava/lang/Throwable;)Z
}

public abstract class kotlinx/coroutines/experimental/CloseableCoroutineDispatcher : kotlinx/coroutines/experimental/CoroutineDispatcher, java/io/Closeable {
public fun <init> ()V
}
Expand Down Expand Up @@ -367,8 +372,9 @@ public final class kotlinx/coroutines/experimental/GlobalScope : kotlinx/corouti

public abstract interface class kotlinx/coroutines/experimental/Job : kotlin/coroutines/experimental/CoroutineContext$Element {
public static final field Key Lkotlinx/coroutines/experimental/Job$Key;
public abstract fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/DisposableHandle;
public abstract fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/ChildHandle;
public abstract fun cancel (Ljava/lang/Throwable;)Z
public abstract fun cancelChild (Lkotlinx/coroutines/experimental/Job;)V
public abstract synthetic fun cancelChildren (Ljava/lang/Throwable;)V
public abstract fun getCancellationException ()Ljava/util/concurrent/CancellationException;
public abstract fun getChildren ()Lkotlin/sequences/Sequence;
Expand All @@ -381,6 +387,7 @@ public abstract interface class kotlinx/coroutines/experimental/Job : kotlin/cor
public abstract fun isActive ()Z
public abstract fun isCancelled ()Z
public abstract fun isCompleted ()Z
public abstract fun isFailed ()Z
public abstract fun join (Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
public abstract fun plus (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
public abstract fun start ()Z
Expand Down Expand Up @@ -438,8 +445,9 @@ public final class kotlinx/coroutines/experimental/LazyDeferredKt {

public final class kotlinx/coroutines/experimental/NonCancellable : kotlin/coroutines/experimental/AbstractCoroutineContextElement, kotlinx/coroutines/experimental/Job {
public static final field INSTANCE Lkotlinx/coroutines/experimental/NonCancellable;
public fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/DisposableHandle;
public fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/ChildHandle;
public fun cancel (Ljava/lang/Throwable;)Z
public fun cancelChild (Lkotlinx/coroutines/experimental/Job;)V
public synthetic fun cancelChildren (Ljava/lang/Throwable;)V
public fun getCancellationException ()Ljava/util/concurrent/CancellationException;
public fun getChildren ()Lkotlin/sequences/Sequence;
Expand All @@ -452,13 +460,15 @@ public final class kotlinx/coroutines/experimental/NonCancellable : kotlin/corou
public fun isActive ()Z
public fun isCancelled ()Z
public fun isCompleted ()Z
public fun isFailed ()Z
public fun join (Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
public fun plus (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
public fun start ()Z
}

public final class kotlinx/coroutines/experimental/NonDisposableHandle : kotlinx/coroutines/experimental/DisposableHandle {
public final class kotlinx/coroutines/experimental/NonDisposableHandle : kotlinx/coroutines/experimental/ChildHandle, kotlinx/coroutines/experimental/DisposableHandle {
public static final field INSTANCE Lkotlinx/coroutines/experimental/NonDisposableHandle;
public fun childFailed (Ljava/lang/Throwable;)Z
public fun dispose ()V
public fun toString ()Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ internal abstract class AbstractContinuation<in T>(
return
}
parent.start() // make sure the parent is started
val handle = parent.invokeOnCompletion(onCancelling = true,
val handle = parent.invokeOnCompletion(onFailing = true,
handler = ChildContinuation(parent, this).asHandler)

parentHandle = handle
Expand Down
27 changes: 19 additions & 8 deletions common/kotlinx-coroutines-core-common/src/AbstractCoroutine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.coroutines.experimental

import kotlinx.coroutines.experimental.CoroutineStart.*
import kotlinx.coroutines.experimental.internal.*
import kotlinx.coroutines.experimental.intrinsics.*
import kotlin.coroutines.experimental.*

Expand All @@ -30,7 +31,11 @@ import kotlin.coroutines.experimental.*
*/
@Suppress("EXPOSED_SUPER_CLASS")
public abstract class AbstractCoroutine<in T>(
private val parentContext: CoroutineContext,
/**
* Context of the parent coroutine.
*/
@JvmField
protected val parentContext: CoroutineContext,
active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
@Suppress("LeakingThis")
Expand Down Expand Up @@ -63,19 +68,25 @@ public abstract class AbstractCoroutine<in T>(
}

/**
* This function is invoked once when this coroutine is cancelled or is completed,
* similarly to [invokeOnCompletion] with `onCancelling` set to `true`.
* @suppress **Deprecated**: Override [onFailing].
*/
@Deprecated("Override onFailing")
protected open fun onCancellation(cause: Throwable?) {}

/**
* This function is invoked once when this coroutine is failing or is completed,
* similarly to [invokeOnCompletion] with `onFailing` set to `true`.
*
* The meaning of [cause] parameter:
* * Cause is `null` when job has completed normally.
* * Cause is an instance of [CancellationException] when job was cancelled _normally_.
* **It should not be treated as an error**. In particular, it should not be reported to error logs.
* * Otherwise, the job had _failed_.
*
* @suppress **Deprecated**: Override [onFailing].
*/
protected open fun onCancellation(cause: Throwable?) {}

internal override fun onCancellationInternal(exceptionally: CompletedExceptionally?) {
onCancellation(exceptionally?.cause)
override fun onFailing(cause: Throwable?) {
onCancellation(cause)
}

/**
Expand All @@ -89,7 +100,7 @@ public abstract class AbstractCoroutine<in T>(
protected open fun onCompletedExceptionally(exception: Throwable) {}

@Suppress("UNCHECKED_CAST")
internal override fun onCompletionInternal(state: Any?, mode: Int) {
internal override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) {
if (state is CompletedExceptionally)
onCompletedExceptionally(state.cause)
else
Expand Down
15 changes: 3 additions & 12 deletions common/kotlinx-coroutines-core-common/src/Builders.common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,11 @@ public suspend fun <T> run(context: CoroutineContext, block: suspend () -> T): T
// --------------- implementation ---------------

private open class StandaloneCoroutine(
private val parentContext: CoroutineContext,
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active) {
override fun hasOnFinishingHandler(update: Any?) = update is CompletedExceptionally

override fun handleJobException(exception: Throwable) {
handleCoroutineException(parentContext, exception, this)
}

override fun onFinishingInternal(update: Any?) {
if (update is CompletedExceptionally && update.cause !is CancellationException) {
parentContext[Job]?.cancel(update.cause)
}
}
override val failsParent: Boolean get() = true
override fun handleJobException(exception: Throwable) = handleExceptionViaHandler(parentContext, exception)
}

private class LazyStandaloneCoroutine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private class CompletableDeferredImpl<T>(
parent: Job?
) : JobSupport(true), CompletableDeferred<T>, SelectClause1<T> {
init { initParentJobInternal(parent) }
override val onCancelMode: Int get() = ON_CANCEL_MAKE_COMPLETING
override val onFailComplete get() = true
override fun getCompleted(): T = getCompletedInternal() as T
override suspend fun await(): T = awaitInternal() as T
override val onAwait: SelectClause1<T> get() = this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import kotlin.coroutines.experimental.*
* Class for an internal state of a job that had completed exceptionally, including cancellation.
*
* **Note: This class cannot be used outside of internal coroutines framework**.
* **Note: cannot be internal until we get rid of MutableDelegateContinuation in IO**
* **Note: cannot be internal and renamed until we get rid of MutableDelegateContinuation in IO**
*
* @param cause the exceptional completion cause. It's either original exceptional cause
* or artificial JobCancellationException if no cause was provided
* @suppress **This is unstable API and it is subject to change.**
*/
open class CompletedExceptionally(
Expand All @@ -28,14 +26,9 @@ open class CompletedExceptionally(
*
* **Note: This class cannot be used outside of internal coroutines framework**.
*
* @param job the job that was cancelled.
* @param cause the exceptional completion cause. If `cause` is null, then a [JobCancellationException] is created.
* @suppress **This is unstable API and it is subject to change.**
*/
internal class Cancelled(
job: Job,
cause: Throwable?
) : CompletedExceptionally(cause ?: JobCancellationException("Job was cancelled normally", null, job))
internal class Cancelled(cause: Throwable) : CompletedExceptionally(cause)

/**
* A specific subclass of [CompletedExceptionally] for cancelled [AbstractContinuation].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ internal expect val CancelHandlerBase.asHandler: CompletionHandler
// :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
// because we play type tricks on Kotlin/JS and handler is not necessarily a function there
internal expect fun CompletionHandler.invokeIt(cause: Throwable?)

internal inline fun <reified T> CompletionHandler.isHandlerOf(): Boolean = this is T
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,27 @@ internal expect fun handleCoroutineExceptionImpl(context: CoroutineContext, exce
* If invocation returned `true`, method terminates: now [Job] is responsible for handling an exception.
* Otherwise, If there is [CoroutineExceptionHandler] in the context, it is used.
* Otherwise all instances of [CoroutineExceptionHandler] found via [ServiceLoader] and [Thread.uncaughtExceptionHandler] are invoked
*
* todo: Deprecate/hide this function.
*/
@JvmOverloads // binary compatibility
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable, caller: Job? = null) {
// if exception handling fails, make sure the original exception is not lost
if (!handleExceptionViaJob(context, exception, caller)) {
handleExceptionViaHandler(context, exception)
}
}

private fun handleExceptionViaJob(context: CoroutineContext, exception: Throwable, caller: Job?): Boolean {
// Ignore CancellationException (they are normal ways to terminate a coroutine)
if (exception is CancellationException) return true
// If job is successfully cancelled, we're done
val job = context[Job]
return job !== null && job !== caller && job.cancel(exception)
}

internal fun handleExceptionViaHandler(context: CoroutineContext, exception: Throwable) {
try {
// Ignore CancellationException (they are normal ways to terminate a coroutine)
if (exception is CancellationException) {
return
}
// If parent is successfully cancelled, we're done, it is now its responsibility to handle the exception
val parent = context[Job]
// E.g. actor registers itself in the context, in that case we should invoke handler
if (parent !== null && parent !== caller && parent.cancel(exception)) {
return
}
// If not, invoke exception handler from the context
// Invoke exception handler from the context if present
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ object GlobalScope : CoroutineScope {
*/
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
// todo: optimize implementation to a single allocated object
val owner = ScopeOwnerCoroutine<R>(coroutineContext)
val owner = ScopeCoroutine<R>(coroutineContext)
owner.start(CoroutineStart.UNDISPATCHED, owner, block)
owner.join()
if (owner.isCancelled) {
Expand Down
54 changes: 10 additions & 44 deletions common/kotlinx-coroutines-core-common/src/Deferred.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,35 @@ import kotlinx.coroutines.experimental.selects.*
import kotlin.coroutines.experimental.*

/**
* Deferred value is a non-blocking cancellable future.
* Deferred value is a non-blocking cancellable future &mdash; it is a [Job] that has a result.
*
* It is created with [async][CoroutineScope.async] coroutine builder or via constructor of [CompletableDeferred] class.
* It is in [active][isActive] state while the value is being computed.
*
* Deferred value has the following states:
*
* | **State** | [isActive] | [isCompleted] | [isCompletedExceptionally] | [isCancelled] |
* | --------------------------------------- | ---------- | ------------- | -------------------------- | ------------- |
* | _New_ (optional initial state) | `false` | `false` | `false` | `false` |
* | _Active_ (default initial state) | `true` | `false` | `false` | `false` |
* | _Completing_ (optional transient state) | `true` | `false` | `false` | `false` |
* | _Cancelling_ (optional transient state) | `false` | `false` | `false` | `true` |
* | _Cancelled_ (final state) | `false` | `true` | `true` | `true` |
* | _Resolved_ (final state) | `false` | `true` | `false` | `false` |
* | _Failed_ (final state) | `false` | `true` | `true` | `false` |
* Deferred value has the same state machine as the [Job] with additional convenience methods to retrieve
* successful or failed result of the computation that was carried out. The result of the deferred is
* available when it is [completed][isCompleted] and can be retrieved by [await] method, which throws
* exception if the deferred had failed.
* A _failed_ deferred is considered to be [completed exceptionally][isCompletedExceptionally].
* The corresponding exception can be retrieved via [getCompletionExceptionOrNull] from a completed instance of deferred.
*
* Usually, a deferred value is created in _active_ state (it is created and started).
* However, [async][CoroutineScope.async] coroutine builder has an optional `start` parameter that creates a deferred value in _new_ state
* when this parameter is set to [CoroutineStart.LAZY].
* Such a deferred can be be made _active_ by invoking [start], [join], or [await].
*
* A deferred can be _cancelled_ at any time with [cancel] function that forces it to transition to
* _cancelling_ state immediately. Deferred that is not backed by a coroutine (see [CompletableDeferred]) and does not have
* [children] becomes _cancelled_ on [cancel] immediately.
* Otherwise, deferred becomes _cancelled_ when it finishes executing its code and
* when all its children [complete][isCompleted].
*
* ```
* wait children
* +-----+ start +--------+ complete +-------------+ finish +-----------+
* | New | ---------------> | Active | ----------> | Completing | ---+-> | Resolved |
* +-----+ +--------+ +-------------+ | |(completed)|
* | | | | +-----------+
* | cancel | cancel | cancel |
* V V | | +-----------+
* +-----------+ finish +------------+ | +-> | Failed |
* | Cancelled | <--------- | Cancelling | <---------------+ |(completed)|
* |(completed)| +------------+ +-----------+
* +-----------+
* ```
*
* A deferred value is a [Job]. A job in the
* [coroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/coroutine-context.html)
* of [async][CoroutineScope.async] builder represents the coroutine itself.
* A deferred value is active while the coroutine is working and cancellation aborts the coroutine when
* the coroutine is suspended on a _cancellable_ suspension point by throwing [CancellationException]
* or the cancellation cause inside the coroutine.
*
* A deferred value can have a _parent_ job. A deferred value with a parent is cancelled when its parent is
* cancelled or completes. Parent waits for all its [children] to complete in _completing_ or
* _cancelling_ state. _Completing_ state is purely internal. For an outside observer a _completing_
* deferred is still active, while internally it is waiting for its children.
*
* All functions on this interface and on all interfaces derived from it are **thread-safe** and can
* be safely invoked from concurrent coroutines without external synchronization.
*/
public interface Deferred<out T> : Job {
/**
* Returns `true` if computation of this deferred value has _completed exceptionally_ -- it had
* either _failed_ with exception during computation or was [cancelled][cancel].
*
* It implies that [isActive] is `false` and [isCompleted] is `true`.
* Returns `true` if computation of this deferred value has _completed exceptionally_.
* It is `true` when both [isCompleted] and [isFailed] are true.
* It implies that [isActive] is `false`.
*/
public val isCompletedExceptionally: Boolean

Expand Down
Loading