Skip to content

Commit 88e72f9

Browse files
qwwdfsadelizarov
authored andcommitted
Exception unwrapping logic rework, do not unwrap cancellation exception if parent can handle exceptions
It allows to properly handle only relevant exceptions by child without reporting the same exception multiple times, fixes suppressions hierarchy and allows to implement supervisorScope properly
1 parent 4fc0a11 commit 88e72f9

File tree

7 files changed

+242
-30
lines changed

7 files changed

+242
-30
lines changed

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -234,18 +234,18 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
234234
* This is a place where we step on our API limitation:
235235
* We can't distinguish internal JobCancellationException from our parent
236236
* from external cancellation, thus we ought to collect all exceptions.
237-
*
238-
* But it has negative consequences: same exception can be added as suppressed more than once.
239-
* Consider concurrent parent-child relationship:
240-
* 1) Child throws E1 and parent throws E2.
241-
* 2) Parent goes to "Cancelling(E1)" and cancels child with E1
242-
* 3) Child goes to "Cancelling(E1)", but throws an exception E2
243-
* 4) When child throws, it notifies parent that it is cancelled, adding its exception to parent's list of exceptions
244-
* 5) Child builds final exception: E1 with suppressed E2, reports it to parent.
245-
* 6) Parent aggregates three exceptions: original E1, reported E2 and "final" E1.
246-
* It filters the third exception, but adds the second one to the first one, thus adding suppressed duplicate.
247-
*
248-
* Note that it's only happening when both parent and child throw exception simultaneously.
237+
* If parent is cancelling, it cancels its children with JCE(rootCause).
238+
* When child is building final exception, it can skip JCE(anything) if it knows
239+
* that parent handles exceptions, because parent should already have this exception.
240+
* If parent does not, then we should unwrap exception, otherwise in the following code
241+
* ```
242+
* val parent = Job()
243+
* launch(parent) {
244+
* try { delay() } finally { throw E2() }
245+
* }
246+
* parent.cancel(E1)
247+
* ```
248+
* E1 will be lost.
249249
*/
250250
var rootCause = exceptions[0]
251251
if (rootCause is CancellationException) {
@@ -275,13 +275,17 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
275275
return suppressed
276276
}
277277

