Skip to content

Commit 1176267

Browse files
qwwdfsadelizarov
andauthored
Remove requirement that job of the pre-created JobCancelNode have to … (#2427)
Remove the requirement that the job of the pre-created JobCancelNode have to be equal to the outer job Job is supposed to be "sealed" interface, but we also have non-sealed Deferred that can be successfully implemented via delegation and such delegation may break code (if assertions are enabled!) in very subtle ways. This assertion is our internal invariant that we're preserving anyway, so it's worth to lift it to simplify the life of our users in the future Fixes #2423 Co-authored-by: Roman Elizarov <[email protected]>
1 parent 556f07a commit 1176267

File tree

7 files changed

+86
-64
lines changed

7 files changed

+86
-64
lines changed

Diff for: kotlinx-coroutines-core/common/src/Await.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package kotlinx.coroutines
66

77
import kotlinx.atomicfu.*
8-
import kotlinx.coroutines.channels.*
98
import kotlin.coroutines.*
109

1110
/**
@@ -75,7 +74,7 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
7574
val nodes = Array(deferreds.size) { i ->
7675
val deferred = deferreds[i]
7776
deferred.start() // To properly await lazily started deferreds
78-
AwaitAllNode(cont, deferred).apply {
77+
AwaitAllNode(cont).apply {
7978
handle = deferred.invokeOnCompletion(asHandler)
8079
}
8180
}
@@ -101,7 +100,7 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
101100
override fun toString(): String = "DisposeHandlersOnCancel[$nodes]"
102101
}
103102

104-
private inner class AwaitAllNode(private val continuation: CancellableContinuation<List<T>>, job: Job) : JobNode<Job>(job) {
103+
private inner class AwaitAllNode(private val continuation: CancellableContinuation<List<T>>) : JobNode() {
105104
lateinit var handle: DisposableHandle
106105

107106
private val _disposer = atomic<DisposeHandlersOnCancel?>(null)

Diff for: kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ internal open class CancellableContinuationImpl<in T>(
129129
val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent
130130
val handle = parent.invokeOnCompletion(
131131
onCancelling = true,
132-
handler = ChildContinuation(parent, this).asHandler
132+
handler = ChildContinuation(this).asHandler
133133
)
134134
parentHandle = handle
135135
// now check our state _after_ registering (could have completed while we were registering)

Diff for: kotlinx-coroutines-core/common/src/Job.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ public interface ChildHandle : DisposableHandle {
490490
* ```
491491
*/
492492
internal fun Job.disposeOnCompletion(handle: DisposableHandle): DisposableHandle =
493-
invokeOnCompletion(handler = DisposeOnCompletion(this, handle).asHandler)
493+
invokeOnCompletion(handler = DisposeOnCompletion(handle).asHandler)
494494

495495
/**
496496
* Cancels the job and suspends the invoking coroutine until the cancelled job is complete.

Diff for: kotlinx-coroutines-core/common/src/JobSupport.kt

+49-53
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
287287
// fast-path method to finalize normally completed coroutines without children
288288
// returns true if complete, and afterCompletion(update) shall be called
289289
private fun tryFinalizeSimpleState(state: Incomplete, update: Any?): Boolean {
290-
assert { state is Empty || state is JobNode<*> } // only simple state without lists where children can concurrently add
290+
assert { state is Empty || state is JobNode } // only simple state without lists where children can concurrently add
291291
assert { update !is CompletedExceptionally } // only for normal completion
292292
if (!_state.compareAndSet(state, update.boxIncomplete())) return false
293293
onCancelling(null) // simple state is not a failure
@@ -313,7 +313,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
313313
* 2) Invoke completion handlers: .join(), callbacks etc.
314314
* It's important to invoke them only AFTER exception handling and everything else, see #208
315315
*/
316-
if (state is JobNode<*>) { // SINGLE/SINGLE+ state -- one completion handler (common case)
316+
if (state is JobNode) { // SINGLE/SINGLE+ state -- one completion handler (common case)
317317
try {
318318
state.invoke(cause)
319319
} catch (ex: Throwable) {
@@ -327,7 +327,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
327327
private fun notifyCancelling(list: NodeList, cause: Throwable) {
328328
// first cancel our own children
329329
onCancelling(cause)
330-
notifyHandlers<JobCancellingNode<*>>(list, cause)
330+
notifyHandlers<JobCancellingNode>(list, cause)
331331
// then cancel parent
332332
cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
333333
}
@@ -359,9 +359,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
359359
}
360360

361361
private fun NodeList.notifyCompletion(cause: Throwable?) =
362-
notifyHandlers<JobNode<*>>(this, cause)
362+
notifyHandlers<JobNode>(this, cause)
363363

364-
private inline fun <reified T: JobNode<*>> notifyHandlers(list: NodeList, cause: Throwable?) {
364+
private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
365365
var exception: Throwable? = null
366366
list.forEach<T> { node ->
367367
try {
@@ -453,21 +453,22 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
453453
invokeImmediately: Boolean,
454454
handler: CompletionHandler
455455
): DisposableHandle {
456-
var nodeCache: JobNode<*>? = null
456+
// Create node upfront -- for common cases it just initializes JobNode.job field,
457+
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
458+
val node: JobNode = makeNode(handler, onCancelling)
457459
loopOnState { state ->
458460
when (state) {
459461
is Empty -> { // EMPTY_X state -- no completion handlers
460462
if (state.isActive) {
461463
// try move to SINGLE state
462-
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
463464
if (_state.compareAndSet(state, node)) return node
464465
} else
465466
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
466467
}
467468
is Incomplete -> {
468469
val list = state.list
469470
if (list == null) { // SINGLE/SINGLE+
470-
promoteSingleToNodeList(state as JobNode<*>)
471+
promoteSingleToNodeList(state as JobNode)
471472
} else {
472473
var rootCause: Throwable? = null
473474
var handle: DisposableHandle = NonDisposableHandle
@@ -479,7 +480,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
479480
// or we are adding a child to a coroutine that is not completing yet
480481
if (rootCause == null || handler.isHandlerOf<ChildHandleNode>() && !state.isCompleting) {
481482
// Note: add node the list while holding lock on state (make sure it cannot change)
482-
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
483483
if (!addLastAtomic(state, list, node)) return@loopOnState // retry
484484
// just return node if we don't have to invoke handler (not cancelling yet)
485485
if (rootCause == null) return node
@@ -493,7 +493,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
493493
if (invokeImmediately) handler.invokeIt(rootCause)
494494
return handle
495495
} else {
496-
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
497496
if (addLastAtomic(state, list, node)) return node
498497
}
499498
}
@@ -508,16 +507,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
508507
}
509508
}
510509

511-
private fun makeNode(handler: CompletionHandler, onCancelling: Boolean): JobNode<*> {
512-
return if (onCancelling)
513-
(handler as? JobCancellingNode<*>)?.also { assert { it.job === this } }
514-
?: InvokeOnCancelling(this, handler)
515-
else
516-
(handler as? JobNode<*>)?.also { assert { it.job === this && it !is JobCancellingNode } }
517-
?: InvokeOnCompletion(this, handler)
510+
private fun makeNode(handler: CompletionHandler, onCancelling: Boolean): JobNode {
511+
val node = if (onCancelling) {
512+
(handler as? JobCancellingNode)
513+
?: InvokeOnCancelling(handler)
514+
} else {
515+
(handler as? JobNode)
516+
?.also { assert { it !is JobCancellingNode } }
517+
?: InvokeOnCompletion(handler)
518+
}
519+
node.job = this
520+
return node
518521
}
519522

520-
private fun addLastAtomic(expect: Any, list: NodeList, node: JobNode<*>) =
523+
private fun addLastAtomic(expect: Any, list: NodeList, node: JobNode) =
521524
list.addLastIf(node) { this.state === expect }
522525

523526
private fun promoteEmptyToNodeList(state: Empty) {
@@ -527,7 +530,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
527530
_state.compareAndSet(state, update)
528531
}
529532

530-
private fun promoteSingleToNodeList(state: JobNode<*>) {
533+
private fun promoteSingleToNodeList(state: JobNode) {
531534
// try to promote it to list (SINGLE+ state)
532535
state.addOneIfEmpty(NodeList())
533536
// it must be in SINGLE+ state or state has changed (node could have need removed from state)
@@ -553,7 +556,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
553556

554557
private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
555558
// We have to invoke join() handler only on cancellation, on completion we will be resumed regularly without handlers
556-
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler))
559+
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
557560
}
558561

559562
public final override val onJoin: SelectClause0
@@ -573,7 +576,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
573576
}
574577
if (startInternal(state) == 0) {
575578
// slow-path -- register waiter for completion
576-
select.disposeOnSelect(invokeOnCompletion(handler = SelectJoinOnCompletion(this, select, block).asHandler))
579+
select.disposeOnSelect(invokeOnCompletion(handler = SelectJoinOnCompletion(select, block).asHandler))
577580
return
578581
}
579582
}
@@ -582,11 +585,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
582585
/**
583586
* @suppress **This is unstable API and it is subject to change.**
584587
*/
585-
internal fun removeNode(node: JobNode<*>) {
588+
internal fun removeNode(node: JobNode) {
586589
// remove logic depends on the state of the job
587590
loopOnState { state ->
588591
when (state) {
589-
is JobNode<*> -> { // SINGE/SINGLE+ state -- one completion handler
592+
is JobNode -> { // SINGE/SINGLE+ state -- one completion handler
590593
if (state !== node) return // a different job node --> we were already removed
591594
// try remove and revert back to empty state
592595
if (_state.compareAndSet(state, EMPTY_ACTIVE)) return
@@ -770,7 +773,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
770773
private fun getOrPromoteCancellingList(state: Incomplete): NodeList? = state.list ?:
771774
when (state) {
772775
is Empty -> NodeList() // we can allocate new empty list that'll get integrated into Cancelling state
773-
is JobNode<*> -> {
776+
is JobNode -> {
774777
// SINGLE/SINGLE+ must be promoted to NodeList first, because otherwise we cannot
775778
// correctly capture a reference to it
776779
promoteSingleToNodeList(state)
@@ -849,7 +852,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
849852
* Otherwise, there can be a race between (completed state -> handled exception and newly attached child/join)
850853
* which may miss unhandled exception.
851854
*/
852-
if ((state is Empty || state is JobNode<*>) && state !is ChildHandleNode && proposedUpdate !is CompletedExceptionally) {
855+
if ((state is Empty || state is JobNode) && state !is ChildHandleNode && proposedUpdate !is CompletedExceptionally) {
853856
if (tryFinalizeSimpleState(state, proposedUpdate)) {
854857
// Completed successfully on fast path -- return updated state
855858
return proposedUpdate
@@ -964,7 +967,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
964967
* If child is attached when the job is already being cancelled, such child will receive immediate notification on
965968
* cancellation, but parent *will* wait for that child before completion and will handle its exception.
966969
*/
967-
return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(this, child).asHandler) as ChildHandle
970+
return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(child).asHandler) as ChildHandle
968971
}
969972

970973
/**
@@ -1147,7 +1150,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
11471150
private val state: Finishing,
11481151
private val child: ChildHandleNode,
11491152
private val proposedUpdate: Any?
1150-
) : JobNode<Job>(child.childJob) {
1153+
) : JobNode() {
11511154
override fun invoke(cause: Throwable?) {
11521155
parent.continueCompleting(state, child, proposedUpdate)
11531156
}
@@ -1225,7 +1228,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
12251228
* thrown and not a JobCancellationException.
12261229
*/
12271230
val cont = AwaitContinuation(uCont.intercepted(), this)
1228-
cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(this, cont).asHandler))
1231+
cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(cont).asHandler))
12291232
cont.getResult()
12301233
}
12311234

@@ -1252,7 +1255,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
12521255
}
12531256
if (startInternal(state) == 0) {
12541257
// slow-path -- register waiter for completion
1255-
select.disposeOnSelect(invokeOnCompletion(handler = SelectAwaitOnCompletion(this, select, block).asHandler))
1258+
select.disposeOnSelect(invokeOnCompletion(handler = SelectAwaitOnCompletion(select, block).asHandler))
12561259
return
12571260
}
12581261
}
@@ -1342,12 +1345,14 @@ internal interface Incomplete {
13421345
val list: NodeList? // is null only for Empty and JobNode incomplete state objects
13431346
}
13441347

1345-
internal abstract class JobNode<out J : Job>(
1346-
@JvmField val job: J
1347-
) : CompletionHandlerBase(), DisposableHandle, Incomplete {
1348+
internal abstract class JobNode : CompletionHandlerBase(), DisposableHandle, Incomplete {
1349+
/**
1350+
* Initialized by [JobSupport.makeNode].
1351+
*/
1352+
lateinit var job: JobSupport
13481353
override val isActive: Boolean get() = true
13491354
override val list: NodeList? get() = null
1350-
override fun dispose() = (job as JobSupport).removeNode(this)
1355+
override fun dispose() = job.removeNode(this)
13511356
override fun toString() = "$classSimpleName@$hexAddress[job@${job.hexAddress}]"
13521357
}
13531358

@@ -1360,7 +1365,7 @@ internal class NodeList : LockFreeLinkedListHead(), Incomplete {
13601365
append(state)
13611366
append("}[")
13621367
var first = true
1363-
this@NodeList.forEach<JobNode<*>> { node ->
1368+
this@NodeList.forEach<JobNode> { node ->
13641369
if (first) first = false else append(", ")
13651370
append(node)
13661371
}
@@ -1379,23 +1384,20 @@ internal class InactiveNodeList(
13791384
}
13801385

13811386
private class InvokeOnCompletion(
1382-
job: Job,
13831387
private val handler: CompletionHandler
1384-
) : JobNode<Job>(job) {
1388+
) : JobNode() {
13851389
override fun invoke(cause: Throwable?) = handler.invoke(cause)
13861390
}
13871391

13881392
private class ResumeOnCompletion(
1389-
job: Job,
13901393
private val continuation: Continuation<Unit>
1391-
) : JobNode<Job>(job) {
1394+
) : JobNode() {
13921395
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
13931396
}
13941397

13951398
private class ResumeAwaitOnCompletion<T>(
1396-
job: JobSupport,
13971399
private val continuation: CancellableContinuationImpl<T>
1398-
) : JobNode<JobSupport>(job) {
1400+
) : JobNode() {
13991401
override fun invoke(cause: Throwable?) {
14001402
val state = job.state
14011403
assert { state !is Incomplete }
@@ -1411,28 +1413,25 @@ private class ResumeAwaitOnCompletion<T>(
14111413
}
14121414

14131415
internal class DisposeOnCompletion(
1414-
job: Job,
14151416
private val handle: DisposableHandle
1416-
) : JobNode<Job>(job) {
1417+
) : JobNode() {
14171418
override fun invoke(cause: Throwable?) = handle.dispose()
14181419
}
14191420

14201421
private class SelectJoinOnCompletion<R>(
1421-
job: JobSupport,
14221422
private val select: SelectInstance<R>,
14231423
private val block: suspend () -> R
1424-
) : JobNode<JobSupport>(job) {
1424+
) : JobNode() {
14251425
override fun invoke(cause: Throwable?) {
14261426
if (select.trySelect())
14271427
block.startCoroutineCancellable(select.completion)
14281428
}
14291429
}
14301430

14311431
private class SelectAwaitOnCompletion<T, R>(
1432-
job: JobSupport,
14331432
private val select: SelectInstance<R>,
14341433
private val block: suspend (T) -> R
1435-
) : JobNode<JobSupport>(job) {
1434+
) : JobNode() {
14361435
override fun invoke(cause: Throwable?) {
14371436
if (select.trySelect())
14381437
job.selectAwaitCompletion(select, block)
@@ -1445,12 +1444,11 @@ private class SelectAwaitOnCompletion<T, R>(
14451444
* Marker for node that shall be invoked on in _cancelling_ state.
14461445
* **Note: may be invoked multiple times.**
14471446
*/
1448-
internal abstract class JobCancellingNode<out J : Job>(job: J) : JobNode<J>(job)
1447+
internal abstract class JobCancellingNode : JobNode()
14491448

14501449
private class InvokeOnCancelling(
1451-
job: Job,
14521450
private val handler: CompletionHandler
1453-
) : JobCancellingNode<Job>(job) {
1451+
) : JobCancellingNode() {
14541452
// delegate handler shall be invoked at most once, so here is an additional flag
14551453
private val _invoked = atomic(0) // todo: replace with atomic boolean after migration to recent atomicFu
14561454
override fun invoke(cause: Throwable?) {
@@ -1459,18 +1457,16 @@ private class InvokeOnCancelling(
14591457
}
14601458

14611459
internal class ChildHandleNode(
1462-
parent: JobSupport,
14631460
@JvmField val childJob: ChildJob
1464-
) : JobCancellingNode<JobSupport>(parent), ChildHandle {
1461+
) : JobCancellingNode(), ChildHandle {
14651462
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
14661463
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
14671464
}
14681465

14691466
// Same as ChildHandleNode, but for cancellable continuation
14701467
internal class ChildContinuation(
1471-
parent: Job,
14721468
@JvmField val child: CancellableContinuationImpl<*>
1473-
) : JobCancellingNode<Job>(parent) {
1469+
) : JobCancellingNode() {
14741470
override fun invoke(cause: Throwable?) {
14751471
child.parentCancelled(child.getContinuationCancellationCause(job))
14761472
}

Diff for: kotlinx-coroutines-core/common/src/selects/Select.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,13 @@ internal class SelectBuilderImpl<in R>(
327327
private fun initCancellability() {
328328
val parent = context[Job] ?: return
329329
val newRegistration = parent.invokeOnCompletion(
330-
onCancelling = true, handler = SelectOnCancelling(parent).asHandler)
330+
onCancelling = true, handler = SelectOnCancelling().asHandler)
331331
parentHandle = newRegistration
332332
// now check our state _after_ registering
333333
if (isSelected) newRegistration.dispose()
334334
}
335335

336-
private inner class SelectOnCancelling(job: Job) : JobCancellingNode<Job>(job) {
336+
private inner class SelectOnCancelling : JobCancellingNode() {
337337
// Note: may be invoked multiple times, but only the first trySelect succeeds anyway
338338
override fun invoke(cause: Throwable?) {
339339
if (trySelect())
@@ -552,7 +552,7 @@ internal class SelectBuilderImpl<in R>(
552552
return decision
553553
}
554554

555-
override val atomicOp: AtomicOp<*>?
555+
override val atomicOp: AtomicOp<*>
556556
get() = otherOp.atomicOp
557557
}
558558

0 commit comments

Comments
 (0)