Skip to content

Commit f7a6334

Browse files
committed
Child fails on parent failure (not cancelled)
* This ensures transparency with respect to the fact that a job is a child in a hierarchy. When it fails, there is no side-effect of its being cancelled by its parent anymore. * ChildJob and ChildHandle are made internal to further deter 3rd-party implementation and usage of parent-child machinery.
1 parent ad84795 commit f7a6334

File tree

8 files changed

+127
-62
lines changed

8 files changed

+127
-62
lines changed

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

+2-8
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,6 @@ public final class kotlinx/coroutines/experimental/CancelledContinuation : kotli
9191
public fun <init> (Lkotlin/coroutines/experimental/Continuation;Ljava/lang/Throwable;)V
9292
}
9393

94-
public abstract interface class kotlinx/coroutines/experimental/ChildHandle : kotlinx/coroutines/experimental/DisposableHandle {
95-
public abstract fun childFailed (Ljava/lang/Throwable;)Z
96-
}
97-
9894
public abstract class kotlinx/coroutines/experimental/CloseableCoroutineDispatcher : kotlinx/coroutines/experimental/CoroutineDispatcher, java/io/Closeable {
9995
public fun <init> ()V
10096
}
@@ -373,9 +369,8 @@ public final class kotlinx/coroutines/experimental/GlobalScope : kotlinx/corouti
373369

