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