Skip to content

Commit a69748c

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 ea913b0 commit a69748c

File tree

1 file changed

+94
-66
lines changed

1 file changed

+94
-66
lines changed

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

Lines changed: 94 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -462,73 +462,57 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
462462
node: JobNode
463463
): DisposableHandle {
464464
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 {
465502
loopOnState { state ->
466503
when (state) {
467504
is Empty -> { // EMPTY_X state -- no completion handlers
468505
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
471508
} else
472509
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
473510
}
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
531514
}
515+
else -> return false
532516
}
533517
}
534518
}
@@ -973,14 +957,58 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
973957
public final override fun attachChild(child: ChildJob): ChildHandle {
974958
/*
975959
* 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.
982967
*/
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
9841012
}
9851013

9861014
/**

0 commit comments

Comments
 (0)