278-
private tailrec fun unwrap(exception: Throwable): Throwable? =
279-
if (exception is CancellationException) {
278+
private tailrec fun unwrap(exception: Throwable): Throwable? {
279+
if (exception is CancellationException && parentHandlesExceptions) {
280+
return null
281+
}
282+
return if (exception is CancellationException) {
280283
val cause = exception.cause
281284
if (cause !== null) unwrap(cause) else null
282285
} else {
283286
exception
284287
}
288+
}
285289

286290
// fast-path method to finalize normally completed coroutines without children
287291
private fun tryFinalizeSimpleState(state: Incomplete, update: Any?, mode: Int): Boolean {
@@ -923,6 +927,10 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
923927
*/
924928
protected open val handlesException: Boolean get() = true
925929

930+
// returns true when we know that parent handles exceptions
931+
private val parentHandlesExceptions: Boolean get() =
932+
(parentHandle as? ChildHandleNode)?.job?.handlesException ?: false
933+
926934
/**
927935
* This method is invoked **exactly once** when the final exception of the job is determined
928936
* and before it becomes complete. At the moment of invocation the job and all its children are complete.

common/kotlinx-coroutines-core-common/test/SupervisorTest.kt

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,73 @@ class SupervisorTest : TestBase() {
107107
}
108108

109109
@Test
110-
fun testSupervisorFlaw() = runTest {
110+
fun testSupervisorThrows() = runTest {
111111
try {
112112
supervisorScope {
113113
expect(1)
114114
launch {
115-
// Test exception from supervisor is handled by launch though it clearly will be rethtown by supervisorScope{}
116115
expect(2)
117116
delay(Long.MAX_VALUE)
118117
}
119118

119+
launch {
120+
expect(3)
121+
delay(Long.MAX_VALUE)
122+
}
123+
120124
yield()
121-
expect(3)
125+
expect(4)
122126
throw TestException1()
123127
}
124128
} catch (e: TestException1) {
125-
finish(4)
129+
finish(5)
130+
}
131+
}
132+
133+
@Test
134+
@Ignore // JS BE bug
135+
fun testSupervisorThrowsWithFailingChild() = runTest(unhandled = listOf({e -> e is TestException2})) {
136+
try {
137+
supervisorScope {
138+
expect(1)
139+
launch {
140+
expect(2)
141+
delay(Long.MAX_VALUE)
142+
}
143+
144+
launch {
145+
expect(3)
146+
try {
147+
delay(Long.MAX_VALUE)
148+
} finally {
149+
throw TestException2()
150+
}
151+
}
152+
153+
yield()
154+
expect(4)
155+
throw TestException1()
156+
}
157+
} catch (e: TestException1) {
158+
finish(5)
159+
}
160+
}
161+
162+
@Test
163+
fun testAsyncCancellation() = runTest {
164+
val parent = SupervisorJob()
165+
val deferred = async(parent) {
166+
expect(2)
167+
delay(Long.MAX_VALUE)
168+
}
169+
expect(1)
170+
yield()
171+
parent.cancel(TestException1())
172+
try {
173+
deferred.await()
174+
expectUnreached()
175+
} catch (e: TestException1) {
176+
finish(3)
126177
}
127178
}
128179

@@ -146,4 +197,4 @@ class SupervisorTest : TestBase() {
146197

147198
private class TestException1 : Exception()
148199
private class TestException2 : Exception()
149-
}
200+
}

core/kotlinx-coroutines-core/test/WithTimeoutOrNullJvmTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package kotlinx.coroutines.experimental
66

7-
import java.io.*
87
import kotlin.test.*
98

109
class WithTimeoutOrNullJvmTest : TestBase() {

core/kotlinx-coroutines-core/test/exceptions/JobExceptionHandlingTest.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,45 @@ class JobExceptionHandlingTest : TestBase() {
3434
checkException<IllegalStateException>(exception)
3535
}
3636

37+
@Test
38+
fun testAsyncCancellationWithCause() = runTest {
39+
val deferred = async {
40+
expect(2)
41+
delay(Long.MAX_VALUE)
42+
}
43+
44+
expect(1)
45+
yield()
46+
deferred.cancel(IOException())
47+
try {
48+
deferred.await()
49+
expectUnreached()
50+
} catch (e: IOException) {
51+
assertTrue(e.suppressed().isEmpty())
52+
finish(3)
53+
}
54+
}
55+
56+
@Test
57+
fun testAsyncCancellationWithCauseAndParent() = runTest {
58+
val parent = Job()
59+
val deferred = async(parent) {
60+
expect(2)
61+
delay(Long.MAX_VALUE)
62+
}
63+
64+
expect(1)
65+
yield()
66+
parent.cancel(IOException())
67+
try {
68+
deferred.await()
69+
expectUnreached()
70+
} catch (e: IOException) {
71+
assertTrue(e.suppressed().isEmpty())
72+
finish(3)
73+
}
74+
}
75+
3776
@Test
3877
fun testExceptionDuringCancellation() {
3978
/*
@@ -191,7 +230,6 @@ class JobExceptionHandlingTest : TestBase() {
191230
checkException<ArithmeticException>(suppressed[0])
192231
}
193232

194-
195233
@Test
196234
fun testMultipleChildrenThrowAtomically() {
197235
/*

core/kotlinx-coroutines-core/test/exceptions/JobNestedExceptionsTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ class JobNestedExceptionsTest : TestBase() {
112112
val exception = exceptions[0]
113113
assertTrue(exception is ArithmeticException, "Exception is $exception")
114114
val suppressed = exception.suppressed()
115-
// the order of suppressed exceptions here is a bit hacky, may change in the future
116-
checkException<NullPointerException>(suppressed[0])
117-
checkException<IOException>(suppressed[1])
115+
val ioe = suppressed[0]
116+
assertTrue(ioe is IOException)
117+
checkException<NullPointerException>(ioe.suppressed()[0])
118118
checkCycles(exception)
119119
}
120120
}

core/kotlinx-coroutines-core/test/exceptions/ProduceExceptionsTest.kt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,102 @@ class ProduceExceptionsTest : TestBase() {
7070
}
7171
}
7272

73+
@Test
74+
fun testCancelProduceChannel() = runTest {
75+
var channel: ReceiveChannel<Int>? = null
76+
channel = produce {
77+
expect(2)
78+
channel!!.cancel()
79+
try {
80+
send(1)
81+
} catch (e: ClosedSendChannelException) {
82+
expect(3)
83+
throw e
84+
}
85+
}
86+
87+
expect(1)
88+
yield()
89+
try {
90+
channel.receive()
91+
} catch (e: ClosedReceiveChannelException) {
92+
assertTrue(e.suppressed().isEmpty())
93+
finish(4)
94+
}
95+
}
96+
97+
@Test
98+
fun testCancelProduceChannelWithException() = runTest {
99+
var channel: ReceiveChannel<Int>? = null
100+
channel = produce {
101+
expect(2)
102+
channel!!.cancel(TestException())
103+
try {
104+
send(1)
105+
// Not a ClosedForSendException
106+
} catch (e: TestException) {
107+
expect(3)
108+
throw e
109+
}
110+
}
111+
112+
expect(1)
113+
yield()
114+
try {
115+
channel.receive()
116+
} catch (e: TestException) {
117+
assertTrue(e.suppressed().isEmpty())
118+
finish(4)
119+
}
120+
}
121+
122+
@Test
123+
fun testCancelChannelWithJob() = runTest {
124+
val job = Job()
125+
val channel = produce(job) {
126+
expect(2)
127+
job.cancel()
128+
try {
129+
send(1)
130+
} catch (e: CancellationException) {
131+
expect(3)
132+
throw e
133+
}
134+
}
135+
136+
expect(1)
137+
yield()
138+
try {
139+
channel.receive()
140+
} catch (e: CancellationException) {
141+
assertTrue(e.suppressed().isEmpty())
142+
finish(4)
143+
}
144+
}
145+
146+
@Test
147+
fun testCancelChannelWithJobWithException() = runTest {
148+
val job = Job()
149+
val channel = produce(job) {
150+
expect(2)
151+
job.cancel(TestException2())
152+
try {
153+
send(1)
154+
} catch (e: CancellationException) { // Not a TestException2
155+
expect(3)
156+
throw e
157+
}
158+
}
159+
160+
expect(1)
161+
yield()
162+
try {
163+
channel.receive()
164+
} catch (e: TestException2) {
165+
finish(4)
166+
}
167+
}
168+
73169
class TestException : Exception()
74170
class TestException2 : Exception()
75171
}

0 commit comments

Comments
 (0)