Skip to content

Commit 532ff3d

Browse files
committed
Extract child-handling logic into another function
This change is mostly a refactoring, except now, an arbitrary `onCancelling` handler that's not a child will not add itself in a `synchronized` block. Instead, only the root cause is read under a lock.
1 parent 6b5cc95 commit 532ff3d

File tree

1 file changed

+92
-66
lines changed

1 file changed

+92
-66
lines changed

kotlinx-coroutines-core/common/src/JobSupport.kt

+92-66
Original file line numberDiff line numberDiff line change
@@ -462,73 +462,55 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
462462
// Create node upfront -- for common cases it just initializes JobNode.job field,
463463
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
464464
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 {
465500
loopOnState { state ->
466501
when (state) {
467502
is Empty -> { // EMPTY_X state -- no completion handlers
468503
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
471506
} else
472507
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
473508
}
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
531512
}
513+
else -> return false
532514
}
533515
}
534516
}
@@ -985,14 +967,58 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
985967
public final override fun attachChild(child: ChildJob): ChildHandle {
986968
/*
987969
* 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.
994977
*/
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
9961022
}
9971023

9981024
/**

0 commit comments

Comments
 (0)