@@ -464,8 +464,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
464
464
if (state.isActive) {
465
465
// try move to SINGLE state
466
466
if (_state .compareAndSet(state, node)) return node
467
- } else
467
+ } else {
468
468
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
469
+ }
469
470
}
470
471
is Incomplete -> {
471
472
val list = state.list
@@ -522,7 +523,32 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
522
523
return node
523
524
}
524
525
525
- private fun addLastAtomic (expect : Any , list : NodeList , node : JobNode ) =
526
+ /* *
527
+ * Adds the [node] to the given [list] if the state of this job is equal to [expect].
528
+ *
529
+ * This method is invoked only from [invokeOnCompletion] (two places) and [expect] is always a snapshot of the
530
+ * `state` that [invokeOnCompletion] works on.
531
+ *
532
+ * It is required to preserve the following invariants:
533
+ *
534
+ * 1) Exactly once handler invocation.
535
+ * Without atomic addIf, the following scenario is possible:
536
+ * * T1 reads `state`, attempts to add a handler to the list, gets preempted
537
+ * * T2 finalizes the state, CASes it and invokes all the handlers from the list
538
+ * * T1 adds its handler to the list to be never invoked
539
+ * If T1 starts to re-check the state after the addition, then the handler might be invoked twice.
540
+ *
541
+ * 2) Parent finalization.
542
+ *
543
+ * NB: it's totally unobvious from the signature, but [tryWaitForChildren] if's over `invokeOnCompletion` result:
544
+ * if it's `NonDisposableHandle` then the addition failed, otherwise it's successfully added.
545
+ * If handler fails to be added, then child is cancelled/completed and there is no need to
546
+ * either wait for it **or** invoke its handler.
547
+ *
548
+ * Additional implementation note: all internal handlers implement [LockFreeLinkedListNode] and
549
+ * LFLL **does not** support additions and removals of the same node (with the same identity) more than once.
550
+ */
551
+ private fun addLastAtomic (expect : Any , list : NodeList , node : JobNode ): Boolean =
526
552
list.addLastIf(node) { this .state == = expect }
527
553
528
554
private fun promoteEmptyToNodeList (state : Empty ) {
@@ -904,7 +930,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
904
930
notifyRootCause?.let { notifyCancelling(list, it) }
905
931
// now wait for children
906
932
val child = firstChild(state)
907
- if (child != null && tryWaitForChild (finishing, child, proposedUpdate))
933
+ if (child != null && tryWaitForChildren (finishing, child, proposedUpdate))
908
934
return COMPLETING_WAITING_CHILDREN
909
935
// otherwise -- we have not children left (all were already cancelled?)
910
936
return finalizeFinishingState(finishing, proposedUpdate)
@@ -916,16 +942,26 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
916
942
private fun firstChild (state : Incomplete ) =
917
943
state as ? ChildHandleNode ? : state.list?.nextChild()
918
944
919
- // return false when there is no more incomplete children to wait
920
- // ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
921
- private tailrec fun tryWaitForChild (state : Finishing , child : ChildHandleNode , proposedUpdate : Any? ): Boolean {
945
+ /* *
946
+ * This method is invoked by the JobSupport lifecycle when it transitions into "completing" state (also includes "cancelling"),
947
+ * and in order to be transitioned into its final state it has to wait for all the children.
948
+ * If this method returns `true` then the current job is waiting for its children.
949
+ * It's done via an addition of a special completion handler to the child that, when invoked, invokes
950
+ * [continueCompleting] that almost immediately fallbacks to [tryWaitForChildren].
951
+ *
952
+ * If it returns `false` then there is no children to wait for and it's safe to finalize the final job's state.
953
+ * Note that at this point, new children can appear:
954
+ * * For `cancelling` state they are immediately cancelled.
955
+ * * For `completing` state they are not cancelled and left hanging out to dry.
956
+ */
957
+ private tailrec fun tryWaitForChildren (state : Finishing , child : ChildHandleNode , proposedUpdate : Any? ): Boolean {
922
958
val handle = child.childJob.invokeOnCompletion(
923
959
invokeImmediately = false ,
924
960
handler = ChildCompletion (this , state, child, proposedUpdate).asHandler
925
961
)
926
962
if (handle != = NonDisposableHandle ) return true // child is not complete and we've started waiting for it
927
963
val nextChild = child.nextChild() ? : return false
928
- return tryWaitForChild (state, nextChild, proposedUpdate)
964
+ return tryWaitForChildren (state, nextChild, proposedUpdate)
929
965
}
930
966
931
967
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
@@ -934,7 +970,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
934
970
// figure out if we need to wait for next child
935
971
val waitChild = lastChild.nextChild()
936
972
// try wait for next child
937
- if (waitChild != null && tryWaitForChild (state, waitChild, proposedUpdate)) return // waiting for next child
973
+ if (waitChild != null && tryWaitForChildren (state, waitChild, proposedUpdate)) return // waiting for next child
938
974
// no more children to wait -- try update state
939
975
val finalState = finalizeFinishingState(state, proposedUpdate)
940
976
afterCompletion(finalState)
0 commit comments