Exception transparency in job.cancel (original cause is rethrown)
Clarified possible states for Job/CancellableContinuation/Deferred/LazyDeferred in docs
Deferred.isCompletedExceptionally and isCancelled are introduced.
Job.getInactiveCancellationException is renamed to getCompletionException
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
index a9482a4..15e7a72 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
@@ -89,7 +89,7 @@
) : AbstractCoroutine<Unit>(parentContext) {
override fun afterCompletion(state: Any?) {
// note the use of the parent's job context below!
- if (state is CompletedExceptionally) handleCoroutineException(parentContext, state.cancelCause)
+ if (state is CompletedExceptionally) handleCoroutineException(parentContext, state.exception)
}
}
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
index f5c536e..53a0940 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
@@ -26,14 +26,24 @@
// --------------- cancellable continuations ---------------
/**
- * Cancellable continuation. Its job is completed when it is resumed or cancelled.
- * When [cancel] function is explicitly invoked, this continuation resumes with [CancellationException].
- * If the cancel reason was not a [CancellationException], then the original exception is added as cause of the
- * [CancellationException] that this continuation resumes with.
+ * Cancellable continuation. Its job is _completed_ when it is resumed or cancelled.
+ * When [cancel] function is explicitly invoked, this continuation resumes with [CancellationException] or
+ * with the specified cancel cause.
+ *
+ * Cancellable continuation has three states:
+ * * _Active_ (initial state) -- [isActive] `true`, [isCancelled] `false`.
+ * * _Resumed_ (final _completed_ state) -- [isActive] `false`, [isCancelled] `false`.
+ * * _Canceled_ (final _completed_ state) -- [isActive] `false`, [isCancelled] `true`.
+ *
+ * Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while
+ * invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state.
+ *
+ * Invocation of [resume] or [resumeWithException] in _resumed_ state produces [IllegalStateException]
+ * but is ignored in _cancelled_ state.
*/
public interface CancellableContinuation<in T> : Continuation<T>, Job {
/**
- * Returns `true` if this continuation was cancelled. It implies that [isActive] is `false`.
+ * Returns `true` if this continuation was [cancelled][cancel]. It implies that [isActive] is `false`.
*/
val isCancelled: Boolean
@@ -87,7 +97,7 @@
internal fun getParentJobOrAbort(cont: Continuation<*>): Job? {
val job = cont.context[Job]
// fast path when parent job is already complete (we don't even construct SafeCancellableContinuation object)
- if (job != null && !job.isActive) throw job.getInactiveCancellationException()
+ if (job != null && !job.isActive) throw job.getCompletionException()
return job
}
@@ -144,7 +154,7 @@
while (true) { // lock-free loop on state
val state = getState() // atomic read
when (state) {
- is Active -> if (tryUpdateState(state, Failed(exception))) return state
+ is Active -> if (tryUpdateState(state, CompletedExceptionally(exception))) return state
else -> return null // cannot resume -- not active anymore
}
}
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineDispatcher.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineDispatcher.kt
index 196793b..3f510bc 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineDispatcher.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineDispatcher.kt
@@ -101,7 +101,7 @@
dispatcher.dispatch(context, Runnable {
withCoroutineContext(context) {
if (job?.isActive == false)
- continuation.resumeWithException(job.getInactiveCancellationException())
+ continuation.resumeWithException(job.getCompletionException())
else
continuation.resume(value)
}
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineScope.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineScope.kt
index 1b41704..8ac8d54 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineScope.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineScope.kt
@@ -59,7 +59,7 @@
while (true) { // lock-free loop on state
val state = getState() // atomic read
when (state) {
- is Active -> if (updateState(state, Failed(exception))) return
+ is Active -> if (updateState(state, CompletedExceptionally(exception))) return
is Cancelled -> {
// ignore resumes on cancelled continuation, but handle exception if a different one is here
if (exception != state.exception) handleCoroutineException(context, exception)
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt
index c9c3582..ab61173 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt
@@ -23,9 +23,34 @@
* Deferred value is conceptually a non-blocking cancellable future.
* It is created with [defer] coroutine builder.
* It is in [active][isActive] state while the value is being computed.
+ *
+ * Deferred value has four states:
+ *
+ * * _Active_ (initial state) -- [isActive] `true`, [isCompletedExceptionally] `false`,
+ * and [isCancelled] `false`.
+ * Both [getCompleted] and [getCompletionException] throw [IllegalStateException].
+ * * _Computed_ (final _completed_ state) -- [isActive] `false`,
+ * [isCompletedExceptionally] `false`, [isCancelled] `false`.
+ * * _Failed_ (final _completed_ state) -- [isActive] `false`,
+ * [isCompletedExceptionally] `true`, [isCancelled] `false`.
+ * * _Canceled_ (final _completed_ state) -- [isActive] `false`,
+ * [isCompletedExceptionally] `true`, [isCancelled] `true`.
*/
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`.
+ */
+ val isCompletedExceptionally: Boolean
+
+ /**
+ * Returns `true` if computation of this deferred value was [cancelled][cancel].
+ * It implies that [isActive] is `false` and [isCompletedExceptionally] is `true`.
+ */
+ val isCancelled: Boolean
+
+ /**
* Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete.
* This suspending function is cancellable.
* If the [Job] of the current coroutine is completed while this suspending function is waiting, this function
@@ -62,6 +87,9 @@
) : AbstractCoroutine<T>(context), Deferred<T> {
protected open fun start(): Boolean = false // LazyDeferredCoroutine overrides
+ override val isCompletedExceptionally: Boolean get() = getState() is CompletedExceptionally
+ override val isCancelled: Boolean get() = getState() is Cancelled
+
@Suppress("UNCHECKED_CAST")
suspend override fun await(): T {
// quick check if already complete (avoid extra object creation)
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt
index cfea2f2..39e0d60 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt
@@ -27,17 +27,23 @@
// --------------- core job interfaces ---------------
/**
- * A background job. It has two states: _active_ (initial state) and _completed_ (final state).
+ * A background job.
+ * A job can be _cancelled_ at any time with [cancel] function that forces it to become _completed_ immediately.
*
- * A job can be _cancelled_ at any time with [cancel] function that forces it to become completed immediately.
+ * It has two states:
+ * * _Active_ (initial state) -- [isActive] `true`,
+ * [getCompletionException] throws [IllegalStateException].
+ * * _Completed_ (final state) -- [isActive] `false`.
+ *
* A job in the coroutine [context][CoroutineScope.context] represents the coroutine itself.
* A job is active while the coroutine is working and job's cancellation aborts the coroutine when
* the coroutine is suspended on a _cancellable_ suspension point by throwing [CancellationException]
- * inside the coroutine.
+ * or the cancellation cause inside the coroutine.
*
* A job can have a _parent_. A job with a parent is cancelled when its parent completes.
*
- * All functions on this interface are thread-safe.
+ * 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 Job : CoroutineContext.Element {
/**
@@ -56,17 +62,23 @@
public val isActive: Boolean
/**
- * Returns [CancellationException] that [cancellable][suspendCancellableCoroutine] suspending functions throw when
- * trying to suspend in the context of this job. This function throws [IllegalAccessException] when invoked
- * for an [active][isActive] job.
+ * Returns the exception that signals the completion of this job -- it returns the original
+ * [cancel] cause or an instance of [CancellationException] if this job had completed
+ * normally or was cancelled without a cause. This function throws
+ * [IllegalStateException] when invoked for an [active][isActive] job.
+ *
+ * The [cancellable][suspendCancellableCoroutine] suspending functions throw this exception
+ * when trying to suspend in the context of this job.
*/
- fun getInactiveCancellationException(): CancellationException
+ fun getCompletionException(): Throwable
/**
* Registers completion handler. The action depends on the state of this job.
* When job is cancelled with [cancel], then the handler is immediately invoked
- * with a cancellation reason. Otherwise, handler will be invoked once when this
- * job is complete (cancellation also is a form of completion).
+ * with a cancellation cause or with a fresh [CancellationException].
+ * Otherwise, handler will be invoked once when this job is complete
+ * (cancellation also is a form of completion).
+ *
* The resulting [Registration] can be used to [Registration.unregister] if this
* registration is no longer needed. There is no need to unregister after completion.
*/
@@ -75,8 +87,8 @@
/**
* Cancel this activity with an optional cancellation [cause]. The result is `true` if this job was
* cancelled as a result of this invocation and `false` otherwise
- * (if it was already cancelled or it is [NonCancellable]).
- * Repeated invocation of this function has no effect and always produces `false`.
+ * (if it was already _completed_ or if it is [NonCancellable]).
+ * Repeated invocations of this function have no effect and always produce `false`.
*
* When cancellation has a clear reason in the code, an instance of [CancellationException] should be created
* at the corresponding original cancellation site and passed into this method to aid in debugging by providing
@@ -247,19 +259,19 @@
fun completeUpdateState(expect: Any, update: Any?) {
// #3. Invoke completion handlers
- val reason = (update as? CompletedExceptionally)?.cancelCause
+ val cause = (update as? CompletedExceptionally)?.exception
var completionException: Throwable? = null
when (expect) {
// SINGLE/SINGLE+ state -- one completion handler (common case)
is JobNode -> try {
- expect.invoke(reason)
+ expect.invoke(cause)
} catch (ex: Throwable) {
completionException = ex
}
// LIST state -- a list of completion handlers
is NodeList -> expect.forEach<JobNode> { node ->
try {
- node.invoke(reason)
+ node.invoke(cause)
} catch (ex: Throwable) {
completionException?.apply { addSuppressed(ex) } ?: run { completionException = ex }
}
@@ -275,12 +287,12 @@
final override val isActive: Boolean get() = state is Active
- override fun getInactiveCancellationException(): CancellationException {
+ override fun getCompletionException(): Throwable {
val state = getState()
return when (state) {
is Active -> throw IllegalStateException("Job is still active")
- is CompletedExceptionally -> state.cancellationException
- else -> CancellationException("Job has completed with result")
+ is CompletedExceptionally -> state.exception
+ else -> CancellationException("Job has completed normally")
}
}
@@ -311,7 +323,7 @@
}
// is not active anymore
else -> {
- handler((state as? Cancelled)?.cancelCause)
+ handler((state as? CompletedExceptionally)?.exception)
return EmptyRegistration
}
}
@@ -379,49 +391,40 @@
*/
internal interface Active
- private object Empty : Active
+ private object Empty : Active {
+ override fun toString(): String = "Empty"
+ }
- private class NodeList : LockFreeLinkedListHead(), Active
+ private class NodeList : LockFreeLinkedListHead(), Active {
+ override fun toString(): String = buildString {
+ append("[")
+ var first = true
+ this@NodeList.forEach<JobNode> { node ->
+ if (first) first = false else append(", ")
+ append(node)
+ }
+ append("]")
+ }
+ }
/**
- * Abstract class for a [state][getState] of a job that had completed exceptionally, including cancellation.
+ * Class for a [state][getState] of a job that had completed exceptionally, including cancellation.
*/
- internal abstract class CompletedExceptionally {
- abstract val cancelCause: Throwable // original reason or fresh CancellationException
- abstract val exception: Throwable // the exception to be thrown in continuation
-
- // convert cancelCause to CancellationException on first need
+ internal open class CompletedExceptionally(cause: Throwable?) {
@Volatile
- private var _cancellationException: CancellationException? = null
+ private var _exception: Throwable? = cause // materialize CancellationException on first need
- val cancellationException: CancellationException get() =
- _cancellationException ?: // atomic read volatile var or else build new
- (cancelCause as? CancellationException ?:
- CancellationException(cancelCause.message)
- .apply { initCause(cancelCause) })
- .also { _cancellationException = it }
+ val exception: Throwable get() =
+ _exception ?: // atomic read volatile var or else create new
+ CancellationException("Job was cancelled").also { _exception = it }
+
+ override fun toString(): String = "${javaClass.simpleName}[$exception]"
}
/**
- * Represents a [state][getState] of a cancelled job.
+ * A specific subclass of [CompletedExceptionally] for cancelled jobs.
*/
- internal class Cancelled(specifiedCause: Throwable?) : CompletedExceptionally() {
- @Volatile
- private var _cancelCause = specifiedCause // materialize CancellationException on first need
-
- override val cancelCause: Throwable get() =
- _cancelCause ?: // atomic read volatile var or else create new
- CancellationException("Job was cancelled").also { _cancelCause = it }
-
- override val exception: Throwable get() = cancellationException
- }
-
- /**
- * Represents a [state][getState] of a failed job.
- */
- internal class Failed(override val exception: Throwable) : CompletedExceptionally() {
- override val cancelCause: Throwable get() = exception
- }
+ internal class Cancelled(cause: Throwable?) : CompletedExceptionally(cause)
}
internal abstract class JobNode(
@@ -438,7 +441,7 @@
val handler: CompletionHandler
) : JobNode(job) {
override fun invoke(reason: Throwable?) = handler.invoke(reason)
- override fun toString() = "InvokeOnCompletion[${handler::class.java.name}@${Integer.toHexString(System.identityHashCode(handler))}]"
+ override fun toString() = "InvokeOnCompletion[${handler.javaClass.name}@${Integer.toHexString(System.identityHashCode(handler))}]"
}
private class ResumeOnCompletion(
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/LazyDeferred.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/LazyDeferred.kt
index 420765c..68cc0a5 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/LazyDeferred.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/LazyDeferred.kt
@@ -25,10 +25,20 @@
* the first [await] or [start] invocation.
* It is created with [lazyDefer] coroutine builder.
*
- * Unlike a simple [Deferred] value, a lazy deferred value has three states:
- * * _Pending_ -- before the starts of the coroutine ([isActive] is `true`, but [isComputing] is `false`).
- * * _Computing_ -- while computing the value ([isActive] is `true` and [isComputing] is `true`).
- * * _Complete_ -- when done computing the value ([isActive] is `false` and [isComputing] is `false`).
+ * Unlike a simple [Deferred] value, a lazy deferred value has five states:
+ *
+ * * _Pending_ (initial, _active_ state before the starts of the coroutine) --
+ * [isActive] `true`, but [isComputing] `false`,
+ * [isCompletedExceptionally] `false`, and [isCancelled] `false`.
+ * * _Computing_ (intermediate state while computing the value) --
+ * [isActive] `true`, [isComputing] `true`,
+ * [isCompletedExceptionally] `false`, and [isCancelled] `false`.
+ * * _Computed_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
+ * [isCompletedExceptionally] `false`, [isCancelled] `false`.
+ * * _Failed_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
+ * [isCompletedExceptionally] `true`, [isCancelled] `false`.
+ * * _Canceled_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
+ * [isCompletedExceptionally] `true`, [isCancelled] `true`.
*
* If this lazy deferred value is [cancelled][cancel], then it becomes immediately complete and
* cancels ongoing computation coroutine if it was started.
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt
index 3b9a152..e85b9c7 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt
@@ -21,7 +21,9 @@
/**
* A non-cancelable job that is always [active][isActive]. It is designed to be used with [run] builder
- * to prevent cancellation of code blocks that need to run without cancellation, like this
+ * to prevent cancellation of code blocks that need to run without cancellation.
+ *
+ * Use it like this:
* ```
* run(NonCancellable) {
* // this code will not be cancelled
@@ -30,7 +32,7 @@
*/
object NonCancellable : AbstractCoroutineContextElement(Job), Job {
override val isActive: Boolean get() = true
- override fun getInactiveCancellationException(): CancellationException = throw IllegalStateException("This job is always active")
+ override fun getCompletionException(): CancellationException = throw IllegalStateException("This job is always active")
override fun onCompletion(handler: CompletionHandler): Job.Registration = EmptyRegistration
override fun cancel(cause: Throwable?): Boolean = false
}
diff --git a/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt b/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt
index 8fb4fdf..ffa8ffc 100644
--- a/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt
+++ b/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt
@@ -139,7 +139,7 @@
throw IOException()
}
- @Test(expected = CancellationException::class)
+ @Test(expected = IOException::class)
fun testCancelParentOnChildException(): Unit = runBlocking {
expect(1)
launch(context) {
@@ -151,7 +151,7 @@
expectUnreached() // because of exception in child
}
- @Test(expected = CancellationException::class)
+ @Test(expected = IOException::class)
fun testCancelParentOnNestedException(): Unit = runBlocking {
expect(1)
launch(context) {