@@ -466,14 +466,47 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
466
466
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
467
467
val added = tryPutNodeIntoList(node) { state, list ->
468
468
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
+ */
469
479
val rootCause = (state as ? Finishing )?.let { synchronized(it) { it.rootCause } }
470
480
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
+ */
471
494
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION )
472
495
} else {
496
+ /* *
497
+ * The root cause is known, so we can invoke the handler immediately and avoid adding it.
498
+ */
473
499
if (invokeImmediately) node.invoke(rootCause)
474
500
return NonDisposableHandle
475
501
}
476
502
} 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
+ */
477
510
list.addLast(node, LIST_ON_COMPLETION_PERMISSION )
478
511
}
479
512
}
@@ -969,7 +1002,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
969
1002
* Note: This function attaches a special ChildHandleNode node object. This node object
970
1003
* is handled in a special way on completion on the coroutine (we wait for all of them) and also
971
1004
* 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.
973
1006
* It's required to properly await all children before completion and provide a linearizable hierarchy view:
974
1007
* If the child is attached when the job is already being cancelled, such a child will receive
975
1008
* an immediate notification on cancellation,
@@ -986,39 +1019,54 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
986
1019
// The child managed to be added before the parent started to cancel or complete. Success.
987
1020
true
988
1021
} 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. */
991
1024
val addedBeforeCompletion = list.addLast(
992
1025
node,
993
1026
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
994
1027
)
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
+ */
1000
1035
val rootCause = when (val latestState = this .state) {
1001
1036
is Finishing -> {
1002
1037
// The state is still incomplete, so we need to notify the child about the completion cause.
1003
1038
synchronized(latestState) { latestState.rootCause }
1004
1039
}
1005
1040
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. */
1008
1043
assert { latestState !is Incomplete }
1009
1044
(latestState as ? CompletedExceptionally )?.cause
1010
1045
}
1011
1046
}
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)
1012
1057
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 }
1014
1061
true
1015
1062
} else {
1016
- node.invoke(rootCause)
1063
+ /* * No sense in retrying: we know it won't succeed, and we already invoked the handler. */
1017
1064
return NonDisposableHandle
1018
1065
}
1019
1066
}
1020
1067
}
1021
1068
if (added) return node
1069
+ /* * We can only end up here if [tryPutNodeIntoList] detected a final state. */
1022
1070
node.invoke((state as ? CompletedExceptionally )?.cause)
1023
1071
return NonDisposableHandle
1024
1072
}
0 commit comments