@@ -127,6 +127,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
127
127
// Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
128
128
private val _state = atomic<Any ?>(if (active) EMPTY_ACTIVE else EMPTY_NEW )
129
129
130
+ /* * `true` means that the Job is cancelling and shouldn't accept [invokeOnCompletion] with `onCancelling = true` */
131
+ private val onCancellingHandlersNotAccepted = atomic(false )
132
+
130
133
private val _parentHandle = atomic<ChildHandle ?>(null )
131
134
internal var parentHandle: ChildHandle ?
132
135
get() = _parentHandle .value
@@ -235,6 +238,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
235
238
if (! wasCancelling) onCancelling(finalException)
236
239
onCompletionInternal(finalState)
237
240
// Then CAS to completed state -> it must succeed
241
+ // forbid any new children
242
+ onCancellingHandlersNotAccepted.value = true
238
243
val casSuccess = _state .compareAndSet(state, finalState.boxIncomplete())
239
244
assert { casSuccess }
240
245
// And process all post-completion actions
@@ -326,7 +331,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
326
331
}
327
332
328
333
private fun notifyCancelling (list : NodeList , cause : Throwable ) {
329
- // first cancel our own children
334
+ // then cancel our own children
330
335
onCancelling(cause)
331
336
notifyHandlers<JobCancellingNode >(list, cause)
332
337
// then cancel parent
@@ -359,8 +364,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
359
364
return parent.childCancelled(cause) || isCancellation
360
365
}
361
366
362
- private fun NodeList.notifyCompletion (cause : Throwable ? ) =
367
+ private fun NodeList.notifyCompletion (cause : Throwable ? ) {
368
+ close()
363
369
notifyHandlers<JobNode >(this , cause)
370
+ }
364
371
365
372
private inline fun <reified T : JobNode > notifyHandlers (list : NodeList , cause : Throwable ? ) {
366
373
var exception: Throwable ? = null
@@ -458,52 +465,63 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
458
465
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
459
466
val node: JobNode = makeNode(handler, onCancelling)
460
467
loopOnState { state ->
461
- when (state) {
462
- is Empty -> { // EMPTY_X state -- no completion handlers
468
+ when {
469
+ state !is Incomplete || onCancelling && onCancellingHandlersNotAccepted.value -> {
470
+ if (invokeImmediately) {
471
+ val exception = when (state) {
472
+ is CompletedExceptionally -> state.cause
473
+ is Finishing -> state.rootCause
474
+ else -> null
475
+ }
476
+ // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
477
+ // because we play type tricks on Kotlin/JS and handler is not necessarily a function there
478
+ handler.invokeIt(exception)
479
+ }
480
+ return NonDisposableHandle
481
+ }
482
+ state is Empty -> { // EMPTY_X state -- no completion handlers
463
483
if (state.isActive) {
464
484
// try move to SINGLE state
465
485
if (_state .compareAndSet(state, node)) return node
466
486
} else
467
487
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
468
488
}
469
- is Incomplete -> {
489
+ else -> {
490
+ // is Incomplete
470
491
val list = state.list
471
492
if (list == null ) { // SINGLE/SINGLE+
472
493
promoteSingleToNodeList(state as JobNode )
473
494
} else {
474
- var rootCause: Throwable ? = null
475
- var handle: DisposableHandle = NonDisposableHandle
476
- if (onCancelling && state is Finishing ) {
477
- synchronized(state) {
478
- // check if we are installing cancellation handler on job that is being cancelled
479
- rootCause = state.rootCause // != null if cancelling job
480
- // We add node to the list in two cases --- either the job is not being cancelled
481
- // or we are adding a child to a coroutine that is not completing yet
482
- if (rootCause == null || handler.isHandlerOf<ChildHandleNode >() && ! state.isCompleting) {
483
- // Note: add node the list while holding lock on state (make sure it cannot change)
484
- if (! addLastAtomic(state, list, node)) return @loopOnState // retry
485
- // just return node if we don't have to invoke handler (not cancelling yet)
486
- if (rootCause == null ) return node
487
- // otherwise handler is invoked immediately out of the synchronized section & handle returned
488
- handle = node
495
+ if (onCancelling) {
496
+ if (state is Finishing ) {
497
+ val rootCause: Throwable ?
498
+ val handle: DisposableHandle
499
+ synchronized(state) {
500
+ // check if we are installing cancellation handler on job that is being cancelled
501
+ rootCause = state.rootCause // != null if cancelling job
502
+ // We add node to the list in two cases --- either the job is not being cancelled
503
+ // or we are adding a child to a coroutine that is not completing yet
504
+ if (rootCause == null || handler.isHandlerOf<ChildHandleNode >() && ! state.isCompleting) {
505
+ // Note: add node the list while holding lock on state (make sure it cannot change)
506
+ if (! list.addLastIf(node) { ! onCancellingHandlersNotAccepted.value }) return @loopOnState // retry
507
+ // just return node if we don't have to invoke handler (not cancelling yet)
508
+ if (rootCause == null ) return node
509
+ // otherwise handler is invoked immediately out of the synchronized section & handle returned
510
+ handle = node
511
+ } else {
512
+ handle = NonDisposableHandle
513
+ }
489
514
}
515
+ // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
516
+ if (invokeImmediately) handler.invokeIt(rootCause)
517
+ return handle
490
518
}
491
- }
492
- if (rootCause != null ) {
493
- // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
494
- if (invokeImmediately) handler.invokeIt(rootCause)
495
- return handle
519
+ if (list.addLastIf(node) { ! onCancellingHandlersNotAccepted.value }) return node
496
520
} else {
497
- if (addLastAtomic(state, list, node)) return node
521
+ if (list.addLast( node)) return node
498
522
}
499
523
}
500
524
}
501
- else -> { // is complete
502
- // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
503
- // because we play type tricks on Kotlin/JS and handler is not necessarily a function there
504
- if (invokeImmediately) handler.invokeIt((state as ? CompletedExceptionally )?.cause)
505
- return NonDisposableHandle
506
- }
507
525
}
508
526
}
509
527
}
@@ -880,6 +898,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
880
898
val finishing = state as ? Finishing ? : Finishing (list, false , null )
881
899
// must synchronize updates to finishing state
882
900
var notifyRootCause: Throwable ? = null
901
+ // forbid any new children
902
+ onCancellingHandlersNotAccepted.value = true
883
903
synchronized(finishing) {
884
904
// check if this state is already completing
885
905
if (finishing.isCompleting) return COMPLETING_ALREADY
0 commit comments