Skip to content

Commit a3826a9

Browse files
committed
Merge branch 'no-unwrap' into version-0.30.1
2 parents 4e1efc8 + b5c9c9f commit a3826a9

File tree

4 files changed

+77
-87
lines changed

4 files changed

+77
-87
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ internal abstract class AbstractContinuation<in T>(
105105
/**
106106
* It is used when parent is cancelled to get the cancellation cause for this continuation.
107107
*/
108-
open fun getParentCancellationCause(parent: Job): Throwable =
108+
open fun getContinuationCancellationCause(parent: Job): Throwable =
109109
parent.getCancellationException()
110110

111111
private fun trySuspend(): Boolean {

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -425,13 +425,30 @@ public inline fun DisposableHandle(crossinline block: () -> Unit) =
425425
internal interface ChildJob : Job {
426426
/**
427427
* Parent is cancelling its child by invoking this method.
428-
* Child finds the cancellation cause using [getCancellationException] of the [parentJob].
428+
* Child finds the cancellation cause using [ParentJob.getChildJobCancellationCause].
429429
* This method does nothing is the child is already being cancelled.
430430
*
431431
* @suppress **This is unstable API and it is subject to change.**
432432
*/
433433
@InternalCoroutinesApi
434-
public fun parentCancelled(parentJob: Job)
434+
public fun parentCancelled(parentJob: ParentJob)
435+
}
436+
437+
/**
438+
* A reference that child receives from its parent when it is being cancelled by the parent.
439+
*
440+
* @suppress **This is unstable API and it is subject to change.**
441+
*/
442+
@InternalCoroutinesApi
443+
internal interface ParentJob : Job {
444+
/**
445+
* Child job is using this method to learn its cancellation cause when the parent cancels it with [ChildJob.parentCancelled].
446+
* This method is invoked only if the child was not already being cancelled.
447+
*
448+
* @suppress **This is unstable API and it is subject to change.**
449+
*/
450+
@InternalCoroutinesApi
451+
public fun getChildJobCancellationCause(): Throwable
435452
}
436453

437454
/**

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

+45-72
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import kotlin.coroutines.experimental.intrinsics.*
2020
* @param active when `true` the job is created in _active_ state, when `false` in _new_ state. See [Job] for details.
2121
* @suppress **This is unstable API and it is subject to change.**
2222
*/
23-
internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, SelectClause0 {
23+
internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 {
2424
final override val key: CoroutineContext.Key<*> get() = Job
2525

2626
/*
@@ -230,63 +230,23 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
230230
if (state.isCancelling) return createJobCancellationException()
231231
return null
232232
}
233-
/*
234-
* This is a place where we step on our API limitation:
235-
* We can't distinguish internal JobCancellationException from our parent
236-
* from external cancellation, thus we ought to collect all exceptions.
237-
* If parent is cancelling, it cancels its children with JCE(rootCause).
238-
* When child is building final exception, it can skip JCE(anything) if it knows
239-
* that parent handles exceptions, because parent should already have this exception.
240-
* If parent does not, then we should unwrap exception, otherwise in the following code
241-
* ```
242-
* val parent = Job()
243-
* launch(parent) {
244-
* try { delay() } finally { throw E2() }
245-
* }
246-
* parent.cancel(E1)
247-
* ```
248-
* E1 will be lost.
249-
*/
250-
var rootCause = exceptions[0]
251-
if (rootCause is CancellationException) {
252-
val cause = unwrap(rootCause)
253-
rootCause = if (cause !== null) {
254-
cause
255-
} else {
256-
exceptions.firstOrNull { unwrap(it) != null } ?: return rootCause
257-
}
258-
}
259-
return rootCause
233+
// Take either the first real exception (not a cancellation) or just the first exception
234+
return exceptions.firstOrNull { it !is CancellationException } ?: exceptions[0]
260235
}
261236

262237
private fun suppressExceptions(rootCause: Throwable, exceptions: List<Throwable>): Boolean {
263238
if (exceptions.size <= 1) return false // nothing more to do here
264239
val seenExceptions = identitySet<Throwable>(exceptions.size)
265240
var suppressed = false
266-
for (i in 1 until exceptions.size) {
267-
val unwrapped = unwrap(exceptions[i])
268-
if (unwrapped !== null && unwrapped !== rootCause) {
269-
if (seenExceptions.add(unwrapped)) {
270-
rootCause.addSuppressedThrowable(unwrapped)
271-
suppressed = true
272-
}
241+
for (exception in exceptions) {
242+
if (exception !== rootCause && exception !is CancellationException && seenExceptions.add(exception)) {
243+
rootCause.addSuppressedThrowable(exception)
244+
suppressed = true
273245
}
274246
}
275247
return suppressed
276248
}
277249

278-
private tailrec fun unwrap(exception: Throwable): Throwable? {
279-
if (exception is CancellationException && parentHandlesExceptions) {
280-
return null
281-
}
282-
return if (exception is CancellationException) {
283-
val cause = exception.cause
284-
if (cause !== null) unwrap(cause) else null
285-
} else {
286-
exception
287-
}
288-
}
289-
290250
// fast-path method to finalize normally completed coroutines without children
291251
private fun tryFinalizeSimpleState(state: Incomplete, update: Any?, mode: Int): Boolean {
292252
check(state is Empty || state is JobNode<*>) // only simple state without lists where children can concurrently add
@@ -616,15 +576,15 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
616576
cancelImpl(cause) && handlesException
617577

618578
// Parent is cancelling child
619-
public final override fun parentCancelled(parentJob: Job) {
579+
public final override fun parentCancelled(parentJob: ParentJob) {
620580
cancelImpl(parentJob)
621581
}
622582

623583
// Child was cancelled with cause
624584
public open fun childCancelled(cause: Throwable): Boolean =
625585
cancelImpl(cause) && handlesException
626586

627-
// cause is Throwable or Job when cancelChild was invoked
587+
// cause is Throwable or ParentJob when cancelChild was invoked
628588
// returns true is exception was handled, false otherwise
629589
private fun cancelImpl(cause: Any?): Boolean {
630590
if (onCancelComplete) {
@@ -636,6 +596,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
636596
return makeCancelling(cause)
637597
}
638598

599+
// cause is Throwable or ParentJob when cancelChild was invoked
639600
private fun cancelMakeCompleting(cause: Any?): Boolean {
640601
loopOnState { state ->
641602
if (state !is Incomplete || state is Finishing && state.isCompleting) {
@@ -654,14 +615,35 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
654615
private fun createJobCancellationException() =
655616
JobCancellationException("Job was cancelled", null, this)
656617

657-
// cause is Throwable or Job when cancelChild was invoked, cause can be null only on cancel
618+
override fun getChildJobCancellationCause(): Throwable {
619+
// determine root cancellation cause of this job (why is it cancelling its children?)
620+
val state = this.state
621+
val rootCause = when (state) {
622+
is Finishing -> state.rootCause
623+
is Incomplete -> error("Cannot be cancelling child in this state: $state")
624+
is CompletedExceptionally -> state.cause
625+
else -> null // create exception with the below code on normal completion
626+
}
627+
/*
628+
* If this parent job handles exceptions, then wrap cause into JobCancellationException, because we
629+
* don't want the child to handle this exception on more time. Otherwise, pass our original rootCause
630+
* to the child for cancellation.
631+
*/
632+
return if (rootCause == null || handlesException && rootCause !is CancellationException) {
633+
JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
634+
} else {
635+
rootCause
636+
}
637+
}
638+
639+
// cause is Throwable or ParentJob when cancelChild was invoked
658640
private fun createCauseException(cause: Any?): Throwable = when(cause) {
659641
is Throwable? -> cause ?: createJobCancellationException()
660-
else -> (cause as Job).getCancellationException()
642+
else -> (cause as ParentJob).getChildJobCancellationCause()
661643
}
662644

663645
// transitions to Cancelling state
664-
// cause is Throwable or Job when cancelChild was invoked, cause can be null only on cancel
646+
// cause is Throwable or ParentJob when cancelChild was invoked
665647
private fun makeCancelling(cause: Any?): Boolean {
666648
var causeExceptionCache: Throwable? = null // lazily init result of createCauseException(cause)
667649
loopOnState { state ->
@@ -927,10 +909,6 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
927909
*/
928910
protected open val handlesException: Boolean get() = true
929911

930-
// returns true when we know that parent handles exceptions
931-
private val parentHandlesExceptions: Boolean get() =
932-
(parentHandle as? ChildHandleNode)?.job?.handlesException ?: false
933-
934912
/**
935913
* This method is invoked **exactly once** when the final exception of the job is determined
936914
* and before it becomes complete. At the moment of invocation the job and all its children are complete.
@@ -959,27 +937,22 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
959937

960938
// for nicer debugging
961939
public override fun toString(): String =
962-
"${nameString()}{${stateString()}}@$hexAddress"
940+
"${nameString()}{${stateString(state)}}@$hexAddress"
963941

964942
/**
965943
* @suppress **This is unstable API and it is subject to change.**
966944
*/
967945
internal open fun nameString(): String = classSimpleName
968946

969-
private fun stateString(): String {
970-
val state = this.state
971-
return when (state) {
972-
is Finishing -> buildString {
973-
when {
974-
state.isCancelling -> append("Cancelling")
975-
else -> append("Active")
976-
}
977-
if (state.isCompleting) append("Completing")
978-
}
979-
is Incomplete -> if (state.isActive) "Active" else "New"
980-
is CompletedExceptionally -> "Cancelled"
981-
else -> "Completed"
947+
private fun stateString(state: Any?): String = when (state) {
948+
is Finishing -> when {
949+
state.isCancelling -> "Cancelling"
950+
state.isCompleting -> "Completing"
951+
else -> "Active"
982952
}
953+
is Incomplete -> if (state.isActive) "Active" else "New"
954+
is CompletedExceptionally -> "Cancelled"
955+
else -> "Completed"
983956
}
984957

985958
// Completing & Cancelling states,
@@ -1068,7 +1041,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
10681041
delegate: Continuation<T>,
10691042
private val job: JobSupport
10701043
) : CancellableContinuationImpl<T>(delegate, MODE_CANCELLABLE) {
1071-
override fun getParentCancellationCause(parent: Job): Throwable {
1044+
override fun getContinuationCancellationCause(parent: Job): Throwable {
10721045
val state = job.state
10731046
/*
10741047
* When the job we are waiting for had already completely completed exceptionally or
@@ -1351,7 +1324,7 @@ internal class ChildContinuation(
13511324
@JvmField val child: AbstractContinuation<*>
13521325
) : JobCancellingNode<Job>(parent) {
13531326
override fun invoke(cause: Throwable?) {
1354-
child.cancelImpl(child.getParentCancellationCause(job))
1327+
child.cancelImpl(child.getContinuationCancellationCause(job))
13551328
}
13561329
override fun toString(): String =
13571330
"ChildContinuation[$child]"

core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt

+12-12
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import kotlin.coroutines.experimental.*
1313
import kotlin.test.*
1414

1515
@RunWith(Parameterized::class)
16-
class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestBase() {
16+
class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() {
17+
enum class Mode { WITH_CONTEXT, ASYNC_AWAIT }
18+
1719
companion object {
18-
@Parameterized.Parameters(name = "withContext={0}")
20+
@Parameterized.Parameters(name = "mode={0}")
1921
@JvmStatic
20-
fun params(): Collection<Array<Any>> = listOf<Array<Any>>(arrayOf(true), arrayOf(false))
22+
fun params(): Collection<Array<Any>> = Mode.values().map { arrayOf<Any>(it) }
2123
}
2224

2325
@Test
@@ -106,16 +108,14 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
106108
/*
107109
* context cancelled with ISE
108110
* block itself throws CE(IOE)
109-
* Result: ISE suppressed IOE
111+
* Result: ISE (because cancellation exception is always ignored and not handled)
110112
*/
111113
val cancellationCause = IllegalStateException()
112114
val thrown = CancellationException()
113115
thrown.initCause(IOException())
114116
runCancellation(cancellationCause, thrown) { e ->
115117
assertSame(cancellationCause, e)
116-
val suppressed = e.suppressed
117-
assertEquals(1, suppressed.size)
118-
assertTrue(suppressed[0] is IOException)
118+
assertTrue(e.suppressed.isEmpty())
119119
}
120120
}
121121

@@ -161,10 +161,11 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
161161

162162
@Test
163163
fun testThrowingCancellationWithCause() = runTest {
164+
// Exception are never unwrapped, so if CE(IOE) is thrown then it is the cancellation cause
164165
val thrown = CancellationException()
165166
thrown.initCause(IOException())
166167
runThrowing(thrown) { e ->
167-
checkException<IOException>(e)
168+
assertSame(thrown, e)
168169
}
169170
}
170171

@@ -247,12 +248,11 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
247248
}
248249

249250
private suspend fun withCtx(context: CoroutineContext, job: Job = Job(), block: suspend CoroutineScope.(Job) -> Nothing) {
250-
if (withContext) {
251-
withContext(context + job) {
251+
when (mode) {
252+
Mode.WITH_CONTEXT -> withContext(context + job) {
252253
block(job)
253254
}
254-
} else {
255-
CoroutineScope(coroutineContext).async(context + job) {
255+
Mode.ASYNC_AWAIT -> CoroutineScope(coroutineContext).async(context + job) {
256256
block(job)
257257
}.await()
258258
}

0 commit comments

Comments
 (0)