@@ -464,14 +464,47 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
464
464
val node: JobNode = makeNode(handler, onCancelling)
465
465
val added = tryPutNodeIntoList(node) { state, list ->
466
466
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
+ */
467
477
val rootCause = (state as ? Finishing )?.let { synchronized(it) { it.rootCause } }
468
478
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
+ */
469
492
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION )
470
493
} else {
494
+ /* *
495
+ * The root cause is known, so we can invoke the handler immediately and avoid adding it.
496
+ */
471
497
if (invokeImmediately) handler.invoke(rootCause)
472
498
return NonDisposableHandle
473
499
}
474
500
} 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
+ */
475
508
list.addLast(node, LIST_ON_COMPLETION_PERMISSION )
476
509
}
477
510
}
@@ -979,7 +1012,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
979
1012
* Note: This function attaches a special ChildHandleNode node object. This node object
980
1013
* is handled in a special way on completion on the coroutine (we wait for all of them) and also
981
1014
* 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.
983
1016
* It's required to properly await all children before completion and provide a linearizable hierarchy view:
984
1017
* If the child is attached when the job is already being cancelled, such a child will receive
985
1018
* an immediate notification on cancellation,
@@ -996,39 +1029,54 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
996
1029
// The child managed to be added before the parent started to cancel or complete. Success.
997
1030
true
998
1031
} 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. */
1001
1034
val addedBeforeCompletion = list.addLast(
1002
1035
node,
1003
1036
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
1004
1037
)
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
+ */
1010
1045
val rootCause = when (val latestState = this .state) {
1011
1046
is Finishing -> {
1012
1047
// The state is still incomplete, so we need to notify the child about the completion cause.
1013
1048
synchronized(latestState) { latestState.rootCause }
1014
1049
}
1015
1050
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. */
1018
1053
assert { latestState !is Incomplete }
1019
1054
(latestState as ? CompletedExceptionally )?.cause
1020
1055
}
1021
1056
}
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)
1022
1067
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 }
1024
1071
true
1025
1072
} else {
1026
- node.invoke(rootCause)
1073
+ /* * No sense in retrying: we know it won't succeed, and we already invoked the handler. */
1027
1074
return NonDisposableHandle
1028
1075
}
1029
1076
}
1030
1077
}
1031
1078
if (added) return node
1079
+ /* * We can only end up here if [tryPutNodeIntoList] detected a final state. */
1032
1080
node.invoke((state as ? CompletedExceptionally )?.cause)
1033
1081
return NonDisposableHandle
1034
1082
}
0 commit comments