Skip to content

Commit c692477

Browse files
qwwdfsadelizarov
authored andcommitted
Replacing isCancelledWithoutCause with proper contract on exception, disabling stacktraces of JobCancellationException in production mode
1 parent f705ff4 commit c692477

File tree

11 files changed

+82
-58
lines changed

11 files changed

+82
-58
lines changed

common/kotlinx-coroutines-core-common/src/main/kotlin/kotlinx/coroutines/experimental/AbstractCoroutine.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ public abstract class AbstractCoroutine<in T>(
7676
* This function is invoked once when this coroutine is cancelled or is completed,
7777
* similarly to [invokeOnCompletion] with `onCancelling` set to `true`.
7878
*
79-
* @param cause the cause that was passed to [Job.cancel] function or `null` if coroutine was cancelled
80-
* without cause or is completing normally.
79+
* @param cause the cause that was passed to [Job.cancel] function, [JobCancellationException] if it was completed without cause
80+
* or `null` if coroutine is completing normally.
8181
*/
8282
protected open fun onCancellation(cause: Throwable?) {}
8383

@@ -98,7 +98,7 @@ public abstract class AbstractCoroutine<in T>(
9898
@Suppress("UNCHECKED_CAST")
9999
internal override fun onCompletionInternal(state: Any?, mode: Int) {
100100
if (state is CompletedExceptionally)
101-
onCompletedExceptionally(state.exception)
101+
onCompletedExceptionally(state.cause)
102102
else
103103
onCompleted(state as T)
104104
}

common/kotlinx-coroutines-core-common/src/main/kotlin/kotlinx/coroutines/experimental/CompletedExceptionally.kt

+5-16
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ package kotlinx.coroutines.experimental
2020
* Class for an internal state of a job that had completed exceptionally, including cancellation.
2121
*
2222
* **Note: This class cannot be used outside of internal coroutines framework**.
23+
* **Note: cannot be internal until we get rid of MutableDelegateContinuation in IO**
2324
*
2425
* @param cause the exceptional completion cause. It's either original exceptional cause
2526
* or artificial JobCancellationException if no cause was provided
2627
* @suppress **This is unstable API and it is subject to change.**
2728
*/
28-
// todo: make it internal
29-
public open class CompletedExceptionally(
29+
open class CompletedExceptionally(
3030
public val cause: Throwable
3131
) {
3232
/**
@@ -36,13 +36,7 @@ public open class CompletedExceptionally(
3636
// todo: Remove exception usages
3737
public val exception: Throwable get() = cause // alias for backward compatibility
3838

39-
/**
40-
* Returns true if the current exceptional reason is cancellation without cause
41-
*/
42-
// todo: Remove it. Instead, classify based on `cause is CancellationException`
43-
public open fun isCancelledWithoutCause() = false
44-
45-
override fun toString(): String = "$classSimpleName[$exception]"
39+
override fun toString(): String = "$classSimpleName[$cause]"
4640
}
4741

4842
/**
@@ -54,12 +48,7 @@ public open class CompletedExceptionally(
5448
* @param cause the exceptional completion cause. If `cause` is null, then a [JobCancellationException] is created.
5549
* @suppress **This is unstable API and it is subject to change.**
5650
*/
57-
// todo: make it internal
58-
public class Cancelled(
51+
internal class Cancelled(
5952
private val job: Job,
6053
cause: Throwable?
61-
) : CompletedExceptionally(cause ?: JobCancellationException("Job was cancelled normally", null, job)) {
62-
63-
override fun isCancelledWithoutCause(): Boolean = cause is JobCancellationException && cause.job == job
64-
}
65-
54+
) : CompletedExceptionally(cause ?: JobCancellationException("Job was cancelled normally", null, job))

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

+18-24
Original file line numberDiff line numberDiff line change
@@ -641,15 +641,16 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
641641
private fun isCorrespondinglyCancelled(cancelled: Cancelled, proposedUpdate: Any?): Boolean {
642642
if (proposedUpdate !is Cancelled) return false
643643
// NOTE: equality comparison of causes is performed here by design, see equals of JobCancellationException
644-
return proposedUpdate.cause == cancelled.cause ||
645-
proposedUpdate.cause is JobCancellationException && cancelled.isCancelledWithoutCause()
644+
return proposedUpdate.cause == cancelled.cause || proposedUpdate.cause is JobCancellationException
646645
}
647646

648647
private fun createCancelled(cancelled: Cancelled, proposedUpdate: Any?): Cancelled {
649648
if (proposedUpdate !is CompletedExceptionally) return cancelled // not exception -- just use original cancelled
650-
val exception = proposedUpdate.exception
651-
if (cancelled.exception == exception) return cancelled // that is the cancelled we need already!
652-
if (!cancelled.isCancelledWithoutCause()) {
649+
val exception = proposedUpdate.cause
650+
if (cancelled.cause == exception) return cancelled // that is the cancelled we need already!
651+
652+
// Do not spam with JCE in suppressed exceptions
653+
if (cancelled.cause !is JobCancellationException) {
653654
exception.addSuppressedThrowable(cancelled.cause)
654655
}
655656
return Cancelled(this, exception)
@@ -752,11 +753,11 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
752753
val state = this.state
753754
return when {
754755
state is Finishing && state.cancelled != null ->
755-
state.cancelled.exception.toCancellationException("Job is being cancelled")
756+
state.cancelled.cause.toCancellationException("Job is being cancelled")
756757
state is Incomplete ->
757758
error("Job was not completed or cancelled yet: $this")
758759
state is CompletedExceptionally ->
759-
state.exception.toCancellationException("Job has failed")
760+
state.cause.toCancellationException("Job has failed")
760761
else -> JobCancellationException("Job has completed normally", null, this)
761762
}
762763
}
@@ -766,21 +767,14 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
766767

767768
/**
768769
* Returns the cause that signals the completion of this job -- it returns the original
769-
* [cancel] cause or **`null` if this job had completed
770-
* normally or was cancelled without a cause**. This function throws
771-
* [IllegalStateException] when invoked for an job that has not [completed][isCompleted] nor
770+
* [cancel] cause, [JobCancellationException] or **`null` if this job had completed normally**.
771+
* This function throws [IllegalStateException] when invoked for an job that has not [completed][isCompleted] nor
772772
* [isCancelled] yet.
773773
*/
774774
protected fun getCompletionCause(): Throwable? {
775775
val state = this.state
776776
return when {
777-
state is Finishing && state.cancelled != null -> {
778-
if (state.cancelled.isCancelledWithoutCause()) {
779-
null
780-
} else {
781-
state.cancelled.cause
782-
}
783-
}
777+
state is Finishing && state.cancelled != null -> state.cancelled.cause
784778
state is Incomplete -> error("Job was not completed or cancelled yet")
785779
state is CompletedExceptionally -> state.cause
786780
else -> null
@@ -1060,7 +1054,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
10601054
}
10611055
// cancel all children in list on exceptional completion
10621056
if (proposedUpdate is CompletedExceptionally)
1063-
child?.cancelChildrenInternal(proposedUpdate.exception)
1057+
child?.cancelChildrenInternal(proposedUpdate.cause)
10641058
// switch to completing state
10651059
val cancelled = (state as? Finishing)?.cancelled ?: (proposedUpdate as? Cancelled)
10661060
val completing = Finishing(list, cancelled, true)
@@ -1080,7 +1074,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
10801074
}
10811075

10821076
private val Any?.exceptionOrNull: Throwable?
1083-
get() = (this as? CompletedExceptionally)?.exception
1077+
get() = (this as? CompletedExceptionally)?.cause
10841078

10851079
private fun firstChild(state: Incomplete) =
10861080
state as? Child ?: state.list?.nextChild()
@@ -1232,7 +1226,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
12321226
internal fun getCompletedInternal(): Any? {
12331227
val state = this.state
12341228
check(state !is Incomplete) { "This job has not completed yet" }
1235-
if (state is CompletedExceptionally) throw state.exception
1229+
if (state is CompletedExceptionally) throw state.cause
12361230
return state
12371231
}
12381232

@@ -1245,7 +1239,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
12451239
val state = this.state
12461240
if (state !is Incomplete) {
12471241
// already complete -- just return result
1248-
if (state is CompletedExceptionally) throw state.exception
1242+
if (state is CompletedExceptionally) throw state.cause
12491243
return state
12501244

12511245
}
@@ -1259,7 +1253,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
12591253
val state = this.state
12601254
check(state !is Incomplete)
12611255
if (state is CompletedExceptionally)
1262-
cont.resumeWithException(state.exception)
1256+
cont.resumeWithException(state.cause)
12631257
else
12641258
cont.resume(state)
12651259
})
@@ -1278,7 +1272,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
12781272
// already complete -- select result
12791273
if (select.trySelect(null)) {
12801274
if (state is CompletedExceptionally)
1281-
select.resumeSelectCancellableWithException(state.exception)
1275+
select.resumeSelectCancellableWithException(state.cause)
12821276
else
12831277
block.startCoroutineUndispatched(state as T, select.completion)
12841278
}
@@ -1300,7 +1294,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
13001294
val state = this.state
13011295
// Note: await is non-atomic (can be cancelled while dispatched)
13021296
if (state is CompletedExceptionally)
1303-
select.resumeSelectCancellableWithException(state.exception)
1297+
select.resumeSelectCancellableWithException(state.cause)
13041298
else
13051299
block.startCoroutineCancellable(state as T, select.completion)
13061300
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public const val DEBUG_PROPERTY_VALUE_ON = "on"
4141
*/
4242
public const val DEBUG_PROPERTY_VALUE_OFF = "off"
4343

44-
private val DEBUG = run {
44+
internal val DEBUG = run {
4545
val value = try { System.getProperty(DEBUG_PROPERTY_NAME) }
4646
catch (e: SecurityException) { null }
4747
when (value) {

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package kotlinx.coroutines.experimental
1818

19-
import java.util.concurrent.*
20-
2119
/**
2220
* This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job].
2321
*/
@@ -47,7 +45,24 @@ public actual class JobCancellationException public actual constructor(
4745
*/
4846
public actual val job: Job
4947
) : CancellationException(message) {
50-
init { if (cause != null) initCause(cause) }
48+
49+
init {
50+
if (cause != null) initCause(cause)
51+
}
52+
53+
override fun fillInStackTrace(): Throwable {
54+
if (DEBUG) {
55+
return super.fillInStackTrace()
56+
}
57+
58+
/*
59+
* In non-debug mode we don't want to have a stacktrace on every cancellation/close,
60+
* parent job reference is enough. Stacktrace of JCE is not needed most of the time (e.g., it is not logged)
61+
* and hurts performance.
62+
*/
63+
return this
64+
}
65+
5166
override fun toString(): String = "${super.toString()}; job=$job"
5267
override fun equals(other: Any?): Boolean =
5368
other === this ||

core/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/channels/Produce.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ private class ProducerCoroutine<E>(
124124
val cause = exceptionally?.cause
125125
val processed = when (exceptionally) {
126126
// producer coroutine was cancelled -- cancel channel, but without cause if it was closed without cause
127-
is Cancelled -> _channel.cancel(if (exceptionally.isCancelledWithoutCause()) null else cause)
127+
is Cancelled -> _channel.cancel(if (cause is JobCancellationException) null else cause)
128128
else -> _channel.close(cause) // producer coroutine has completed -- close channel
129129
}
130130
if (!processed && cause != null)

core/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/channels/ActorTest.kt

+32-6
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import kotlinx.coroutines.experimental.*
2020
import org.hamcrest.core.*
2121
import org.junit.*
2222
import org.junit.Assert.*
23-
import org.junit.runner.RunWith
24-
import org.junit.runners.Parameterized
25-
import java.io.IOException
23+
import org.junit.runner.*
24+
import org.junit.runners.*
25+
import java.io.*
2626
import kotlin.coroutines.experimental.*
2727

2828
@RunWith(Parameterized::class)
@@ -80,9 +80,8 @@ class ActorTest(private val capacity: Int) : TestBase() {
8080
finish(7)
8181
}
8282

83-
8483
@Test
85-
fun testCancelWithoutCause() = runTest {
84+
fun testCloseWithoutCause() = runTest {
8685
val actor = actor<Int>(coroutineContext, capacity) {
8786
val element = channel.receiveOrNull()
8887
expect(2)
@@ -101,7 +100,7 @@ class ActorTest(private val capacity: Int) : TestBase() {
101100
}
102101

103102
@Test
104-
fun testCancelWithCause() = runTest {
103+
fun testCloseWithCause() = runTest {
105104
val actor = actor<Int>(coroutineContext, capacity) {
106105
val element = channel.receiveOrNull()
107106
expect(2)
@@ -120,4 +119,31 @@ class ActorTest(private val capacity: Int) : TestBase() {
120119
yield()
121120
finish(4)
122121
}
122+
123+
@Test
124+
fun testCancelEnclosingJob() = runTest {
125+
val job = async(coroutineContext) {
126+
actor<Int>(coroutineContext, capacity) {
127+
expect(1)
128+
channel.receiveOrNull()
129+
expectUnreached()
130+
}
131+
}
132+
133+
yield()
134+
yield()
135+
136+
expect(2)
137+
yield()
138+
job.cancel()
139+
140+
try {
141+
job.await()
142+
expectUnreached()
143+
} catch (e: JobCancellationException) {
144+
assertTrue(e.message?.contains("Job was cancelled normally") ?: false)
145+
}
146+
147+
finish(3)
148+
}
123149
}

core/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/channels/ArrayChannelTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package kotlinx.coroutines.experimental.channels
1919
import kotlinx.coroutines.experimental.*
2020
import org.junit.*
2121
import org.junit.Assert.*
22-
import java.io.IOException
22+
import java.io.*
2323
import kotlin.coroutines.experimental.*
2424

2525
class ArrayChannelTest : TestBase() {

reactive/kotlinx-coroutines-reactive/src/main/kotlin/kotlinx/coroutines/experimental/reactive/Publish.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ private class PublisherCoroutine<in T>(
167167
_nRequested.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
168168
val cause = getCompletionCause()
169169
try {
170-
if (cause != null)
170+
if (cause != null && cause !is JobCancellationException)
171171
subscriber.onError(cause)
172172
else
173173
subscriber.onComplete()

reactive/kotlinx-coroutines-rx1/src/main/kotlin/kotlinx/coroutines/experimental/rx1/RxObservable.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private class RxObservableCoroutine<in T>(
168168
_nRequested.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
169169
val cause = getCompletionCause()
170170
try {
171-
if (cause != null)
171+
if (cause != null && cause !is JobCancellationException)
172172
subscriber.onError(cause)
173173
else
174174
subscriber.onCompleted()

reactive/kotlinx-coroutines-rx2/src/main/kotlin/kotlinx/coroutines/experimental/rx2/RxObservable.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ private class RxObservableCoroutine<T>(
158158
_signal.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
159159
val cause = getCompletionCause()
160160
try {
161-
if (cause != null)
161+
if (cause != null && cause !is JobCancellationException)
162162
subscriber.onError(cause)
163163
else
164164
subscriber.onComplete()

0 commit comments

Comments
 (0)