Skip to content

Commit 970da72

Browse files
committed
Improve the docs
1 parent 815db36 commit 970da72

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
@@ -466,14 +466,47 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
466466
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
467467
val added = tryPutNodeIntoList(node) { state, list ->
468468
if (node.onCancelling) {
469+
/**
470+
* We are querying whether the job was already cancelled when we entered this block.
471+
* We can't naively attempt to add the node to the list, because a lot of time could pass between
472+
* notifying the cancellation handlers (and thus closing the list, forcing us to retry)
473+
* and reaching a final state.
474+
*
475+
* Alternatively, we could also try to add the node to the list first and then read the latest state
476+
* to check for an exception, but that logic would need to manually handle the final state, which is
477+
* less straightforward.
478+
*/
469479
val rootCause = (state as? Finishing)?.let { synchronized(it) { it.rootCause } }
470480
if (rootCause == null) {
481+
/**
482+
* There is no known root cause yet, so we can add the node to the list of state handlers.
483+
*
484+
* If this call fails, because of the bitmask, this means one of the two happened:
485+
* - [notifyCancelling] was already called.
486+
* This means that the job is already being cancelled: otherwise, with what exception would we
487+
* notify the handler?
488+
* So, we can retry the operation: either the state is already final, or the `rootCause` check
489+
* above will give a different result.
490+
* - [notifyCompletion] was already called.
491+
* This means that the job is already complete.
492+
* We can retry the operation and will observe the final state.
493+
*/
471494
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION)
472495
} else {
496+
/**
497+
* The root cause is known, so we can invoke the handler immediately and avoid adding it.
498+
*/
473499
if (invokeImmediately) node.invoke(rootCause)
474500
return NonDisposableHandle
475501
}
476502
} else {
503+
/**
504+
* The non-[onCancelling]-handlers are interested in completions only, so it's safe to add them at
505+
* any time before [notifyCompletion] is called (which closes the list).
506+
*
507+
* If the list *is* closed, on a retry, we'll observe the final state, as [notifyCompletion] is only
508+
* called after the state transition.
509+
*/
477510
list.addLast(node, LIST_ON_COMPLETION_PERMISSION)
478511
}
479512
}
@@ -969,7 +1002,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
9691002
* Note: This function attaches a special ChildHandleNode node object. This node object
9701003
* is handled in a special way on completion on the coroutine (we wait for all of them) and also
9711004
* can't be added simply with `invokeOnCompletionInternal` -- we add this node to the list even
972-
* if the job is already cancelling. For cancelling state, the child is attached under state lock.
1005+
* if the job is already cancelling.
9731006
* It's required to properly await all children before completion and provide a linearizable hierarchy view:
9741007
* If the child is attached when the job is already being cancelled, such a child will receive
9751008
* an immediate notification on cancellation,
@@ -986,39 +1019,54 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
9861019
// The child managed to be added before the parent started to cancel or complete. Success.
9871020
true
9881021
} else {
989-
// Either cancellation or completion already happened, the child was not added.
990-
// Now we need to try adding it for completion.
1022+
/* Either cancellation or completion already happened, the child was not added.
1023+
* Now we need to try adding it just for completion. */
9911024
val addedBeforeCompletion = list.addLast(
9921025
node,
9931026
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
9941027
)
995-
// Whether or not we managed to add the child before the parent completed, we need to investigate:
996-
// why didn't we manage to add it before cancellation?
997-
// If it's because cancellation happened in the meantime, we need to notify the child.
998-
// We check the latest state because the original state with which we started may not have had
999-
// the information about the cancellation yet.
1028+
/*
1029+
* Whether or not we managed to add the child before the parent completed, we need to investigate:
1030+
* why didn't we manage to add it before cancellation?
1031+
* If it's because cancellation happened in the meantime, we need to notify the child about it.
1032+
* We check the latest state because the original state with which we started may not have had
1033+
* the information about the cancellation yet.
1034+
*/
10001035
val rootCause = when (val latestState = this.state) {
10011036
is Finishing -> {
10021037
// The state is still incomplete, so we need to notify the child about the completion cause.
10031038
synchronized(latestState) { latestState.rootCause }
10041039
}
10051040
else -> {
1006-
// Since the list is already closed for `onCancelling`, the job is either Finishing or
1007-
// already completed. We need to notify the child about the completion cause.
1041+
/** Since the list is already closed for [onCancelling], the job is either Finishing or
1042+
* already completed. We need to notify the child about the completion cause. */
10081043
assert { latestState !is Incomplete }
10091044
(latestState as? CompletedExceptionally)?.cause
10101045
}
10111046
}
1047+
/**
1048+
* We must cancel the child if the parent was cancelled already, even if we successfully attached,
1049+
* as this child didn't make it before [notifyCancelling] and won't be notified that it should be
1050+
* cancelled.
1051+
*
1052+
* And if the parent wasn't cancelled and the previous [LockFreeLinkedListNode.addLast] failed because
1053+
* the job is in its final state already, we won't be able to attach anyway, so we must just invoke
1054+
* the handler and return.
1055+
*/
1056+
node.invoke(rootCause)
10121057
if (addedBeforeCompletion) {
1013-
if (rootCause != null) node.invoke(rootCause)
1058+
/** The root cause can't be null: since the earlier addition to the list failed, this means that
1059+
* the job was already cancelled or completed. */
1060+
assert { rootCause != null }
10141061
true
10151062
} else {
1016-
node.invoke(rootCause)
1063+
/** No sense in retrying: we know it won't succeed, and we already invoked the handler. */
10171064
return NonDisposableHandle
10181065
}
10191066
}
10201067
}
10211068
if (added) return node
1069+
/** We can only end up here if [tryPutNodeIntoList] detected a final state. */
10221070
node.invoke((state as? CompletedExceptionally)?.cause)
10231071
return NonDisposableHandle
10241072
}

0 commit comments

Comments
 (0)