Skip to content

Commit 0aad8f1

Browse files
committed
Hide cancel(Throwable), introduce cancel(CancellationException)
Job/ReceiveChannel/BroadcastChannel.cancel(Throwable) is now hidden and cannot be invoked from a newly compiled code. This function had broken semantics when used with an arbitrary exception. A safe replacement is available in the form of cancel(CancellationException) where CancellationException can be used to supply additional information for debugging purposes. Fixes #975
1 parent 1be63c0 commit 0aad8f1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+416
-346
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+51-31
Large diffs are not rendered by default.

integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,17 @@ class ListenableFutureTest : TestBase() {
4646
}
4747

4848
@Test
49-
fun testAwaitWithContextCancellation() = runTest(expected = {it is IOException}) {
49+
fun testAwaitWithCancellation() = runTest(expected = {it is TestCancellationException}) {
5050
val future = SettableFuture.create<Int>()
5151
val deferred = async {
5252
withContext(Dispatchers.Default) {
5353
future.await()
5454
}
5555
}
5656

57-
deferred.cancel(IOException())
58-
deferred.await()
57+
deferred.cancel(TestCancellationException())
58+
deferred.await() // throws TCE
59+
expectUnreached()
5960
}
6061

6162
@Test

integration/kotlinx-coroutines-jdk8/src/future/Future.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.coroutines.future
66

77
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.CancellationException
89
import java.util.concurrent.*
910
import java.util.function.*
1011
import kotlin.coroutines.*
@@ -70,7 +71,11 @@ private class CompletableFutureCoroutine<T>(
7071
*/
7172
public fun <T> Deferred<T>.asCompletableFuture(): CompletableFuture<T> {
7273
val future = CompletableFuture<T>()
73-
future.whenComplete { _, exception -> cancel(exception) }
74+
future.whenComplete { _, exception ->
75+
cancel(exception?.let {
76+
it as? CancellationException ?: CancellationException("Future failed", it)
77+
})
78+
}
7479
invokeOnCompletion {
7580
try {
7681
future.complete(getCompleted())

kotlinx-coroutines-core/common/src/CoroutineScope.kt

+6-4
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,12 @@ public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
196196
ContextScope(if (context[Job] != null) context else context + Job())
197197

198198
/**
199-
* Cancels this scope, including its job and all its children.
199+
* Cancels this scope, including its job and all its children with an optional cancellation [cause].
200+
* A cause can be used to specify an error message or to provide other details on
201+
* a cancellation reason for debugging purposes.
200202
* Throws [IllegalStateException] if the scope does not have a job in it.
201-
**/
202-
public fun CoroutineScope.cancel() {
203+
*/
204+
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
203205
val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
204-
job.cancel()
206+
job.cancel(cause)
205207
}

kotlinx-coroutines-core/common/src/Exceptions.common.kt

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public expect class CompletionHandlerException(message: String, cause: Throwable
1212

1313
public expect open class CancellationException(message: String?) : IllegalStateException
1414

15+
@Suppress("FunctionName")
16+
public expect fun CancellationException(message: String?, cause: Throwable?) : CancellationException
17+
1518
internal expect class JobCancellationException(
1619
message: String,
1720
cause: Throwable?,

kotlinx-coroutines-core/common/src/Job.kt

+50-45
Original file line numberDiff line numberDiff line change
@@ -154,27 +154,25 @@ public interface Job : CoroutineContext.Element {
154154
*/
155155
public fun start(): Boolean
156156

157+
157158
/**
158-
* @suppress
159+
* Cancels this job with an optional cancellation [cause].
160+
* A cause can be used to specify an error message or to provide other details on
161+
* a cancellation reason for debugging purposes.
162+
* See [Job] documentation for full explanation of cancellation machinery.
159163
*/
160-
@Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION")
161-
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Left here for binary compatibility")
162-
@JvmName("cancel")
163-
public fun cancel0(): Boolean = cancel(null)
164+
public fun cancel(cause: CancellationException? = null)
164165

165166
/**
166-
* Cancels this job.
167-
* See [Job] documentation for full explanation of cancellation machinery.
167+
* @suppress This method implements old version of JVM ABI. Use [cancel].
168168
*/
169-
public fun cancel(): Unit
169+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
170+
public fun cancel() = cancel(null)
170171

171172
/**
172-
* @suppress
173+
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel].
173174
*/
174-
@ObsoleteCoroutinesApi
175-
@Deprecated(level = DeprecationLevel.WARNING, message = "Use CompletableDeferred.completeExceptionally(cause) or Job.cancel() instead",
176-
replaceWith = ReplaceWith("cancel()")
177-
)
175+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
178176
public fun cancel(cause: Throwable? = null): Boolean
179177

180178
// ------------ parent-child ------------
@@ -479,21 +477,26 @@ public suspend fun Job.cancelAndJoin() {
479477
}
480478

481479
/**
482-
* @suppress
480+
* Cancels all [children][Job.children] jobs of this coroutine using [Job.cancel] for all of them
481+
* with an optional cancellation [cause].
482+
* Unlike [Job.cancel] on this job as a whole, the state of this job itself is not affected.
483483
*/
484-
@ObsoleteCoroutinesApi
485-
@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancelChildren() without cause", replaceWith = ReplaceWith("cancelChildren()"))
486-
public fun Job.cancelChildren(cause: Throwable? = null) {
487-
@Suppress("DEPRECATION")
484+
public fun Job.cancelChildren(cause: CancellationException? = null) {
488485
children.forEach { it.cancel(cause) }
489486
}
490487

491488
/**
492-
* Cancels all [children][Job.children] jobs of this coroutine using [Job.cancel] for all of them.
493-
* Unlike [Job.cancel] on this job as a whole, the state of this job itself is not affected.
489+
* @suppress This method implements old version of JVM ABI. Use [cancel].
494490
*/
495-
public fun Job.cancelChildren() {
496-
children.forEach { it.cancel() }
491+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
492+
public fun Job.cancelChildren() = cancelChildren(null)
493+
494+
/**
495+
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [Job.cancelChildren].
496+
*/
497+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
498+
public fun Job.cancelChildren(cause: Throwable? = null) {
499+
children.forEach { (it as? JobSupport)?.cancelInternal(cause) }
497500
}
498501

499502
// -------------------- CoroutineContext extensions --------------------
@@ -517,47 +520,49 @@ public fun Job.cancelChildren() {
517520
public val CoroutineContext.isActive: Boolean
518521
get() = this[Job]?.isActive == true
519522

520-
521523
/**
522-
* @suppress
524+
* Cancels [Job] of this context with an optional cancellation cause.
525+
* See [Job.cancel] for details.
523526
*/
524-
@Suppress("unused")
525-
@JvmName("cancel")
526-
@Deprecated(message = "Binary compatibility", level = DeprecationLevel.HIDDEN)
527-
public fun CoroutineContext.cancel0(): Boolean {
528-
this[Job]?.cancel()
529-
return true
527+
public fun CoroutineContext.cancel(cause: CancellationException? = null) {
528+
this[Job]?.cancel(cause)
530529
}
531530

532531
/**
533-
* Cancels [Job] of this context. See [Job.cancel] for details.
532+
* @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancel].
534533
*/
535-
public fun CoroutineContext.cancel(): Unit {
536-
this[Job]?.cancel()
537-
}
534+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
535+
public fun CoroutineContext.cancel() = cancel(null)
538536

539537
/**
540-
* @suppress
538+
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancel].
541539
*/
542-
@ObsoleteCoroutinesApi
543-
@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancel() without cause", replaceWith = ReplaceWith("cancel()"))
540+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
544541
public fun CoroutineContext.cancel(cause: Throwable? = null): Boolean =
545542
@Suppress("DEPRECATION")
546-
this[Job]?.cancel(cause) ?: false
543+
(this[Job] as? JobSupport)?.cancelInternal(cause) ?: false
547544

548545
/**
549-
* Cancels all children of the [Job] in this context, without touching the the state of this job itself.
546+
* Cancels all children of the [Job] in this context, without touching the the state of this job itself
547+
* with an optional cancellation cause. See [Job.cancel].
550548
* It does not do anything if there is no job in the context or it has no children.
551549
*/
552-
public fun CoroutineContext.cancelChildren() {
553-
this[Job]?.children?.forEach { it.cancel() }
550+
public fun CoroutineContext.cancelChildren(cause: CancellationException? = null) {
551+
this[Job]?.children?.forEach { it.cancel(cause) }
554552
}
555553

556-
@ObsoleteCoroutinesApi
557-
@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancelChildren() without cause", replaceWith = ReplaceWith("cancelChildren()"))
554+
/**
555+
* @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancelChildren].
556+
*/
557+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
558+
public fun CoroutineContext.cancelChildren() = cancelChildren(null)
559+
560+
/**
561+
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancelChildren].
562+
*/
563+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
558564
public fun CoroutineContext.cancelChildren(cause: Throwable? = null) {
559-
@Suppress("DEPRECATION")
560-
this[Job]?.children?.forEach { it.cancel(cause) }
565+
this[Job]?.children?.forEach { (it as? JobSupport)?.cancelInternal(cause) }
561566
}
562567

563568
/**

kotlinx-coroutines-core/common/src/JobSupport.kt

+22-11
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,17 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
370370
public final override fun getCancellationException(): CancellationException {
371371
val state = this.state
372372
return when (state) {
373-
is Finishing -> state.rootCause?.toCancellationException("Job is cancelling")
373+
is Finishing -> state.rootCause?.toCancellationException("$classSimpleName is cancelling")
374374
?: error("Job is still new or active: $this")
375375
is Incomplete -> error("Job is still new or active: $this")
376-
is CompletedExceptionally -> state.cause.toCancellationException("Job was cancelled")
377-
else -> JobCancellationException("Job has completed normally", null, this)
376+
is CompletedExceptionally -> state.cause.toCancellationException()
377+
else -> JobCancellationException("$classSimpleName has completed normally", null, this)
378378
}
379379
}
380380

381-
private fun Throwable.toCancellationException(message: String): CancellationException =
382-
this as? CancellationException ?: JobCancellationException(message, this, this@JobSupport)
381+
protected fun Throwable.toCancellationException(message: String? = null): CancellationException =
382+
this as? CancellationException ?:
383+
JobCancellationException(message ?: "$classSimpleName was cancelled", this, this@JobSupport)
383384

384385
/**
385386
* Returns the cause that signals the completion of this job -- it returns the original
@@ -565,14 +566,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
565566
*/
566567
internal open val onCancelComplete: Boolean get() = false
567568

568-
// external cancel without cause, never invoked implicitly from internal machinery
569-
public override fun cancel() {
570-
@Suppress("DEPRECATION")
571-
cancel(null) // must delegate here, because some classes override cancel(x)
569+
// external cancel with cause, never invoked implicitly from internal machinery
570+
public override fun cancel(cause: CancellationException?) {
571+
cancelInternal(cause) // must delegate here, because some classes override cancelInternal(x)
572572
}
573573

574+
// HIDDEN in Job interface. Invoked only by legacy compiled code.
574575
// external cancel with (optional) cause, never invoked implicitly from internal machinery
576+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
575577
public override fun cancel(cause: Throwable?): Boolean =
578+
cancelInternal(cause)
579+
580+
// It is overridden in channel-linked implementation
581+
// Note: Boolean result is used only in DEPRECATED functions
582+
public open fun cancelInternal(cause: Throwable?): Boolean =
576583
cancelImpl(cause) && handlesException
577584

578585
// Parent is cancelling child
@@ -581,11 +588,15 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
581588
}
582589

583590
// Child was cancelled with cause
591+
// It is overridden in supervisor implementations to ignore child cancellation
584592
public open fun childCancelled(cause: Throwable): Boolean =
585593
cancelImpl(cause) && handlesException
586594

587-
// For AbstractCoroutine implementations
588-
protected fun cancelCoroutine(cause: Throwable?) =
595+
/**
596+
* Makes this [Job] cancelled with a specified [cause].
597+
* It is used in [AbstractCoroutine]-derived classes when there is an internal failure.
598+
*/
599+
public fun cancelCoroutine(cause: Throwable?) =
589600
cancelImpl(cause)
590601

591602
// cause is Throwable or ParentJob when cancelChild was invoked

kotlinx-coroutines-core/common/src/NonCancellable.kt

+3-5
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,13 @@ public object NonCancellable : AbstractCoroutineContextElement(Job), Job {
9292
* @suppress **This an internal API and should not be used from general code.**
9393
*/
9494
@InternalCoroutinesApi
95-
@Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE")
96-
override fun cancel(): Unit {
97-
}
95+
override fun cancel(cause: CancellationException?) {}
9896

9997
/**
10098
* Always returns `false`.
101-
* @suppress **This an internal API and should not be used from general code.**
99+
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel].
102100
*/
103-
@InternalCoroutinesApi
101+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
104102
override fun cancel(cause: Throwable?): Boolean = false // never handles exceptions
105103

106104
/**

kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt

+8-4
Original file line numberDiff line numberDiff line change
@@ -655,12 +655,16 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
655655
return if (result === POLL_FAILED) null else receiveOrNullResult(result)
656656
}
657657

658-
override fun cancel() {
659-
@Suppress("DEPRECATION")
660-
cancel(null)
658+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
659+
final override fun cancel(cause: Throwable?): Boolean =
660+
cancelInternal(cause)
661+
662+
final override fun cancel(cause: CancellationException?) {
663+
cancelInternal(cause)
661664
}
662665

663-
override fun cancel(cause: Throwable?): Boolean =
666+
// It needs to be internal to support deprecated cancel(Throwable?) API
667+
internal open fun cancelInternal(cause: Throwable?): Boolean =
664668
close(cause).also {
665669
cleanupSendQueueOnCancel()
666670
}

kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt

+11-4
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,17 @@ internal class ArrayBroadcastChannel<E>(
6767
return true
6868
}
6969

70-
public override fun cancel(cause: Throwable?): Boolean =
70+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
71+
override fun cancel(cause: Throwable?): Boolean =
72+
cancelInternal(cause)
73+
74+
override fun cancel(cause: CancellationException?) {
75+
cancelInternal(cause)
76+
}
77+
78+
private fun cancelInternal(cause: Throwable?): Boolean =
7179
close(cause).also {
72-
@Suppress("DEPRECATION")
73-
for (sub in subscribers) sub.cancel(cause)
80+
for (sub in subscribers) sub.cancelInternal(cause)
7481
}
7582

7683
// result is `OFFER_SUCCESS | OFFER_FAILED | Closed`
@@ -201,7 +208,7 @@ internal class ArrayBroadcastChannel<E>(
201208
override val isBufferAlwaysFull: Boolean get() = error("Should not be used")
202209
override val isBufferFull: Boolean get() = error("Should not be used")
203210

204-
override fun cancel(cause: Throwable?): Boolean =
211+
override fun cancelInternal(cause: Throwable?): Boolean =
205212
close(cause).also { closed ->
206213
if (closed) broadcastChannel.updateHead(removeSub = this)
207214
clearBuffer()

kotlinx-coroutines-core/common/src/channels/Broadcast.kt

+12-5
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,18 @@ private open class BroadcastCoroutine<E>(
9595
override val channel: SendChannel<E>
9696
get() = this
9797

98-
override fun cancel(cause: Throwable?): Boolean {
99-
val wasCancelled = _channel.cancel(cause)
100-
@Suppress("DEPRECATION")
101-
if (wasCancelled) cancelCoroutine(cause) // cancel the job
102-
return wasCancelled
98+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
99+
final override fun cancel(cause: Throwable?): Boolean =
100+
cancelInternal(cause)
101+
102+
final override fun cancel(cause: CancellationException?) {
103+
cancelInternal(cause)
104+
}
105+
106+
override fun cancelInternal(cause: Throwable?): Boolean {
107+
_channel.cancel(cause?.toCancellationException()) // cancel the channel
108+
cancelCoroutine(cause) // cancel the job
109+
return true // does not matter - result is used in DEPRECATED functions only
103110
}
104111

105112
override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) {

0 commit comments

Comments
 (0)