@@ -462,73 +462,57 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
462
462
node : JobNode
463
463
): DisposableHandle {
464
464
node.job = this
465
+ // Create node upfront -- for common cases it just initializes JobNode.job field,
466
+ // for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
467
+ val added = tryPutNodeIntoList(node) { state, list ->
468
+ if (node.onCancelling) {
469
+ val rootCause = (state as ? Finishing )?.let { synchronized(it) { it.rootCause } }
470
+ if (rootCause == null ) {
471
+ list.addLast(node, allowedAfterPartialClosing = false )
472
+ } else {
473
+ if (invokeImmediately) node.invoke(rootCause)
474
+ return NonDisposableHandle
475
+ }
476
+ } else {
477
+ list.addLast(node, allowedAfterPartialClosing = true )
478
+ }
479
+ }
480
+ when {
481
+ added -> return node
482
+ invokeImmediately -> node.invoke((state as ? CompletedExceptionally )?.cause)
483
+ }
484
+ return NonDisposableHandle
485
+ }
486
+
487
+ /* *
488
+ * Puts [node] into the current state's list of completion handlers.
489
+ *
490
+ * Returns `false` if the state is already complete and doesn't accept new handlers.
491
+ * Returns `true` if the handler was successfully added to the list.
492
+ *
493
+ * [tryAdd] is invoked when the state is [Incomplete] and the list is not `null`, to decide on the specific
494
+ * behavior in this case. It must return
495
+ * - `true` if the element was successfully added to the list
496
+ * - `false` if the operation needs to be retried
497
+ */
498
+ private inline fun tryPutNodeIntoList (
499
+ node : JobNode ,
500
+ tryAdd : (Incomplete , NodeList ) -> Boolean
501
+ ): Boolean {
465
502
loopOnState { state ->
466
503
when (state) {
467
504
is Empty -> { // EMPTY_X state -- no completion handlers
468
505
if (state.isActive) {
469
- // try move to SINGLE state
470
- if (_state .compareAndSet(state, node)) return node
506
+ // try to move to the SINGLE state
507
+ if (_state .compareAndSet(state, node)) return true
471
508
} else
472
509
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
473
510
}
474
- is Incomplete -> {
475
- val list = state.list
476
- if (list == null ) { // SINGLE/SINGLE+
477
- promoteSingleToNodeList(state as JobNode )
478
- } else {
479
- var rootCause: Throwable ? = null
480
- var handle: DisposableHandle = NonDisposableHandle
481
- if (node.onCancelling && state is Finishing ) {
482
- synchronized(state) {
483
- // check if we are installing cancellation handler on job that is being cancelled
484
- rootCause = state.rootCause // != null if cancelling job
485
- // We add node to the list in two cases --- either the job is not being cancelled
486
- // or we are adding a child to a coroutine that is not completing yet
487
- if (rootCause == null || node is ChildHandleNode && ! state.isCompleting) {
488
- // Note: add node the list while holding lock on state (make sure it cannot change)
489
- if (! list.addLast(
490
- node,
491
- allowedAfterPartialClosing = node is ChildHandleNode
492
- )
493
- ) return @loopOnState // retry
494
- // just return node if we don't have to invoke handler (not cancelling yet)
495
- if (rootCause == null ) return node
496
- // otherwise handler is invoked immediately out of the synchronized section & handle returned
497
- handle = node
498
- }
499
- }
500
- }
501
- if (rootCause != null ) {
502
- // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
503
- if (invokeImmediately) node.invoke(rootCause)
504
- return handle
505
- } else if (list.addLast(
506
- node, allowedAfterPartialClosing = ! node.onCancelling || node is ChildHandleNode
507
- )) {
508
- if (node is ChildHandleNode ) {
509
- /* * Handling the following case:
510
- * - A child requested to be added to the list;
511
- * - We checked the state and saw that it wasn't `Finishing`;
512
- * - Then, the job got cancelled and notified everyone about it;
513
- * - Only then did we add the child to the list
514
- * - and ended up here.
515
- */
516
- val latestState = this @JobSupport.state
517
- if (latestState is Finishing ) {
518
- assert { invokeImmediately }
519
- synchronized(latestState) { latestState.rootCause }?.let { node.invoke(it) }
520
- }
521
- }
522
- return node
523
- }
524
- }
525
- }
526
- else -> { // is complete
527
- // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
528
- // because we play type tricks on Kotlin/JS and handler is not necessarily a function there
529
- if (invokeImmediately) node.invoke((state as ? CompletedExceptionally )?.cause)
530
- return NonDisposableHandle
511
+ is Incomplete -> when (val list = state.list) {
512
+ null -> promoteSingleToNodeList(state as JobNode )
513
+ else -> if (tryAdd(state, list)) return true
531
514
}
515
+ else -> return false
532
516
}
533
517
}
534
518
}
@@ -973,14 +957,58 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
973
957
public final override fun attachChild (child : ChildJob ): ChildHandle {
974
958
/*
975
959
* Note: This function attaches a special ChildHandleNode node object. This node object
976
- * is handled in a special way on completion on the coroutine (we wait for all of them) and
977
- * is handled specially by invokeOnCompletion itself -- it adds this node to the list even
978
- * if the job is already cancelling. For cancelling state child is attached under state lock.
979
- * It's required to properly wait all children before completion and provide linearizable hierarchy view:
980
- * If child is attached when the job is already being cancelled, such child will receive immediate notification on
981
- * cancellation, but parent *will* wait for that child before completion and will handle its exception.
960
+ * is handled in a special way on completion on the coroutine (we wait for all of them) and also
961
+ * can't be added simply with `invokeOnCompletionInternal` -- we add this node to the list even
962
+ * if the job is already cancelling. For cancelling state, the child is attached under state lock.
963
+ * It's required to properly await all children before completion and provide a linearizable hierarchy view:
964
+ * If the child is attached when the job is already being cancelled, such a child will receive
965
+ * an immediate notification on cancellation,
966
+ * but the parent *will* wait for that child before completion and will handle its exception.
982
967
*/
983
- return invokeOnCompletion(handler = ChildHandleNode (child)) as ChildHandle
968
+ val node = ChildHandleNode (child).also { it.job = this }
969
+ val added = tryPutNodeIntoList(node) { state, list ->
970
+ if (state is Finishing ) {
971
+ val rootCause: Throwable
972
+ val handle: ChildHandle
973
+ synchronized(state) {
974
+ // check if we are installing cancellation handler on job that is being cancelled
975
+ val maybeRootCause = state.rootCause // != null if cancelling job
976
+ // We add the node to the list in two cases --- either the job is not being cancelled,
977
+ // or we are adding a child to a coroutine that is not completing yet
978
+ if (maybeRootCause == null || ! state.isCompleting) {
979
+ // Note: add node the list while holding lock on state (make sure it cannot change)
980
+ if (! list.addLast(node, allowedAfterPartialClosing = true ))
981
+ return @tryPutNodeIntoList false // retry
982
+ // just return the node if we don't have to invoke the handler (not cancelling yet)
983
+ rootCause = maybeRootCause ? : return @tryPutNodeIntoList true
984
+ // otherwise handler is invoked immediately out of the synchronized section & handle returned
985
+ handle = node
986
+ } else {
987
+ rootCause = maybeRootCause
988
+ handle = NonDisposableHandle
989
+ }
990
+ }
991
+ node.invoke(rootCause)
992
+ return handle
993
+ } else list.addLast(node, allowedAfterPartialClosing = true ).also { success ->
994
+ if (success) {
995
+ /* * Handling the following case:
996
+ * - A child requested to be added to the list;
997
+ * - We checked the state and saw that it wasn't `Finishing`;
998
+ * - Then, the job got cancelled and notified everyone about it;
999
+ * - Only then did we add the child to the list
1000
+ * - and ended up here.
1001
+ */
1002
+ val latestState = this @JobSupport.state
1003
+ if (latestState is Finishing ) {
1004
+ synchronized(latestState) { latestState.rootCause }?.let { node.invoke(it) }
1005
+ }
1006
+ }
1007
+ }
1008
+ }
1009
+ if (added) return node
1010
+ node.invoke((state as ? CompletedExceptionally )?.cause)
1011
+ return NonDisposableHandle
984
1012
}
985
1013
986
1014
/* *
0 commit comments