@@ -456,51 +456,86 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
456456 // Create node upfront -- for common cases it just initializes JobNode.job field,
457457 // for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
458458 val node: JobNode = makeNode(handler, onCancelling)
459+ /*
460+ * LinkedList algorithm does not support removing and re-adding the same node,
461+ * so here we check whether the node is already added to the list to avoid adding the node twice.
462+ */
463+ var isNodeAdded = false
459464 loopOnState { state ->
460465 when (state) {
461466 is Empty -> { // EMPTY_X state -- no completion handlers
462467 if (state.isActive) {
463468 // try move to SINGLE state
464469 if (_state .compareAndSet(state, node)) return node
465- } else
470+ } else {
466471 promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
472+ }
467473 }
468474 is Incomplete -> {
469475 val list = state.list
470476 if (list == null ) { // SINGLE/SINGLE+
471477 promoteSingleToNodeList(state as JobNode )
472- } else {
473- var rootCause: Throwable ? = null
474- var handle: DisposableHandle = NonDisposableHandle
475- if (onCancelling && state is Finishing ) {
476- synchronized(state) {
477- // check if we are installing cancellation handler on job that is being cancelled
478- rootCause = state.rootCause // != null if cancelling job
479- // We add node to the list in two cases --- either the job is not being cancelled
480- // or we are adding a child to a coroutine that is not completing yet
481- if (rootCause == null || handler.isHandlerOf<ChildHandleNode >() && ! state.isCompleting) {
482- // Note: add node the list while holding lock on state (make sure it cannot change)
483- if (! addLastAtomic(state, list, node)) return @loopOnState // retry
484- // just return node if we don't have to invoke handler (not cancelling yet)
485- if (rootCause == null ) return node
486- // otherwise handler is invoked immediately out of the synchronized section & handle returned
487- handle = node
478+ return @loopOnState // retry
479+ }
480+ // ...else {, but without nesting
481+ var rootCause: Throwable ? = null
482+ var handle: DisposableHandle = NonDisposableHandle
483+ if (onCancelling && state is Finishing ) {
484+ synchronized(state) {
485+ // check if we are installing cancellation handler on job that is being cancelled
486+ rootCause = state.rootCause // != null if cancelling job
487+ // We add node to the list in two cases --- either the job is not being cancelled
488+ // or we are adding a child to a coroutine that is not completing yet
489+ if (rootCause == null || handler.isHandlerOf<ChildHandleNode >() && ! state.isCompleting) {
490+ // Note: add node the list while holding lock on state (make sure it cannot change)
491+ if (! isNodeAdded) list.addLast(node)
492+ if (this .state != = state) {
493+ /*
494+ * Here we have an additional check for ChildCompletion. Invoking the handler once is not enough
495+ * for this particular kind of node -- the caller makes a decision based on whether the node was added
496+ * or not and that decision should be made **once**.
497+ * To be more precise, the caller of ChildCompletion, in case when it's the last child,
498+ * should make a decision whether to start transition to the final state, based on
499+ * whether the ChildCompletion was added to the list or not. If not -- the JobNode.invoke will do that.
500+ * See comment to JobNode.invoke, we cannot differentiate the situation when external state updater
501+ * invoked or skipped the node, thus we additionally synchronize on 'markInvoked'.
502+ */
503+ if (node is ChildCompletion && ! node.markInvoked()) return node
504+ // The state can only change to Complete here, so the node can stay in the list, just retry
505+ return @loopOnState
488506 }
507+ // just return node if we don't have to invoke handler (not cancelling yet)
508+ if (rootCause == null ) return node
509+ // otherwise handler is invoked immediately out of the synchronized section & handle returned
510+ handle = node
489511 }
490512 }
491- if (rootCause != null ) {
492- // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
493- if (invokeImmediately) handler.invokeIt(rootCause)
494- return handle
513+ }
514+ if (rootCause != null ) {
515+ // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
516+ if (invokeImmediately) {
517+ node.invoke(rootCause)
518+ }
519+ return handle
520+ } else {
521+ if (! isNodeAdded) list.addLast(node)
522+ if (this .state != = state) {
523+ // Here again, we try to prevent concurrent finalization of the parent,
524+ // if the parent fails to add ChildCompletion because the child changed it's state to Completed.
525+ if (node is ChildCompletion && ! node.markInvoked()) return node
526+ // If the state has changed to Complete, the node can stay in the list, just retry.
527+ if (this .state !is Incomplete ) return @loopOnState
528+ // If the state is Incomplete, set the flag that the node is already added to the list instead of removing it.
529+ isNodeAdded = true
495530 } else {
496- if (addLastAtomic(state, list, node)) return node
531+ return node
497532 }
498533 }
499534 }
500535 else -> { // is complete
501- // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
502- // because we play type tricks on Kotlin/JS and handler is not necessarily a function there
503- if (invokeImmediately) handler.invokeIt((state as ? CompletedExceptionally )?.cause)
536+ if (invokeImmediately) {
537+ node.invoke((state as ? CompletedExceptionally )?.cause)
538+ }
504539 return NonDisposableHandle
505540 }
506541 }
@@ -520,9 +555,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
520555 return node
521556 }
522557
523- private fun addLastAtomic (expect : Any , list : NodeList , node : JobNode ) =
524- list.addLastIf(node) { this .state == = expect }
525-
526558 private fun promoteEmptyToNodeList (state : Empty ) {
527559 // try to promote it to LIST state with the corresponding state
528560 val list = NodeList ()
@@ -596,7 +628,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
596628 }
597629 is Incomplete -> { // may have a list of completion handlers
598630 // remove node from the list if there is a list
599- if (state.list != null ) node.remove()
631+ if (state.list != null ) {
632+ if (! node.remove() && node.isRemoved) {
633+ // Note: .remove() returns 'false' if the node wasn't added at all, e.g.
634+ // because it was an optimized "single element list"
635+ node.helpRemove()
636+ }
637+ }
600638 return
601639 }
602640 else -> return // it is complete and does not have any completion handlers
@@ -814,7 +852,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
814852 }
815853 }
816854 }
817- }
855+ }
818856
819857 /* *
820858 * Completes this job. Used by [AbstractCoroutine.resume].
@@ -1151,9 +1189,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
11511189 private val child : ChildHandleNode ,
11521190 private val proposedUpdate : Any?
11531191 ) : JobNode() {
1154- override fun invoke (cause : Throwable ? ) {
1192+ override fun invokeOnce (cause : Throwable ? ) {
11551193 parent.continueCompleting(state, child, proposedUpdate)
11561194 }
1195+ override fun toString (): String =
1196+ " ChildCompletion[$child , $proposedUpdate ]"
11571197 }
11581198
11591199 private class AwaitContinuation <T >(
@@ -1355,6 +1395,31 @@ internal abstract class JobNode : CompletionHandlerBase(), DisposableHandle, Inc
13551395 override val isActive: Boolean get() = true
13561396 override val list: NodeList ? get() = null
13571397 override fun dispose () = job.removeNode(this )
1398+ private val isInvoked = atomic(false )
1399+
1400+ /*
1401+ * This method is guaranteed to be invoked once per JobNode lifecycle.
1402+ */
1403+ protected abstract fun invokeOnce (cause : Throwable ? )
1404+
1405+ /*
1406+ * This method can be invoked more than once thus it's protected
1407+ * with atomic flag.
1408+ *
1409+ * It can be invoked twice via [invokeOnCompletion(invokeImmediately = true)]
1410+ * when addLastIf fails due to the race with Job state update.
1411+ * In that case, we cannot distinguish the situation when the node was added
1412+ * and the external state updater actually invoked it from the situation
1413+ * when the state updater already invoked all elements from a list and then
1414+ * we added a new node to already abandoned list.
1415+ * So, when addLastIf fails, we invoke handler in-place, using
1416+ * markInvoked as protection against the former case.
1417+ */
1418+ final override fun invoke (cause : Throwable ? ) {
1419+ if (! markInvoked()) return
1420+ invokeOnce(cause)
1421+ }
1422+ fun markInvoked () = isInvoked.compareAndSet(false , true )
13581423 override fun toString () = " $classSimpleName @$hexAddress [job@${job.hexAddress} ]"
13591424}
13601425
@@ -1387,20 +1452,22 @@ internal class InactiveNodeList(
13871452
13881453private class InvokeOnCompletion (
13891454 private val handler : CompletionHandler
1390- ) : JobNode() {
1391- override fun invoke (cause : Throwable ? ) = handler.invoke(cause)
1455+ ) : JobNode() {
1456+ override fun invokeOnce (cause : Throwable ? ) = handler.invoke(cause)
1457+ override fun toString () = " InvokeOnCompletion[$classSimpleName @$hexAddress ]"
13921458}
13931459
13941460private class ResumeOnCompletion (
13951461 private val continuation : Continuation <Unit >
13961462) : JobNode() {
1397- override fun invoke (cause : Throwable ? ) = continuation.resume(Unit )
1463+ override fun invokeOnce (cause : Throwable ? ) = continuation.resume(Unit )
1464+ override fun toString () = " ResumeOnCompletion[$continuation ]"
13981465}
13991466
14001467private class ResumeAwaitOnCompletion <T >(
14011468 private val continuation : CancellableContinuationImpl <T >
14021469) : JobNode() {
1403- override fun invoke (cause : Throwable ? ) {
1470+ override fun invokeOnce (cause : Throwable ? ) {
14041471 val state = job.state
14051472 assert { state !is Incomplete }
14061473 if (state is CompletedExceptionally ) {
@@ -1412,32 +1479,36 @@ private class ResumeAwaitOnCompletion<T>(
14121479 continuation.resume(state.unboxState() as T )
14131480 }
14141481 }
1482+ override fun toString () = " ResumeAwaitOnCompletion[$continuation ]"
14151483}
14161484
14171485internal class DisposeOnCompletion (
14181486 private val handle : DisposableHandle
14191487) : JobNode() {
1420- override fun invoke (cause : Throwable ? ) = handle.dispose()
1488+ override fun invokeOnce (cause : Throwable ? ) = handle.dispose()
1489+ override fun toString (): String = " DisposeOnCompletion[${handle} ]"
14211490}
14221491
14231492private class SelectJoinOnCompletion <R >(
14241493 private val select : SelectInstance <R >,
14251494 private val block : suspend () -> R
14261495) : JobNode() {
1427- override fun invoke (cause : Throwable ? ) {
1496+ override fun invokeOnce (cause : Throwable ? ) {
14281497 if (select.trySelect())
14291498 block.startCoroutineCancellable(select.completion)
14301499 }
1500+ override fun toString (): String = " SelectJoinOnCompletion[$select ]"
14311501}
14321502
14331503private class SelectAwaitOnCompletion <T , R >(
14341504 private val select : SelectInstance <R >,
14351505 private val block : suspend (T ) -> R
14361506) : JobNode() {
1437- override fun invoke (cause : Throwable ? ) {
1507+ override fun invokeOnce (cause : Throwable ? ) {
14381508 if (select.trySelect())
14391509 job.selectAwaitCompletion(select, block)
14401510 }
1511+ override fun toString (): String = " SelectAwaitOnCompletion[$select ]"
14411512}
14421513
14431514// -------- invokeOnCancellation nodes
@@ -1450,28 +1521,28 @@ internal abstract class JobCancellingNode : JobNode()
14501521
14511522private class InvokeOnCancelling (
14521523 private val handler : CompletionHandler
1453- ) : JobCancellingNode() {
1524+ ) : JobCancellingNode() {
14541525 // delegate handler shall be invoked at most once, so here is an additional flag
1455- private val _invoked = atomic(0 ) // todo: replace with atomic boolean after migration to recent atomicFu
1456- override fun invoke (cause : Throwable ? ) {
1457- if (_invoked .compareAndSet(0 , 1 )) handler.invoke(cause)
1458- }
1526+ override fun invokeOnce (cause : Throwable ? ) = handler.invoke(cause)
1527+ override fun toString () = " InvokeOnCancelling[$classSimpleName @$hexAddress ]"
14591528}
14601529
14611530internal class ChildHandleNode (
14621531 @JvmField val childJob : ChildJob
14631532) : JobCancellingNode(), ChildHandle {
14641533 override val parent: Job get() = job
1465- override fun invoke (cause : Throwable ? ) = childJob.parentCancelled(job)
1534+ override fun invokeOnce (cause : Throwable ? ) = childJob.parentCancelled(job)
14661535 override fun childCancelled (cause : Throwable ): Boolean = job.childCancelled(cause)
1536+ override fun toString (): String = " ChildHandle[$childJob ]"
14671537}
14681538
14691539// Same as ChildHandleNode, but for cancellable continuation
14701540internal class ChildContinuation (
14711541 @JvmField val child : CancellableContinuationImpl <* >
14721542) : JobCancellingNode() {
1473- override fun invoke (cause : Throwable ? ) {
1543+ override fun invokeOnce (cause : Throwable ? ) =
14741544 child.parentCancelled(child.getContinuationCancellationCause(job))
1475- }
1545+ override fun toString (): String =
1546+ " ChildContinuation[$child ]"
14761547}
14771548
0 commit comments