Skip to content

Commit 8fbd8b7

Browse files
committed
Recursively check whether parent handles exception to avoid duplicates reporting when Job() is present in the hierarchy
Fixes #689
1 parent b0f6e05 commit 8fbd8b7

File tree

5 files changed

+77
-14
lines changed

5 files changed

+77
-14
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin
387387
protected final fun getCompletionCause ()Ljava/lang/Throwable;
388388
protected final fun getCompletionCauseHandled ()Z
389389
public final fun getCompletionExceptionOrNull ()Ljava/lang/Throwable;
390-
protected fun getHandlesException ()Z
391390
public final fun getKey ()Lkotlin/coroutines/CoroutineContext$Key;
392391
public final fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0;
393392
protected fun handleJobException (Ljava/lang/Throwable;)Z

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

+30-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlinx.coroutines.intrinsics.*
1111
import kotlinx.coroutines.selects.*
1212
import kotlin.coroutines.*
1313
import kotlin.coroutines.intrinsics.*
14+
import kotlin.js.*
1415
import kotlin.jvm.*
1516

1617
/**
@@ -127,7 +128,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
127128
private val _state = atomic<Any?>(if (active) EMPTY_ACTIVE else EMPTY_NEW)
128129

129130
@Volatile
130-
private var parentHandle: ChildHandle? = null
131+
@JvmField
132+
internal var parentHandle: ChildHandle? = null
131133

132134
// ------------ initialization ------------
133135

@@ -639,12 +641,12 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
639641
// determine root cancellation cause of this job (why is it cancelling its children?)
640642
val state = this.state
641643
val rootCause = when (state) {
642-
is Finishing -> state.rootCause as? CancellationException ?: JobCancellationException("Parent job was cancelled", state.rootCause, this)
643-
is CompletedExceptionally -> state.cause as? CancellationException ?: JobCancellationException("Parent job was cancelled", state.cause, this)
644+
is Finishing -> state.rootCause
645+
is CompletedExceptionally -> state.cause
644646
is Incomplete -> error("Cannot be cancelling child in this state: $state")
645647
else -> null // create exception with the below code on normal completion
646648
}
647-
return rootCause ?: JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
649+
return (rootCause as? CancellationException) ?: JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
648650
}
649651

650652
// cause is Throwable or ParentJob when cancelChild was invoked
@@ -915,13 +917,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
915917
protected open val cancelsParent: Boolean get() = true
916918

917919
/**
918-
* Returns `true` for jobs that handle their exceptions or integrate them
919-
* into the job's result via [onCompletionInternal]. The only instance of the [Job] that does not
920-
* handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl].
920+
* Returns `true` for jobs that handle their exceptions or integrate them into the job's result via [onCompletionInternal].
921+
* A valid implementation of this getter should recursively check parent as well before returning `false`.
921922
*
923+
* The only instance of the [Job] that does not handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl].
922924
* @suppress **This is unstable API and it is subject to change.*
923925
*/
924-
protected open val handlesException: Boolean get() = true
926+
internal open val handlesException: Boolean get() = true
925927

926928
/**
927929
* Handles the final job [exception] that was not handled by the parent coroutine.
@@ -1222,10 +1224,29 @@ private class Empty(override val isActive: Boolean) : Incomplete {
12221224
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
12231225
init { initParentJobInternal(parent) }
12241226
override val onCancelComplete get() = true
1225-
override val handlesException: Boolean get() = false
1227+
/*
1228+
* Check whether parent is able to handle exceptions as well.
1229+
* With this check, an exception in that pattern will be handled once:
1230+
* ```
1231+
* launch {
1232+
* val child = Job(coroutineContext[Job])
1233+
* launch(child) { throw ... }
1234+
* }
1235+
* ```
1236+
*/
1237+
override val handlesException: Boolean = handlesException()
12261238
override fun complete() = makeCompleting(Unit)
12271239
override fun completeExceptionally(exception: Throwable): Boolean =
12281240
makeCompleting(CompletedExceptionally(exception))
1241+
1242+
@JsName("handlesExceptionF")
1243+
private fun handlesException(): Boolean {
1244+
var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false
1245+
while (true) {
1246+
if (parentJob.handlesException) return true
1247+
parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false
1248+
}
1249+
}
12291250
}
12301251

12311252
// -------- invokeOnCompletion nodes

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.test.*
1616
class ParentCancellationTest : TestBase() {
1717
@Test
1818
fun testJobChild() = runTest {
19-
testParentCancellation(expectUnhandled = true) { fail ->
19+
testParentCancellation(expectUnhandled = false) { fail ->
2020
val child = Job(coroutineContext[Job])
2121
CoroutineScope(coroutineContext + child).fail()
2222
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ class SupervisorTest : TestBase() {
172172
deferred.await()
173173
expectUnreached()
174174
} catch (e: CancellationException) {
175-
assertTrue(e.cause?.cause is TestException1)
175+
val cause = if (RECOVER_STACK_TRACES) e.cause?.cause!! else e.cause
176+
assertTrue(cause is TestException1)
176177
finish(3)
177178
}
178179
}

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

+44-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class JobExceptionHandlingTest : TestBase() {
1818
/*
1919
* Root parent: JobImpl()
2020
* Child: throws ISE
21-
* Result: ISE in exception handlerWithContextExceptionHandlingTest
21+
* Result: ISE in exception handler
2222
*/
2323
val exception = captureExceptionsRun {
2424
val job = Job()
@@ -304,6 +304,48 @@ class JobExceptionHandlingTest : TestBase() {
304304
finish(2)
305305
}
306306

307+
@Test
308+
fun testExceptionIsNotReported() = runTest {
309+
try {
310+
expect(1)
311+
coroutineScope {
312+
val job = Job(coroutineContext[Job])
313+
launch(job) {
314+
throw TestException()
315+
}
316+
}
317+
expectUnreached()
318+
} catch (e: TestException) {
319+
finish(2)
320+
}
321+
}
322+
323+
@Test
324+
fun testExceptionIsNotReportedTripleChain() = runTest {
325+
try {
326+
expect(1)
327+
coroutineScope {
328+
val job = Job(Job(Job(coroutineContext[Job])))
329+
launch(job) {
330+
throw TestException()
331+
}
332+
}
333+
expectUnreached()
334+
} catch (e: TestException) {
335+
finish(2)
336+
}
337+
}
338+
339+
@Test
340+
fun testAttachToCancelledJob() = runTest(unhandled = listOf({ e -> e is TestException })) {
341+
val parent = launch(Job()) {
342+
throw TestException()
343+
}.apply { join() }
344+
345+
launch(parent) { expectUnreached() }
346+
launch(Job(parent)) { expectUnreached() }
347+
}
348+
307349
@Test
308350
fun testBadException() = runTest(unhandled = listOf({e -> e is BadException})) {
309351
val job = launch(Job()) {
@@ -313,7 +355,7 @@ class JobExceptionHandlingTest : TestBase() {
313355
throw BadException()
314356
}
315357

316-
launch(start = CoroutineStart.ATOMIC) {
358+
launch(start = ATOMIC) {
317359
expect(4)
318360
throw BadException()
319361
}

0 commit comments

Comments
 (0)