Skip to content

Commit 7e23875

Browse files
committed
Fixed race between creation of a child & cancellation of parent job
1 parent ecbc85c commit 7e23875

File tree

2 files changed

+65
-3
lines changed

2 files changed

+65
-3
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
255255

256256
// fast-path method to finalize normally completed coroutines without children
257257
private fun tryFinalizeSimpleState(state: Incomplete, update: Any?, mode: Int): Boolean {
258-
check(state !is Finishing) // only for non-finishing state
258+
check(state is Empty || state is JobNode<*>) // only simple state without lists where children can concurrently add
259259
check(update !is CompletedExceptionally) // only for normal completion
260260
if (!_state.compareAndSet(state, update)) return false
261261
completeStateFinalization(state, update, mode, false)
@@ -735,9 +735,9 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
735735
return COMPLETING_ALREADY_COMPLETING
736736
// find first child
737737
val child = firstChild(state)
738-
// FAST PATH -- no children to wait for && not finishing && not failing => can complete immediately
738+
// FAST PATH -- no children to wait for && simple state (no list) && not failing => can complete immediately
739739
// Failures always have to go through Finishing state to serialize exception handling
740-
if (child == null && state !is Finishing && proposedUpdate !is CompletedExceptionally) {
740+
if (child == null && (state is Empty || state is JobNode<*>) && proposedUpdate !is CompletedExceptionally) {
741741
if (!tryFinalizeSimpleState(state, proposedUpdate, mode)) return COMPLETING_RETRY
742742
return COMPLETING_COMPLETED
743743
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.experimental
6+
7+
import org.junit.*
8+
import org.junit.Test
9+
import java.util.concurrent.*
10+
import kotlin.test.*
11+
12+
class JobChildStressTest : TestBase() {
13+
private val N_ITERATIONS = 10_000 * stressTestMultiplier
14+
private val pool = newFixedThreadPoolContext(3, "JobChildStressTest")
15+
16+
@After
17+
fun tearDown() {
18+
pool.close()
19+
}
20+
21+
/**
22+
* Perform concurrent launch of a child job & cancellation of the explicit parent job
23+
*/
24+
@Test
25+
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
26+
fun testChild() = runTest {
27+
val barrier = CyclicBarrier(3)
28+
repeat(N_ITERATIONS) {
29+
var wasLaunched = false
30+
var unhandledException: Throwable? = null
31+
val handler = CoroutineExceptionHandler { _, ex ->
32+
unhandledException = ex
33+
}
34+
val scope = CoroutineScope(pool + handler)
35+
val parent = CompletableDeferred<Unit>()
36+
// concurrent child launcher
37+
val launcher = scope.launch {
38+
barrier.await()
39+
// A: launch child for a parent job
40+
launch(parent) {
41+
wasLaunched = true
42+
throw TestException()
43+
}
44+
}
45+
// concurrent cancel
46+
val canceller = scope.launch {
47+
barrier.await()
48+
// B: cancel parent job of a child
49+
parent.cancel()
50+
}
51+
barrier.await()
52+
joinAll(launcher, canceller, parent)
53+
assertNull(unhandledException)
54+
if (wasLaunched) {
55+
val exception = parent.getCompletionExceptionOrNull()
56+
assertTrue(exception is TestException, "exception=$exception")
57+
}
58+
}
59+
}
60+
61+
private class TestException : Exception()
62+
}

0 commit comments

Comments
 (0)