Skip to content

Commit 4a4ada0

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 1cc88d6 commit 4a4ada0

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
@@ -353,7 +353,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
353353
}
354354

355355
private fun NodeList.notifyCompletion(cause: Throwable?) {
356-
close(LIST_MAX_PERMISSION)
356+
close(LIST_ON_COMPLETION_PERMISSION)
357357
notifyHandlers<JobNode>(this, cause)
358358
}
359359

@@ -466,13 +466,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
466466
if (onCancelling) {
467467
val rootCause = (state as? Finishing)?.let { synchronized(it) { it.rootCause } }
468468
if (rootCause == null) {
469-
list.addLast(node, LIST_CANCELLATION_PERMISSION)
469+
list.addLast(node, LIST_CANCELLATION_PERMISSION or LIST_ON_COMPLETION_PERMISSION)
470470
} else {
471471
if (invokeImmediately) handler.invoke(rootCause)
472472
return NonDisposableHandle
473473
}
474474
} else {
475-
list.addLast(node, LIST_MAX_PERMISSION)
475+
list.addLast(node, LIST_ON_COMPLETION_PERMISSION)
476476
}
477477
}
478478
when {
@@ -988,14 +988,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
988988
val node = ChildHandleNode(child).also { it.job = this }
989989
val added = tryPutNodeIntoList(node) { _, list ->
990990
// First, try to add a child along the cancellation handlers
991-
val addedBeforeCancellation = list.addLast(node, LIST_CANCELLATION_PERMISSION)
991+
val addedBeforeCancellation = list.addLast(
992+
node,
993+
LIST_ON_COMPLETION_PERMISSION or LIST_CHILD_PERMISSION or LIST_CANCELLATION_PERMISSION
994+
)
992995
if (addedBeforeCancellation) {
993996
// The child managed to be added before the parent started to cancel or complete. Success.
994997
true
995998
} else {
996999
// Either cancellation or completion already happened, the child was not added.
9971000
// Now we need to try adding it for completion.
998-
val addedBeforeCompletion = list.addLast(node, LIST_CHILD_PERMISSION)
1001+
val addedBeforeCompletion = list.addLast(
1002+
node,
1003+
LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION
1004+
)
9991005
// Whether or not we managed to add the child before the parent completed, we need to investigate:
10001006
// why didn't we manage to add it before cancellation?
10011007
// If it's because cancellation happened in the meantime, we need to notify the child.
@@ -1353,9 +1359,10 @@ private val SEALED = Symbol("SEALED")
13531359
private val EMPTY_NEW = Empty(false)
13541360
private val EMPTY_ACTIVE = Empty(true)
13551361

1356-
private const val LIST_MAX_PERMISSION = Int.MAX_VALUE
1357-
private const val LIST_CHILD_PERMISSION = 1
1358-
private const val LIST_CANCELLATION_PERMISSION = 0
1362+
// bit mask
1363+
private const val LIST_ON_COMPLETION_PERMISSION = 1
1364+
private const val LIST_CHILD_PERMISSION = 2
1365+
private const val LIST_CANCELLATION_PERMISSION = 4
13591366

13601367
private class Empty(override val isActive: Boolean) : Incomplete {
13611368
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

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:
@@ -285,4 +288,4 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
285288
override val isRemoved: Boolean get() = false
286289
}
287290

288-
private class ListClosed(val maxForbidden: Int): LockFreeLinkedListNode()
291+
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
@@ -22,8 +22,9 @@ public open class LinkedListNode : DisposableHandle {
2222
public inline val prevNode get() = _prev
2323
public inline val isRemoved get() = _removed
2424

25-
public fun addLast(node: Node, clearanceLevel: Int): Boolean = when (val prev = this._prev) {
26-
is ListClosed -> prev.maxForbidden < clearanceLevel && prev.addLast(node, clearanceLevel)
25+
public fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
26+
is ListClosed ->
27+
prev.forbiddenElementsBitmask and permissionsBitmask == 0 && prev.addLast(node, permissionsBitmask)
2728
else -> {
2829
node._next = this
2930
node._prev = prev
@@ -33,8 +34,8 @@ public open class LinkedListNode : DisposableHandle {
3334
}
3435
}
3536

36-
public fun close(maxForbidden: Int) {
37-
addLast(ListClosed(maxForbidden), maxForbidden)
37+
public fun close(forbiddenElementsBit: Int) {
38+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
3839
}
3940

4041
/*
@@ -83,4 +84,4 @@ public open class LinkedListHead : LinkedListNode() {
8384
public final override fun remove(): Nothing = throw UnsupportedOperationException()
8485
}
8586

86-
private class ListClosed(val maxForbidden: Int): LinkedListNode()
87+
private class ListClosed(val forbiddenElementsBitmask: Int): LinkedListNode()

0 commit comments

Comments
 (0)