374370
public abstract interface class kotlinx/coroutines/experimental/Job : kotlin/coroutines/experimental/CoroutineContext$Element {
375371
public static final field Key Lkotlinx/coroutines/experimental/Job$Key;
376-
public abstract fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/ChildHandle;
372+
public abstract fun attachChild (Lkotlinx/coroutines/experimental/ChildJob;)Lkotlinx/coroutines/experimental/ChildHandle;
377373
public abstract fun cancel (Ljava/lang/Throwable;)Z
378-
public abstract fun cancelChild (Lkotlinx/coroutines/experimental/Job;)V
379374
public abstract synthetic fun cancelChildren (Ljava/lang/Throwable;)V
380375
public abstract fun getCancellationException ()Ljava/util/concurrent/CancellationException;
381376
public abstract fun getChildren ()Lkotlin/sequences/Sequence;
@@ -446,9 +441,8 @@ public final class kotlinx/coroutines/experimental/LazyDeferredKt {
446441

447442
public final class kotlinx/coroutines/experimental/NonCancellable : kotlin/coroutines/experimental/AbstractCoroutineContextElement, kotlinx/coroutines/experimental/Job {
448443
public static final field INSTANCE Lkotlinx/coroutines/experimental/NonCancellable;
449-
public fun attachChild (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/ChildHandle;
444+
public fun attachChild (Lkotlinx/coroutines/experimental/ChildJob;)Lkotlinx/coroutines/experimental/ChildHandle;
450445
public fun cancel (Ljava/lang/Throwable;)Z
451-
public fun cancelChild (Lkotlinx/coroutines/experimental/Job;)V
452446
public synthetic fun cancelChildren (Ljava/lang/Throwable;)V
453447
public fun getCancellationException ()Ljava/util/concurrent/CancellationException;
454448
public fun getChildren ()Lkotlin/sequences/Sequence;

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

+28-17
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.coroutines.experimental.*
1616
/**
1717
* A background job. Conceptually, a job is a cancellable thing with a life-cycle that
1818
* culminates in its completion. Jobs can be arranged into parent-child hierarchies where failure or cancellation
19-
* of parent immediately cancels all its [children].
19+
* of parent lead to an immediate failure of all its [children].
2020
*
2121
* The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
2222
* `Job()` factory function.
@@ -69,7 +69,7 @@ import kotlin.coroutines.experimental.*
6969
* [coroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/coroutine-context.html)
7070
* represents the coroutine itself.
7171
*
72-
* A job can have a _parent_ job. A job with a parent is cancelled when its parent is cancelled or is failing.
72+
* A job can have a _parent_ job. A job with a parent is failing when its parent is cancelled or is failing.
7373
* Parent job waits in _completing_, _failing_, or _cancelling_ state for all its children to complete before finishing.
7474
* Note, that _completing_ state is purely internal to the job. For an outside observer a _completing_ job is still
7575
* active, while internally it is waiting for its children.
@@ -181,15 +181,6 @@ public interface Job : CoroutineContext.Element {
181181

182182
// ------------ parent-child ------------
183183

184-
/**
185-
* Cancels child job. This method is invoked by [parentJob] to cancel this child job.
186-
* Child finds the cancellation cause using [getCancellationException] of the [parentJob].
187-
* This method does nothing is the child is already being cancelled.
188-
*
189-
* @suppress **This is unstable API and it is subject to change.**
190-
*/
191-
public fun cancelChild(parentJob: Job)
192-
193184
/**
194185
* Returns a sequence of this job's children.
195186
*
@@ -216,21 +207,23 @@ public interface Job : CoroutineContext.Element {
216207
*
217208
* A parent-child relation has the following effect:
218209
* * Cancellation of parent with [cancel] or its exceptional completion (failure)
219-
* immediately cancels all its children.
210+
* immediately fails all its children.
220211
* * Parent cannot complete until all its children are complete. Parent waits for all its children to
221-
* complete in _completing_ or _cancelling_ state.
212+
* complete in _completing_, _failing_, or _cancelling_ states.
222213
*
223-
* **A child must store the resulting [DisposableHandle] and [dispose][DisposableHandle.dispose] the attachment
214+
* **A child must store the resulting [ChildHandle] and [dispose][DisposableHandle.dispose] the attachment
224215
* to its parent on its own completion.**
225216
*
226217
* Coroutine builders and job factory functions that accept `parent` [CoroutineContext] parameter
227218
* lookup a [Job] instance in the parent context and use this function to attach themselves as a child.
228-
* They also store a reference to the resulting [DisposableHandle] and dispose a handle when they complete.
219+
* They also store a reference to the resulting [ChildHandle] and dispose a handle when they complete.
229220
*
230221
* @suppress **This is unstable API and it is subject to change.**
231222
* This is an internal API. This method is too error prone for public API.
232223
*/
233-
public fun attachChild(child: Job): ChildHandle
224+
// ChildJob and ChildHandle are made internal on purpose to further deter 3rd-party impl of Job
225+
@Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "EXPOSED_PARAMETER_TYPE")
226+
public fun attachChild(child: ChildJob): ChildHandle
234227

235228
/**
236229
* Cancels all children jobs of this coroutine with the given [cause]. Unlike [cancel],
@@ -387,9 +380,27 @@ public interface DisposableHandle {
387380
}
388381

389382
/**
383+
* A reference that parent receives from its child so that it can report its failure.
384+
*
385+
* @suppress **This is unstable API and it is subject to change.**
386+
*/
387+
internal interface ChildJob : Job {
388+
/**
389+
* Parent is reporting failure to the child by invoking this method.
390+
* Child finds the failure cause using [getCancellationException] of the [parentJob].
391+
* This method does nothing is the child is already failing.
392+
*
393+
* @suppress **This is unstable API and it is subject to change.**
394+
*/
395+
public fun parentFailed(parentJob: Job)
396+
}
397+
398+
/**
399+
* A handle that child keep onto its parent so that it is able to report its failure.
400+
*
390401
* @suppress **This is unstable API and it is subject to change.**
391402
*/
392-
public interface ChildHandle : DisposableHandle {
403+
internal interface ChildHandle : DisposableHandle {
393404
/**
394405
* Child is reporting failure to the parent by invoking this method.
395406
* This method is invoked by the child twice. The first time child report its root cause as soon as possible,

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

+25-25
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import kotlin.coroutines.experimental.*
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, SelectClause0 {
23+
internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, SelectClause0 {
2424
final override val key: CoroutineContext.Key<*> get() = Job
2525

2626
/*
@@ -476,7 +476,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
476476
rootCause = state.rootCause // != null if we are failing
477477
// We add node to the list in two cases --- either the job is not failing
478478
// or we are adding a child to a coroutine that is not completing yet
479-
if (rootCause == null || handler.isHandlerOf<ChildJob>() && !state.isCompleting) {
479+
if (rootCause == null || handler.isHandlerOf<ChildHandleImpl>() && !state.isCompleting) {
480480
// Note: add node the list while holding lock on state (make sure it cannot change)
481481
val node = nodeCache ?: makeNode(handler, onFailing).also { nodeCache = it }
482482
if (!addLastAtomic(state, list, node)) return@loopOnState // retry
@@ -613,15 +613,15 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
613613
public override fun cancel(cause: Throwable?): Boolean =
614614
fail(cause, cancel = true) && handlesException
615615

616+
// parent is reporting failure to a child child
617+
public final override fun parentFailed(parentJob: Job) {
618+
fail(parentJob, cancel = false)
619+
}
620+
616621
// child is reporting failure to the parent
617-
internal fun childFailed(cause: Throwable) =
622+
public fun childFailed(cause: Throwable) =
618623
fail(cause, cancel = false) && handlesException
619624

620-
// parent is cancelling child
621-
public override fun cancelChild(parentJob: Job) {
622-
fail(parentJob, cancel = true)
623-
}
624-
625625
// cause is Throwable or Job when cancelChild was invoked, cause can be null only on cancel
626626
// returns true is exception was handled, false otherwise
627627
private fun fail(cause: Any?, cancel: Boolean): Boolean {
@@ -783,7 +783,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
783783
* Otherwise, there can be a race between (completed state -> handled exception and newly attached child/join)
784784
* which may miss unhandled exception.
785785
*/
786-
if ((state is Empty || state is JobNode<*>) && state !is ChildJob && proposedUpdate !is CompletedExceptionally) {
786+
if ((state is Empty || state is JobNode<*>) && state !is ChildHandleImpl && proposedUpdate !is CompletedExceptionally) {
787787
if (!tryFinalizeSimpleState(state, proposedUpdate, mode)) return COMPLETING_RETRY
788788
return COMPLETING_COMPLETED
789789
}
@@ -833,11 +833,11 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
833833
get() = (this as? CompletedExceptionally)?.cause
834834

835835
private fun firstChild(state: Incomplete) =
836-
state as? ChildJob ?: state.list?.nextChild()
836+
state as? ChildHandleImpl ?: state.list?.nextChild()
837837

838838
// return false when there is no more incomplete children to wait
839839
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
840-
private tailrec fun tryWaitForChild(state: Finishing, child: ChildJob, proposedUpdate: Any?): Boolean {
840+
private tailrec fun tryWaitForChild(state: Finishing, child: ChildHandleImpl, proposedUpdate: Any?): Boolean {
841841
val handle = child.childJob.invokeOnCompletion(
842842
invokeImmediately = false,
843843
handler = ChildCompletion(this, state, child, proposedUpdate).asHandler
@@ -848,7 +848,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
848848
}
849849

850850
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
851-
private fun continueCompleting(state: Finishing, lastChild: ChildJob, proposedUpdate: Any?) {
851+
private fun continueCompleting(state: Finishing, lastChild: ChildHandleImpl, proposedUpdate: Any?) {
852852
require(this.state === state) // consistency check -- it cannot change while we are waiting for children
853853
// figure out if we need to wait for next child
854854
val waitChild = lastChild.nextChild()
@@ -858,39 +858,39 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
858858
if (tryFinalizeFinishingState(state, proposedUpdate, MODE_ATOMIC_DEFAULT)) return
859859
}
860860

861-
private fun LockFreeLinkedListNode.nextChild(): ChildJob? {
861+
private fun LockFreeLinkedListNode.nextChild(): ChildHandleImpl? {
862862
var cur = this
863863
while (cur.isRemoved) cur = cur.prevNode // rollback to prev non-removed (or list head)
864864
while (true) {
865865
cur = cur.nextNode
866866
if (cur.isRemoved) continue
867-
if (cur is ChildJob) return cur
867+
if (cur is ChildHandleImpl) return cur
868868
if (cur is NodeList) return null // checked all -- no more children
869869
}
870870
}
871871

872872
public final override val children: Sequence<Job> get() = buildSequence {
873873
val state = this@JobSupport.state
874874
when (state) {
875-
is ChildJob -> yield(state.childJob)
875+
is ChildHandleImpl -> yield(state.childJob)
876876
is Incomplete -> state.list?.let { list ->
877-
list.forEach<ChildJob> { yield(it.childJob) }
877+
list.forEach<ChildHandleImpl> { yield(it.childJob) }
878878
}
879879
}
880880
}
881881

882882
@Suppress("OverridingDeprecatedMember")
883-
public final override fun attachChild(child: Job): ChildHandle {
883+
public final override fun attachChild(child: ChildJob): ChildHandle {
884884
/*
885885
* Note: This function attaches a special ChildNode object. This node object
886886
* is handled in a special way on completion on the coroutine (we wait for all of them) and
887887
* is handled specially by invokeOnCompletion itself -- it adds this node to the list even
888888
* if the job is already failing. For "failing" state child is attached under state lock.
889889
* It's required to properly wait all children before completion and provide linearizable hierarchy view:
890-
* If child is attached when job is failing, such child will receive immediate cancellation exception,
890+
* If child is attached when job is failing, such child will receive immediate notification on failure,
891891
* but parent *will* wait for that child before completion and will handle its exception.
892892
*/
893-
return invokeOnCompletion(onFailing = true, handler = ChildJob(this, child).asHandler) as ChildHandle
893+
return invokeOnCompletion(onFailing = true, handler = ChildHandleImpl(this, child).asHandler) as ChildHandle
894894
}
895895

896896
@Suppress("OverridingDeprecatedMember")
@@ -1058,7 +1058,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
10581058
private class ChildCompletion(
10591059
private val parent: JobSupport,
10601060
private val state: Finishing,
1061-
private val child: ChildJob,
1061+
private val child: ChildHandleImpl,
10621062
private val proposedUpdate: Any?
10631063
) : JobNode<Job>(child.childJob) {
10641064
override fun invoke(cause: Throwable?) {
@@ -1301,16 +1301,16 @@ private class InvokeOnFailing(
13011301
override fun toString() = "InvokeOnFailing[$classSimpleName@$hexAddress]"
13021302
}
13031303

1304-
internal class ChildJob(
1304+
internal class ChildHandleImpl(
13051305
parent: JobSupport,
1306-
@JvmField val childJob: Job
1306+
@JvmField val childJob: ChildJob
13071307
) : JobFailingNode<JobSupport>(parent), ChildHandle {
1308-
override fun invoke(cause: Throwable?) = childJob.cancelChild(job)
1308+
override fun invoke(cause: Throwable?) = childJob.parentFailed(job)
13091309
override fun childFailed(cause: Throwable): Boolean = job.childFailed(cause)
1310-
override fun toString(): String = "ChildJob[$childJob]"
1310+
override fun toString(): String = "ChildHandle[$childJob]"
13111311
}
13121312

1313-
// Same as ChildJob, but for cancellable continuation
1313+
// Same as ChildHandleImpl, but for cancellable continuation
13141314
internal class ChildContinuation(
13151315
parent: Job,
13161316
@JvmField val child: AbstractContinuation<*>

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,17 @@ public object NonCancellable : AbstractCoroutineContextElement(Job), Job {
6868
/** Always returns `false`. */
6969
override fun cancel(cause: Throwable?): Boolean = false // never handles exceptions
7070

71-
/** @suppress */
72-
override fun cancelChild(parentJob: Job): Unit = error("Cannot be invoked, does not have a parent")
73-
7471
/** Always returns [emptySequence]. */
7572
override val children: Sequence<Job>
7673
get() = emptySequence()
7774

78-
/** Always returns [NonDisposableHandle] and does not do anything. */
79-
@Suppress("OverridingDeprecatedMember")
80-
override fun attachChild(child: Job): ChildHandle = NonDisposableHandle
75+
/**
76+
* Always returns [NonDisposableHandle] and does not do anything.
77+
* @suppress **This is unstable API and it is subject to change.**
78+
* This is an internal API. This method is too error prone for public API.
79+
*/
80+
@Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "EXPOSED_PARAMETER_TYPE")
81+
override fun attachChild(child: ChildJob): ChildHandle = NonDisposableHandle
8182

8283
/** Does not do anything. */
8384
@Suppress("OverridingDeprecatedMember")

common/kotlinx-coroutines-core-common/test/CompletableDeferredTest.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,19 @@ class CompletableDeferredTest : TestBase() {
101101
}
102102

103103
@Test
104-
fun testParentCancelsChild() {
104+
fun testParentFailsChild() {
105105
val parent = Job()
106106
val c = CompletableDeferred<String>(parent)
107107
checkFresh(c)
108108
parent.cancel()
109109
assertEquals(false, parent.isActive)
110110
assertEquals(true, parent.isCancelled)
111-
checkCancel(c)
111+
assertEquals(false, c.isActive)
112+
assertEquals(false, c.isCancelled)
113+
assertEquals(true, c.isCompleted)
114+
assertEquals(true, c.isCompletedExceptionally)
115+
assertThrows<CancellationException> { c.getCompleted() }
116+
assertTrue(c.getCompletionExceptionOrNull() is CancellationException)
112117
}
113118

114119
@Test

common/kotlinx-coroutines-core-common/test/CoroutinesTest.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -286,15 +286,15 @@ class CoroutinesTest : TestBase() {
286286
}
287287

288288
@Test
289-
fun testParentCrashCancelsChildren() = runTest(
289+
fun testParentCrashFailsChildren() = runTest(
290290
unhandled = listOf({ it -> it is TestException })
291291
) {
292292
expect(1)
293293
val parent = launch(Job()) {
294294
expect(4)
295295
throw TestException("Crashed")
296296
}
297-
launch(parent, CoroutineStart.UNDISPATCHED) {
297+
val child = launch(parent, CoroutineStart.UNDISPATCHED) {
298298
expect(2)
299299
try {
300300
yield() // to test
@@ -310,6 +310,9 @@ class CoroutinesTest : TestBase() {
310310
expect(6)
311311
parent.join() // make sure crashed parent still waits for its child
312312
finish(8)
313+
// make sure child fails but it is not cancelled
314+
assertTrue(child.isFailed)
315+
assertFalse(child.isCancelled)
313316
}
314317

315318
@Test

0 commit comments

Comments
 (0)