Skip to content

Commit 815db36

Browse files
committed
Fix a spinlock in invokeOnCompletion with onCancelling = true
Before this change, when children were prohibited from adding themselves to the list, cancellation handlers were also prohibited from doing so. This was plain incorrect, because a list that's closed for new children could still get cancelled later, and also because being closed for new children happened before advancing the state to the final one.
1 parent 85c45a0 commit 815db36

File tree

4 files changed

+32
-20
lines changed

4 files changed

+32
-20
lines changed

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

+15-8
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
352352
}
353353

354354
private fun NodeList.notifyCompletion(cause: Throwable?) {
355-
close(LIST_MAX_PERMISSION)
355+
close(LIST_ON_COMPLETION_PERMISSION)
356356
notifyHandlers(this, cause) { true }
357357
}
358358

@@ -468,13 +468,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
468468
if (node.onCancelling) {
469469
val rootCause = (state as? Finishing)?.let { synchronized(it) { it.rootCause } }
470470
if (rootCause == null) {
471-
list.addLast(node, LIST_CANCELLATION_PERMISSION)
471+
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION)
472472
} else {
473473
if (invokeImmediately) node.invoke(rootCause)
474474
return NonDisposableHandle
475475
}
476476
} else {
477-
list.addLast(node, LIST_MAX_PERMISSION)
477+
list.addLast(node, LIST_ON_COMPLETION_PERMISSION)
478478
}
479479
}
480480
when {
@@ -978,14 +978,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
978978
val node = ChildHandleNode(child).also { it.job = this }
979979
val added = tryPutNodeIntoList(node) { _, list ->
980980
// First, try to add a child along the cancellation handlers
981-
val addedBeforeCancellation = list.addLast(node, LIST_CANCELLATION_PERMISSION)
981+
val addedBeforeCancellation = list.addLast(
982+
node,
983+
LIST_ON_COMPLETION_PERMISSION or LIST_CHILD_PERMISSION or LIST_CANCELLATION_PERMISSION
984+
)
982985
if (addedBeforeCancellation) {
983986
// The child managed to be added before the parent started to cancel or complete. Success.
984987
true
985988
} else {
986989
// Either cancellation or completion already happened, the child was not added.
987990
// Now we need to try adding it for completion.
988-
val addedBeforeCompletion = list.addLast(node, LIST_CHILD_PERMISSION)
991+
val addedBeforeCompletion = list.addLast(
992+
node,
993+
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
994+
)
989995
// Whether or not we managed to add the child before the parent completed, we need to investigate:
990996
// why didn't we manage to add it before cancellation?
991997
// If it's because cancellation happened in the meantime, we need to notify the child.
@@ -1345,9 +1351,10 @@ private val SEALED = Symbol("SEALED")
13451351
private val EMPTY_NEW = Empty(false)
13461352
private val EMPTY_ACTIVE = Empty(true)
13471353

1348-
private const val LIST_MAX_PERMISSION = Int.MAX_VALUE
1349-
private const val LIST_CHILD_PERMISSION = 1
1350-
private const val LIST_CANCELLATION_PERMISSION = 0
1354+
// bit mask
1355+
private const val LIST_ON_COMPLETION_PERMISSION = 1
1356+
private const val LIST_CHILD_PERMISSION = 2
1357+
private const val LIST_CANCELLATION_PERMISSION = 4
13511358

13521359
private class Empty(override val isActive: Boolean) : Incomplete {
13531360
override val list: NodeList? get() = null

kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ public expect open class LockFreeLinkedListNode() {
77
public val isRemoved: Boolean
88
public val nextNode: LockFreeLinkedListNode
99
public val prevNode: LockFreeLinkedListNode
10-
public fun addLast(node: LockFreeLinkedListNode, clearanceLevel: Int): Boolean
10+
public fun addLast(node: LockFreeLinkedListNode, permissionsBitmask: Int): Boolean
1111
public fun addOneIfEmpty(node: LockFreeLinkedListNode): Boolean
1212
public open fun remove(): Boolean
1313

1414
/**
15-
* Closes the list for [maxForbidden] and all numbers below.
15+
* Closes the list for anything that requests the permission [forbiddenElementsBit].
16+
* Only a single permission can be forbidden at a time, but this isn't checked.
1617
*/
17-
public fun close(maxForbidden: Int)
18+
public fun close(forbiddenElementsBit: Int)
1819
}
1920

2021
/** @suppress **This is unstable API and it is subject to change.** */

kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt

+7-4
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ public actual open class LockFreeLinkedListNode {
8080
/**
8181
* Adds last item to this list. Returns `false` if the list is closed.
8282
*/
83-
public actual fun addLast(node: Node, clearanceLevel: Int): Boolean {
83+
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean {
8484
while (true) { // lock-free loop on prev.next
8585
val currentPrev = prevNode
8686
return when {
8787
currentPrev is ListClosed ->
88-
currentPrev.maxForbidden < clearanceLevel && currentPrev.addLast(node, clearanceLevel)
88+
currentPrev.forbiddenElementsBitmask and permissionsBitmask == 0 &&
89+
currentPrev.addLast(node, permissionsBitmask)
8990
currentPrev.addNext(node, this) -> true
9091
else -> continue
9192
}
@@ -95,7 +96,9 @@ public actual open class LockFreeLinkedListNode {
9596
/**
9697
* Forbids adding new items to this list.
9798
*/
98-
public actual fun close(maxForbidden: Int) { addLast(ListClosed(maxForbidden), maxForbidden) }
99+
public actual fun close(forbiddenElementsBit: Int) {
100+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
101+
}
99102

100103
/**
101104
* Given:
@@ -283,4 +286,4 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
283286
override val isRemoved: Boolean get() = false
284287
}
285288

286-
private class ListClosed(val maxForbidden: Int): LockFreeLinkedListNode()
289+
private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ public actual open class LockFreeLinkedListNode {
1414
inline actual val prevNode get() = _prev
1515
inline actual val isRemoved get() = _removed
1616

17-
public actual fun addLast(node: Node, clearanceLevel: Int): Boolean = when (val prev = this._prev) {
18-
is ListClosed -> prev.maxForbidden < clearanceLevel && prev.addLast(node, clearanceLevel)
17+
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
18+
is ListClosed ->
19+
prev.forbiddenElementsBitmask and permissionsBitmask == 0 && prev.addLast(node, permissionsBitmask)
1920
else -> {
2021
node._next = this
2122
node._prev = prev
@@ -25,8 +26,8 @@ public actual open class LockFreeLinkedListNode {
2526
}
2627
}
2728

28-
public actual fun close(maxForbidden: Int) {
29-
addLast(ListClosed(maxForbidden), maxForbidden)
29+
public actual fun close(forbiddenElementsBit: Int) {
30+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
3031
}
3132

3233
/*
@@ -69,4 +70,4 @@ public actual open class LockFreeLinkedListHead : Node() {
6970
public actual final override fun remove(): Nothing = throw UnsupportedOperationException()
7071
}
7172

72-
private class ListClosed(val maxForbidden: Int): LockFreeLinkedListNode()
73+
private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

0 commit comments

Comments
 (0)