Skip to content

Commit b7c46de

Browse files
committed
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
1 parent 4d821e3 commit b7c46de

File tree

9 files changed

+122
-69
lines changed

9 files changed

+122
-69
lines changed

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private class StandaloneCoroutine(
8989
) : AbstractCoroutine<Unit>(parentContext) {
9090
override fun afterCompletion(state: Any?) {
9191
// note the use of the parent's job context below!
92-
if (state is CompletedExceptionally) handleCoroutineException(parentContext, state.cancelCause)
92+
if (state is CompletedExceptionally) handleCoroutineException(parentContext, state.exception)
9393
}
9494
}
9595

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,24 @@ import kotlin.coroutines.experimental.suspendCoroutine
2626
// --------------- cancellable continuations ---------------
2727

2828
/**
29-
* Cancellable continuation. Its job is completed when it is resumed or cancelled.
30-
* When [cancel] function is explicitly invoked, this continuation resumes with [CancellationException].
31-
* If the cancel reason was not a [CancellationException], then the original exception is added as cause of the
32-
* [CancellationException] that this continuation resumes with.
29+
* Cancellable continuation. Its job is _completed_ when it is resumed or cancelled.
30+
* When [cancel] function is explicitly invoked, this continuation resumes with [CancellationException] or
31+
* with the specified cancel cause.
32+
*
33+
* Cancellable continuation has three states:
34+
* * _Active_ (initial state) -- [isActive] `true`, [isCancelled] `false`.
35+
* * _Resumed_ (final _completed_ state) -- [isActive] `false`, [isCancelled] `false`.
36+
* * _Canceled_ (final _completed_ state) -- [isActive] `false`, [isCancelled] `true`.
37+
*
38+
* Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while
39+
* invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state.
40+
*
41+
* Invocation of [resume] or [resumeWithException] in _resumed_ state produces [IllegalStateException]
42+
* but is ignored in _cancelled_ state.
3343
*/
3444
public interface CancellableContinuation<in T> : Continuation<T>, Job {
3545
/**
36-
* Returns `true` if this continuation was cancelled. It implies that [isActive] is `false`.
46+
* Returns `true` if this continuation was [cancelled][cancel]. It implies that [isActive] is `false`.
3747
*/
3848
val isCancelled: Boolean
3949

@@ -87,7 +97,7 @@ public inline suspend fun <T> suspendCancellableCoroutine(
8797
internal fun getParentJobOrAbort(cont: Continuation<*>): Job? {
8898
val job = cont.context[Job]
8999
// fast path when parent job is already complete (we don't even construct SafeCancellableContinuation object)
90-
if (job != null && !job.isActive) throw job.getInactiveCancellationException()
100+
if (job != null && !job.isActive) throw job.getCompletionException()
91101
return job
92102
}
93103

@@ -144,7 +154,7 @@ internal class SafeCancellableContinuation<in T>(
144154
while (true) { // lock-free loop on state
145155
val state = getState() // atomic read
146156
when (state) {
147-
is Active -> if (tryUpdateState(state, Failed(exception))) return state
157+
is Active -> if (tryUpdateState(state, CompletedExceptionally(exception))) return state
148158
else -> return null // cannot resume -- not active anymore
149159
}
150160
}

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineDispatcher.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ internal class DispatchedContinuation<T>(
101101
dispatcher.dispatch(context, Runnable {
102102
withCoroutineContext(context) {
103103
if (job?.isActive == false)
104-
continuation.resumeWithException(job.getInactiveCancellationException())
104+
continuation.resumeWithException(job.getCompletionException())
105105
else
106106
continuation.resume(value)
107107
}

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineScope.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal abstract class AbstractCoroutine<in T>(context: CoroutineContext) : Job
5959
while (true) { // lock-free loop on state
6060
val state = getState() // atomic read
6161
when (state) {
62-
is Active -> if (updateState(state, Failed(exception))) return
62+
is Active -> if (updateState(state, CompletedExceptionally(exception))) return
6363
is Cancelled -> {
6464
// ignore resumes on cancelled continuation, but handle exception if a different one is here
6565
if (exception != state.exception) handleCoroutineException(context, exception)

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,33 @@ import kotlin.coroutines.experimental.startCoroutine
2323
* Deferred value is conceptually a non-blocking cancellable future.
2424
* It is created with [defer] coroutine builder.
2525
* It is in [active][isActive] state while the value is being computed.
26+
*
27+
* Deferred value has four states:
28+
*
29+
* * _Active_ (initial state) -- [isActive] `true`, [isCompletedExceptionally] `false`,
30+
* and [isCancelled] `false`.
31+
* Both [getCompleted] and [getCompletionException] throw [IllegalStateException].
32+
* * _Computed_ (final _completed_ state) -- [isActive] `false`,
33+
* [isCompletedExceptionally] `false`, [isCancelled] `false`.
34+
* * _Failed_ (final _completed_ state) -- [isActive] `false`,
35+
* [isCompletedExceptionally] `true`, [isCancelled] `false`.
36+
* * _Canceled_ (final _completed_ state) -- [isActive] `false`,
37+
* [isCompletedExceptionally] `true`, [isCancelled] `true`.
2638
*/
2739
public interface Deferred<out T> : Job {
40+
/**
41+
* Returns `true` if computation of this deferred value has _completed exceptionally_ -- it had
42+
* either _failed_ with exception during computation or was [cancelled][cancel].
43+
* It implies that [isActive] is `false`.
44+
*/
45+
val isCompletedExceptionally: Boolean
46+
47+
/**
48+
* Returns `true` if computation of this deferred value was [cancelled][cancel].
49+
* It implies that [isActive] is `false` and [isCompletedExceptionally] is `true`.
50+
*/
51+
val isCancelled: Boolean
52+
2853
/**
2954
* Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete.
3055
* This suspending function is cancellable.
@@ -62,6 +87,9 @@ internal open class DeferredCoroutine<T>(
6287
) : AbstractCoroutine<T>(context), Deferred<T> {
6388
protected open fun start(): Boolean = false // LazyDeferredCoroutine overrides
6489

90+
override val isCompletedExceptionally: Boolean get() = getState() is CompletedExceptionally
91+
override val isCancelled: Boolean get() = getState() is Cancelled
92+
6593
@Suppress("UNCHECKED_CAST")
6694
suspend override fun await(): T {
6795
// quick check if already complete (avoid extra object creation)

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,23 @@ import kotlin.coroutines.experimental.CoroutineContext
2727
// --------------- core job interfaces ---------------
2828

2929
/**
30-
* A background job. It has two states: _active_ (initial state) and _completed_ (final state).
30+
* A background job.
31+
* A job can be _cancelled_ at any time with [cancel] function that forces it to become _completed_ immediately.
32+
*
33+
* It has two states:
34+
* * _Active_ (initial state) -- [isActive] `true`,
35+
* [getCompletionException] throws [IllegalStateException].
36+
* * _Completed_ (final state) -- [isActive] `false`.
3137
*
32-
* A job can be _cancelled_ at any time with [cancel] function that forces it to become completed immediately.
3338
* A job in the coroutine [context][CoroutineScope.context] represents the coroutine itself.
3439
* A job is active while the coroutine is working and job's cancellation aborts the coroutine when
3540
* the coroutine is suspended on a _cancellable_ suspension point by throwing [CancellationException]
36-
* inside the coroutine.
41+
* or the cancellation cause inside the coroutine.
3742
*
3843
* A job can have a _parent_. A job with a parent is cancelled when its parent completes.
3944
*
40-
* All functions on this interface are thread-safe.
45+
* All functions on this interface and on all interfaces derived from it are **thread-safe** and can
46+
* be safely invoked from concurrent coroutines without external synchronization.
4147
*/
4248
public interface Job : CoroutineContext.Element {
4349
/**
@@ -56,17 +62,23 @@ public interface Job : CoroutineContext.Element {
5662
public val isActive: Boolean
5763

5864
/**
59-
* Returns [CancellationException] that [cancellable][suspendCancellableCoroutine] suspending functions throw when
60-
* trying to suspend in the context of this job. This function throws [IllegalAccessException] when invoked
61-
* for an [active][isActive] job.
65+
* Returns the exception that signals the completion of this job -- it returns the original
66+
* [cancel] cause or an instance of [CancellationException] if this job had completed
67+
* normally or was cancelled without a cause. This function throws
68+
* [IllegalStateException] when invoked for an [active][isActive] job.
69+
*
70+
* The [cancellable][suspendCancellableCoroutine] suspending functions throw this exception
71+
* when trying to suspend in the context of this job.
6272
*/
63-
fun getInactiveCancellationException(): CancellationException
73+
fun getCompletionException(): Throwable
6474

6575
/**
6676
* Registers completion handler. The action depends on the state of this job.
6777
* When job is cancelled with [cancel], then the handler is immediately invoked
68-
* with a cancellation reason. Otherwise, handler will be invoked once when this
69-
* job is complete (cancellation also is a form of completion).
78+
* with a cancellation cause or with a fresh [CancellationException].
79+
* Otherwise, handler will be invoked once when this job is complete
80+
* (cancellation also is a form of completion).
81+
*
7082
* The resulting [Registration] can be used to [Registration.unregister] if this
7183
* registration is no longer needed. There is no need to unregister after completion.
7284
*/
@@ -75,8 +87,8 @@ public interface Job : CoroutineContext.Element {
7587
/**
7688
* Cancel this activity with an optional cancellation [cause]. The result is `true` if this job was
7789
* cancelled as a result of this invocation and `false` otherwise
78-
* (if it was already cancelled or it is [NonCancellable]).
79-
* Repeated invocation of this function has no effect and always produces `false`.
90+
* (if it was already _completed_ or if it is [NonCancellable]).
91+
* Repeated invocations of this function have no effect and always produce `false`.
8092
*
8193
* When cancellation has a clear reason in the code, an instance of [CancellationException] should be created
8294
* at the corresponding original cancellation site and passed into this method to aid in debugging by providing
@@ -247,19 +259,19 @@ internal open class JobSupport : AbstractCoroutineContextElement(Job), Job {
247259

248260
fun completeUpdateState(expect: Any, update: Any?) {
249261
// #3. Invoke completion handlers
250-
val reason = (update as? CompletedExceptionally)?.cancelCause
262+
val cause = (update as? CompletedExceptionally)?.exception
251263
var completionException: Throwable? = null
252264
when (expect) {
253265
// SINGLE/SINGLE+ state -- one completion handler (common case)
254266
is JobNode -> try {
255-
expect.invoke(reason)
267+
expect.invoke(cause)
256268
} catch (ex: Throwable) {
257269
completionException = ex
258270
}
259271
// LIST state -- a list of completion handlers
260272
is NodeList -> expect.forEach<JobNode> { node ->
261273
try {
262-
node.invoke(reason)
274+
node.invoke(cause)
263275
} catch (ex: Throwable) {
264276
completionException?.apply { addSuppressed(ex) } ?: run { completionException = ex }
265277
}
@@ -275,12 +287,12 @@ internal open class JobSupport : AbstractCoroutineContextElement(Job), Job {
275287

276288
final override val isActive: Boolean get() = state is Active
277289

278-
override fun getInactiveCancellationException(): CancellationException {
290+
override fun getCompletionException(): Throwable {
279291
val state = getState()
280292
return when (state) {
281293
is Active -> throw IllegalStateException("Job is still active")
282-
is CompletedExceptionally -> state.cancellationException
283-
else -> CancellationException("Job has completed with result")
294+
is CompletedExceptionally -> state.exception
295+
else -> CancellationException("Job has completed normally")
284296
}
285297
}
286298

@@ -311,7 +323,7 @@ internal open class JobSupport : AbstractCoroutineContextElement(Job), Job {
311323
}
312324
// is not active anymore
313325
else -> {
314-
handler((state as? Cancelled)?.cancelCause)
326+
handler((state as? CompletedExceptionally)?.exception)
315327
return EmptyRegistration
316328
}
317329
}
@@ -379,49 +391,40 @@ internal open class JobSupport : AbstractCoroutineContextElement(Job), Job {
379391
*/
380392
internal interface Active
381393

382-
private object Empty : Active
383-
384-
private class NodeList : LockFreeLinkedListHead(), Active
385-
386-
/**
387-
* Abstract class for a [state][getState] of a job that had completed exceptionally, including cancellation.
388-
*/
389-
internal abstract class CompletedExceptionally {
390-
abstract val cancelCause: Throwable // original reason or fresh CancellationException
391-
abstract val exception: Throwable // the exception to be thrown in continuation
394+
private object Empty : Active {
395+
override fun toString(): String = "Empty"
396+
}
392397

393-
// convert cancelCause to CancellationException on first need
394-
@Volatile
395-
private var _cancellationException: CancellationException? = null
396-
397-
val cancellationException: CancellationException get() =
398-
_cancellationException ?: // atomic read volatile var or else build new
399-
(cancelCause as? CancellationException ?:
400-
CancellationException(cancelCause.message)
401-
.apply { initCause(cancelCause) })
402-
.also { _cancellationException = it }
398+
private class NodeList : LockFreeLinkedListHead(), Active {
399+
override fun toString(): String = buildString {
400+
append("[")
401+
var first = true
402+
this@NodeList.forEach<JobNode> { node ->
403+
if (first) first = false else append(", ")
404+
append(node)
405+
}
406+
append("]")
407+
}
403408
}
404409

405410
/**
406-
* Represents a [state][getState] of a cancelled job.
411+
* Class for a [state][getState] of a job that had completed exceptionally, including cancellation.
407412
*/
408-
internal class Cancelled(specifiedCause: Throwable?) : CompletedExceptionally() {
413+
internal open class CompletedExceptionally(cause: Throwable?) {
409414
@Volatile
410-
private var _cancelCause = specifiedCause // materialize CancellationException on first need
415+
private var _exception: Throwable? = cause // materialize CancellationException on first need
411416

412-
override val cancelCause: Throwable get() =
413-
_cancelCause ?: // atomic read volatile var or else create new
414-
CancellationException("Job was cancelled").also { _cancelCause = it }
417+
val exception: Throwable get() =
418+
_exception ?: // atomic read volatile var or else create new
419+
CancellationException("Job was cancelled").also { _exception = it }
415420

416-
override val exception: Throwable get() = cancellationException
421+
override fun toString(): String = "${javaClass.simpleName}[$exception]"
417422
}
418423

419424
/**
420-
* Represents a [state][getState] of a failed job.
425+
* A specific subclass of [CompletedExceptionally] for cancelled jobs.
421426
*/
422-
internal class Failed(override val exception: Throwable) : CompletedExceptionally() {
423-
override val cancelCause: Throwable get() = exception
424-
}
427+
internal class Cancelled(cause: Throwable?) : CompletedExceptionally(cause)
425428
}
426429

427430
internal abstract class JobNode(
@@ -438,7 +441,7 @@ private class InvokeOnCompletion(
438441
val handler: CompletionHandler
439442
) : JobNode(job) {
440443
override fun invoke(reason: Throwable?) = handler.invoke(reason)
441-
override fun toString() = "InvokeOnCompletion[${handler::class.java.name}@${Integer.toHexString(System.identityHashCode(handler))}]"
444+
override fun toString() = "InvokeOnCompletion[${handler.javaClass.name}@${Integer.toHexString(System.identityHashCode(handler))}]"
442445
}
443446

444447
private class ResumeOnCompletion(

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/LazyDeferred.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ import kotlin.coroutines.experimental.startCoroutine
2525
* the first [await] or [start] invocation.
2626
* It is created with [lazyDefer] coroutine builder.
2727
*
28-
* Unlike a simple [Deferred] value, a lazy deferred value has three states:
29-
* * _Pending_ -- before the starts of the coroutine ([isActive] is `true`, but [isComputing] is `false`).
30-
* * _Computing_ -- while computing the value ([isActive] is `true` and [isComputing] is `true`).
31-
* * _Complete_ -- when done computing the value ([isActive] is `false` and [isComputing] is `false`).
28+
* Unlike a simple [Deferred] value, a lazy deferred value has five states:
29+
*
30+
* * _Pending_ (initial, _active_ state before the starts of the coroutine) --
31+
* [isActive] `true`, but [isComputing] `false`,
32+
* [isCompletedExceptionally] `false`, and [isCancelled] `false`.
33+
* * _Computing_ (intermediate state while computing the value) --
34+
* [isActive] `true`, [isComputing] `true`,
35+
* [isCompletedExceptionally] `false`, and [isCancelled] `false`.
36+
* * _Computed_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
37+
* [isCompletedExceptionally] `false`, [isCancelled] `false`.
38+
* * _Failed_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
39+
* [isCompletedExceptionally] `true`, [isCancelled] `false`.
40+
* * _Canceled_ (final _completed_ state) -- [isActive] `false`, [isComputing] `false`,
41+
* [isCompletedExceptionally] `true`, [isCancelled] `true`.
3242
*
3343
* If this lazy deferred value is [cancelled][cancel], then it becomes immediately complete and
3444
* cancels ongoing computation coroutine if it was started.

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import kotlin.coroutines.experimental.AbstractCoroutineContextElement
2121

2222
/**
2323
* A non-cancelable job that is always [active][isActive]. It is designed to be used with [run] builder
24-
* to prevent cancellation of code blocks that need to run without cancellation, like this
24+
* to prevent cancellation of code blocks that need to run without cancellation.
25+
*
26+
* Use it like this:
2527
* ```
2628
* run(NonCancellable) {
2729
* // this code will not be cancelled
@@ -30,7 +32,7 @@ import kotlin.coroutines.experimental.AbstractCoroutineContextElement
3032
*/
3133
object NonCancellable : AbstractCoroutineContextElement(Job), Job {
3234
override val isActive: Boolean get() = true
33-
override fun getInactiveCancellationException(): CancellationException = throw IllegalStateException("This job is always active")
35+
override fun getCompletionException(): CancellationException = throw IllegalStateException("This job is always active")
3436
override fun onCompletion(handler: CompletionHandler): Job.Registration = EmptyRegistration
3537
override fun cancel(cause: Throwable?): Boolean = false
3638
}

kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class CoroutinesTest : TestBase() {
139139
throw IOException()
140140
}
141141

142-
@Test(expected = CancellationException::class)
142+
@Test(expected = IOException::class)
143143
fun testCancelParentOnChildException(): Unit = runBlocking {
144144
expect(1)
145145
launch(context) {
@@ -151,7 +151,7 @@ class CoroutinesTest : TestBase() {
151151
expectUnreached() // because of exception in child
152152
}
153153

154-
@Test(expected = CancellationException::class)
154+
@Test(expected = IOException::class)
155155
fun testCancelParentOnNestedException(): Unit = runBlocking {
156156
expect(1)
157157
launch(context) {

0 commit comments

Comments
 (0)