Skip to content

Commit fe51b70

Browse files
committed
Improve the docs
1 parent 4a4ada0 commit fe51b70

File tree

1 file changed

+60
-12
lines changed

1 file changed

+60
-12
lines changed

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

+60-12
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,47 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
464464
val node: JobNode = makeNode(handler, onCancelling)
465465
val added = tryPutNodeIntoList(node) { state, list ->
466466
if (onCancelling) {
467+
/**
468+
* We are querying whether the job was already cancelled when we entered this block.
469+
* We can't naively attempt to add the node to the list, because a lot of time could pass between
470+
* notifying the cancellation handlers (and thus closing the list, forcing us to retry)
471+
* and reaching a final state.
472+
*
473+
* Alternatively, we could also try to add the node to the list first and then read the latest state
474+
* to check for an exception, but that logic would need to manually handle the final state, which is
475+
* less straightforward.
476+
*/
467477
val rootCause = (state as? Finishing)?.let { synchronized(it) { it.rootCause } }
468478
if (rootCause == null) {
479+
/**
480+
* There is no known root cause yet, so we can add the node to the list of state handlers.
481+
*
482+
* If this call fails, because of the bitmask, this means one of the two happened:
483+
* - [notifyCancelling] was already called.
484+
* This means that the job is already being cancelled: otherwise, with what exception would we
485+
* notify the handler?
486+
* So, we can retry the operation: either the state is already final, or the `rootCause` check
487+
* above will give a different result.
488+
* - [notifyCompletion] was already called.
489+
* This means that the job is already complete.
490+
* We can retry the operation and will observe the final state.
491+
*/
469492
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION)
470493
} else {
494+
/**
495+
* The root cause is known, so we can invoke the handler immediately and avoid adding it.
496+
*/
471497
if (invokeImmediately) handler.invoke(rootCause)
472498
return NonDisposableHandle
473499
}
474500
} else {
501+
/**
502+
* The non-[onCancelling]-handlers are interested in completions only, so it's safe to add them at
503+
* any time before [notifyCompletion] is called (which closes the list).
504+
*
505+
* If the list *is* closed, on a retry, we'll observe the final state, as [notifyCompletion] is only
506+
* called after the state transition.
507+
*/
475508
list.addLast(node, LIST_ON_COMPLETION_PERMISSION)
476509
}
477510
}
@@ -979,7 +1012,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
9791012
* Note: This function attaches a special ChildHandleNode node object. This node object
9801013
* is handled in a special way on completion on the coroutine (we wait for all of them) and also
9811014
* can't be added simply with `invokeOnCompletionInternal` -- we add this node to the list even
982-
* if the job is already cancelling. For cancelling state, the child is attached under state lock.
1015+
* if the job is already cancelling.
9831016
* It's required to properly await all children before completion and provide a linearizable hierarchy view:
9841017
* If the child is attached when the job is already being cancelled, such a child will receive
9851018
* an immediate notification on cancellation,
@@ -996,39 +1029,54 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
9961029
// The child managed to be added before the parent started to cancel or complete. Success.
9971030
true
9981031
} else {
999-
// Either cancellation or completion already happened, the child was not added.
1000-
// Now we need to try adding it for completion.
1032+
/* Either cancellation or completion already happened, the child was not added.
1033+
* Now we need to try adding it just for completion. */
10011034
val addedBeforeCompletion = list.addLast(
10021035
node,
10031036
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
10041037
)
1005-
// Whether or not we managed to add the child before the parent completed, we need to investigate:
1006-
// why didn't we manage to add it before cancellation?
1007-
// If it's because cancellation happened in the meantime, we need to notify the child.
1008-
// We check the latest state because the original state with which we started may not have had
1009-
// the information about the cancellation yet.
1038+
/*
1039+
* Whether or not we managed to add the child before the parent completed, we need to investigate:
1040+
* why didn't we manage to add it before cancellation?
1041+
* If it's because cancellation happened in the meantime, we need to notify the child about it.
1042+
* We check the latest state because the original state with which we started may not have had
1043+
* the information about the cancellation yet.
1044+
*/
10101045
val rootCause = when (val latestState = this.state) {
10111046
is Finishing -> {
10121047
// The state is still incomplete, so we need to notify the child about the completion cause.
10131048
synchronized(latestState) { latestState.rootCause }
10141049
}
10151050
else -> {
1016-
// Since the list is already closed for `onCancelling`, the job is either Finishing or
1017-
// already completed. We need to notify the child about the completion cause.
1051+
/** Since the list is already closed for [onCancelling], the job is either Finishing or
1052+
* already completed. We need to notify the child about the completion cause. */
10181053
assert { latestState !is Incomplete }
10191054
(latestState as? CompletedExceptionally)?.cause
10201055
}
10211056
}
1057+
/**
1058+
* We must cancel the child if the parent was cancelled already, even if we successfully attached,
1059+
* as this child didn't make it before [notifyCancelling] and won't be notified that it should be
1060+
* cancelled.
1061+
*
1062+
* And if the parent wasn't cancelled and the previous [LockFreeLinkedListNode.addLast] failed because
1063+
* the job is in its final state already, we won't be able to attach anyway, so we must just invoke
1064+
* the handler and return.
1065+
*/
1066+
node.invoke(rootCause)
10221067
if (addedBeforeCompletion) {
1023-
if (rootCause != null) node.invoke(rootCause)
1068+
/** The root cause can't be null: since the earlier addition to the list failed, this means that
1069+
* the job was already cancelled or completed. */
1070+
assert { rootCause != null }
10241071
true
10251072
} else {
1026-
node.invoke(rootCause)
1073+
/** No sense in retrying: we know it won't succeed, and we already invoked the handler. */
10271074
return NonDisposableHandle
10281075
}
10291076
}
10301077
}
10311078
if (added) return node
1079+
/** We can only end up here if [tryPutNodeIntoList] detected a final state. */
10321080
node.invoke((state as? CompletedExceptionally)?.cause)
10331081
return NonDisposableHandle
10341082
}

0 commit comments

Comments
 (0)