diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 16ba32a385..661f3605cc 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -382,12 +382,11 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public final fun getCancellationException ()Ljava/util/concurrent/CancellationException; protected fun getCancelsParent ()Z - public fun getChildJobCancellationCause ()Ljava/lang/Throwable; + public fun getChildJobCancellationCause ()Ljava/util/concurrent/CancellationException; public final fun getChildren ()Lkotlin/sequences/Sequence; protected final fun getCompletionCause ()Ljava/lang/Throwable; protected final fun getCompletionCauseHandled ()Z public final fun getCompletionExceptionOrNull ()Ljava/lang/Throwable; - protected fun getHandlesException ()Z public final fun getKey ()Lkotlin/coroutines/CoroutineContext$Key; public final fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; protected fun handleJobException (Ljava/lang/Throwable;)Z @@ -447,7 +446,7 @@ public abstract interface annotation class kotlinx/coroutines/ObsoleteCoroutines } public abstract interface class kotlinx/coroutines/ParentJob : kotlinx/coroutines/Job { - public abstract fun getChildJobCancellationCause ()Ljava/lang/Throwable; + public abstract fun getChildJobCancellationCause ()Ljava/util/concurrent/CancellationException; } public final class kotlinx/coroutines/ParentJob$DefaultImpls { diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt index 1ccbaa75aa..f818749420 100644 --- a/kotlinx-coroutines-core/common/src/CompletableJob.kt +++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt @@ -24,6 +24,7 @@ public interface CompletableJob : Job { /** * Completes this job exceptionally with a given [exception]. The result is `true` if this job was * completed as a result of this invocation and `false` otherwise (if it was already completed). + * [exception] parameter is used as an additional debug information that is not handled by any exception handlers. * * Subsequent invocations of this function have no effect and always produce `false`. * diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 2471e25a17..8c20fee37b 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -422,10 +422,13 @@ public interface ParentJob : Job { * Child job is using this method to learn its cancellation cause when the parent cancels it with [ChildJob.parentCancelled]. * This method is invoked only if the child was not already being cancelled. * + * Note that [CancellationException] is the method's return type: if child is cancelled by its parent, + * then the original exception is **already** handled by either the parent or the original source of failure. + * * @suppress **This is unstable API and it is subject to change.** */ @InternalCoroutinesApi - public fun getChildJobCancellationCause(): Throwable + public fun getChildJobCancellationCause(): CancellationException } /** diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index a20774b774..ff3190971f 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* +import kotlin.js.* import kotlin.jvm.* /** @@ -127,7 +128,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren private val _state = atomic(if (active) EMPTY_ACTIVE else EMPTY_NEW) @Volatile - private var parentHandle: ChildHandle? = null + @JvmField + internal var parentHandle: ChildHandle? = null // ------------ initialization ------------ @@ -635,29 +637,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren private fun createJobCancellationException() = JobCancellationException("Job was cancelled", null, this) - override fun getChildJobCancellationCause(): Throwable { + override fun getChildJobCancellationCause(): CancellationException { // determine root cancellation cause of this job (why is it cancelling its children?) val state = this.state val rootCause = when (state) { is Finishing -> state.rootCause - is Incomplete -> error("Cannot be cancelling child in this state: $state") is CompletedExceptionally -> state.cause + is Incomplete -> error("Cannot be cancelling child in this state: $state") else -> null // create exception with the below code on normal completion } - /* - * If this parent job handles exceptions, then wrap cause into JobCancellationException, because we - * don't want the child to handle this exception on more time. Otherwise, pass our original rootCause - * to the child for cancellation. - */ - return if (rootCause == null || handlesException && rootCause !is CancellationException) { - JobCancellationException("Parent job is ${stateString(state)}", rootCause, this) - } else { - rootCause - } + return (rootCause as? CancellationException) ?: JobCancellationException("Parent job is ${stateString(state)}", rootCause, this) } // cause is Throwable or ParentJob when cancelChild was invoked - private fun createCauseException(cause: Any?): Throwable = when(cause) { + private fun createCauseException(cause: Any?): Throwable = when (cause) { is Throwable? -> cause ?: createJobCancellationException() else -> (cause as ParentJob).getChildJobCancellationCause() } @@ -924,13 +917,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren protected open val cancelsParent: Boolean get() = true /** - * Returns `true` for jobs that handle their exceptions or integrate them - * into the job's result via [onCompletionInternal]. The only instance of the [Job] that does not - * handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl]. + * Returns `true` for jobs that handle their exceptions or integrate them into the job's result via [onCompletionInternal]. + * A valid implementation of this getter should recursively check parent as well before returning `false`. * + * The only instance of the [Job] that does not handle its exceptions is [JobImpl] and its subclass [SupervisorJobImpl]. * @suppress **This is unstable API and it is subject to change.* */ - protected open val handlesException: Boolean get() = true + internal open val handlesException: Boolean get() = true /** * Handles the final job [exception] that was not handled by the parent coroutine. @@ -1231,10 +1224,29 @@ private class Empty(override val isActive: Boolean) : Incomplete { internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob { init { initParentJobInternal(parent) } override val onCancelComplete get() = true - override val handlesException: Boolean get() = false + /* + * Check whether parent is able to handle exceptions as well. + * With this check, an exception in that pattern will be handled once: + * ``` + * launch { + * val child = Job(coroutineContext[Job]) + * launch(child) { throw ... } + * } + * ``` + */ + override val handlesException: Boolean = handlesException() override fun complete() = makeCompleting(Unit) override fun completeExceptionally(exception: Throwable): Boolean = makeCompleting(CompletedExceptionally(exception)) + + @JsName("handlesExceptionF") + private fun handlesException(): Boolean { + var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false + while (true) { + if (parentJob.handlesException) return true + parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false + } + } } // -------- invokeOnCompletion nodes diff --git a/kotlinx-coroutines-core/common/test/ParentCancellationTest.kt b/kotlinx-coroutines-core/common/test/ParentCancellationTest.kt index 23f2a10a97..96c5cf3f4f 100644 --- a/kotlinx-coroutines-core/common/test/ParentCancellationTest.kt +++ b/kotlinx-coroutines-core/common/test/ParentCancellationTest.kt @@ -16,7 +16,7 @@ import kotlin.test.* class ParentCancellationTest : TestBase() { @Test fun testJobChild() = runTest { - testParentCancellation(expectUnhandled = true) { fail -> + testParentCancellation(expectUnhandled = false) { fail -> val child = Job(coroutineContext[Job]) CoroutineScope(coroutineContext + child).fail() } diff --git a/kotlinx-coroutines-core/common/test/SupervisorTest.kt b/kotlinx-coroutines-core/common/test/SupervisorTest.kt index 7fdd8fcbdd..fae7091851 100644 --- a/kotlinx-coroutines-core/common/test/SupervisorTest.kt +++ b/kotlinx-coroutines-core/common/test/SupervisorTest.kt @@ -171,7 +171,9 @@ class SupervisorTest : TestBase() { try { deferred.await() expectUnreached() - } catch (e: TestException1) { + } catch (e: CancellationException) { + val cause = if (RECOVER_STACK_TRACES) e.cause?.cause!! else e.cause + assertTrue(cause is TestException1) finish(3) } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt index 53c7680406..13023e3122 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt @@ -54,7 +54,7 @@ class CapturingHandler : AbstractCoroutineContextElement(CoroutineExceptionHandl } } -internal fun runBlock( +internal fun captureExceptionsRun( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit ): Throwable { @@ -63,7 +63,7 @@ internal fun runBlock( return handler.getException() } -internal fun runBlockForMultipleExceptions( +internal fun captureMultipleExceptionsRun( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit ): List { diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt index 746909a61e..eaa73bdb50 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt @@ -20,7 +20,7 @@ class JobExceptionHandlingTest : TestBase() { * Child: throws ISE * Result: ISE in exception handler */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job, start = ATOMIC) { expect(2) @@ -36,29 +36,30 @@ class JobExceptionHandlingTest : TestBase() { } @Test - fun testAsyncCancellationWithCause() = runTest { - val deferred = async(NonCancellable) { + fun testAsyncCancellationWithCauseAndParent() = runTest { + val parent = Job() + val deferred = async(parent) { expect(2) delay(Long.MAX_VALUE) } expect(1) yield() - deferred.cancel(TestCancellationException("TEST")) + parent.completeExceptionally(IOException()) try { deferred.await() expectUnreached() - } catch (e: TestCancellationException) { - assertEquals("TEST", e.message) + } catch (e: CancellationException) { assertTrue(e.suppressed.isEmpty()) + assertTrue(e.cause?.suppressed?.isEmpty() ?: false) finish(3) } } @Test - fun testAsyncCancellationWithCauseAndParent() = runTest { + fun testAsyncCancellationWithCauseAndParentDoesNotTriggerHandling() = runTest { val parent = Job() - val deferred = async(parent) { + val job = launch(parent) { expect(2) delay(Long.MAX_VALUE) } @@ -66,13 +67,8 @@ class JobExceptionHandlingTest : TestBase() { expect(1) yield() parent.completeExceptionally(IOException()) - try { - deferred.await() - expectUnreached() - } catch (e: IOException) { - assertTrue(e.suppressed.isEmpty()) - finish(3) - } + job.join() + finish(3) } @Test @@ -85,7 +81,7 @@ class JobExceptionHandlingTest : TestBase() { * * Github issue #354 */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() val child = launch(job, start = ATOMIC) { expect(2) @@ -109,7 +105,7 @@ class JobExceptionHandlingTest : TestBase() { * Inner child: throws AE * Result: AE in exception handler */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job) { expect(2) // <- child is launched successfully @@ -145,7 +141,7 @@ class JobExceptionHandlingTest : TestBase() { * Inner child: throws AE * Result: AE */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job, start = ATOMIC) { expect(2) @@ -173,7 +169,7 @@ class JobExceptionHandlingTest : TestBase() { * Inner child: throws AE * Result: IOE with suppressed AE */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job) { expect(2) // <- child is launched successfully @@ -196,11 +192,9 @@ class JobExceptionHandlingTest : TestBase() { finish(5) } - assertTrue(exception is IOException) + assertTrue(exception is ArithmeticException) assertNull(exception.cause) - val suppressed = exception.suppressed - assertEquals(1, suppressed.size) - checkException(suppressed[0]) + assertTrue(exception.suppressed.isEmpty()) } @Test @@ -211,7 +205,7 @@ class JobExceptionHandlingTest : TestBase() { * Child: launch 3 children, each of them throws an exception (AE, IOE, IAE) and calls delay() * Result: AE with suppressed IOE and IAE */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job, start = ATOMIC) { expect(2) @@ -253,7 +247,7 @@ class JobExceptionHandlingTest : TestBase() { * Child: launch 2 children (each of them throws an exception (IOE, IAE)), throws AE * Result: AE with suppressed IOE and IAE */ - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job, start = ATOMIC) { expect(2) @@ -282,6 +276,76 @@ class JobExceptionHandlingTest : TestBase() { assertTrue(suppressed[1] is IllegalArgumentException) } + @Test + fun testExceptionIsHandledOnce() = runTest(unhandled = listOf { e -> e is TestException }) { + val job = Job() + val j1 = launch(job) { + expect(1) + delay(Long.MAX_VALUE) + } + + val j2 = launch(job) { + expect(2) + throw TestException() + } + + joinAll(j1 ,j2) + finish(3) + } + + @Test + fun testCancelledParent() = runTest { + expect(1) + val parent = Job() + parent.completeExceptionally(TestException()) + launch(parent) { + expectUnreached() + }.join() + finish(2) + } + + @Test + fun testExceptionIsNotReported() = runTest { + try { + expect(1) + coroutineScope { + val job = Job(coroutineContext[Job]) + launch(job) { + throw TestException() + } + } + expectUnreached() + } catch (e: TestException) { + finish(2) + } + } + + @Test + fun testExceptionIsNotReportedTripleChain() = runTest { + try { + expect(1) + coroutineScope { + val job = Job(Job(Job(coroutineContext[Job]))) + launch(job) { + throw TestException() + } + } + expectUnreached() + } catch (e: TestException) { + finish(2) + } + } + + @Test + fun testAttachToCancelledJob() = runTest(unhandled = listOf({ e -> e is TestException })) { + val parent = launch(Job()) { + throw TestException() + }.apply { join() } + + launch(parent) { expectUnreached() } + launch(Job(parent)) { expectUnreached() } + } + @Test fun testBadException() = runTest(unhandled = listOf({e -> e is BadException})) { val job = launch(Job()) { @@ -291,7 +355,7 @@ class JobExceptionHandlingTest : TestBase() { throw BadException() } - launch(start = CoroutineStart.ATOMIC) { + launch(start = ATOMIC) { expect(4) throw BadException() } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionsStressTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionsStressTest.kt index b85a6bfe3f..4c977e85c5 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionsStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionsStressTest.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.exceptions import kotlinx.coroutines.* import org.junit.* import org.junit.Test -import java.io.* import java.util.concurrent.* import kotlin.test.* @@ -28,7 +27,7 @@ class JobExceptionsStressTest : TestBase() { * Result: one of the exceptions with the rest two as suppressed */ repeat(1000 * stressTestMultiplier) { - val exception = runBlock(executor) { + val exception = captureExceptionsRun(executor) { val barrier = CyclicBarrier(4) val job = launch(NonCancellable) { launch(start = CoroutineStart.ATOMIC) { diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/JobNestedExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/JobNestedExceptionsTest.kt index a2fa59e760..4a5fa74639 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/JobNestedExceptionsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/JobNestedExceptionsTest.kt @@ -13,7 +13,7 @@ class JobNestedExceptionsTest : TestBase() { @Test fun testExceptionUnwrapping() { - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job) { expect(2) @@ -37,7 +37,7 @@ class JobNestedExceptionsTest : TestBase() { @Test fun testExceptionUnwrappingWithSuspensions() { - val exception = runBlock { + val exception = captureExceptionsRun { val job = Job() launch(job) { expect(2) @@ -66,7 +66,7 @@ class JobNestedExceptionsTest : TestBase() { @Test fun testNestedAtomicThrow() { - val exception = runBlock { + val exception = captureExceptionsRun { expect(1) val job = launch(NonCancellable + CoroutineName("outer"), start = CoroutineStart.ATOMIC) { expect(2) @@ -86,7 +86,7 @@ class JobNestedExceptionsTest : TestBase() { @Test fun testChildThrowsDuringCompletion() { - val exceptions = runBlockForMultipleExceptions { + val exceptions = captureMultipleExceptionsRun { expect(1) val job = launch(NonCancellable + CoroutineName("outer"), start = CoroutineStart.ATOMIC) { expect(2) diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt index 83bd72355f..33585f443d 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt @@ -160,7 +160,9 @@ class ProduceExceptionsTest : TestBase() { yield() try { channel.receive() - } catch (e: TestException2) { + } catch (e: CancellationException) { + // RECOVER_STACK_TRACES + assertTrue(e.cause?.cause is TestException2) finish(4) } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt index 124e5569ab..a95a3da379 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt @@ -11,7 +11,6 @@ import org.junit.runners.* import kotlin.coroutines.* import kotlin.test.* -@Suppress("DEPRECATION") // cancel(cause) @RunWith(Parameterized::class) class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { enum class Mode { WITH_CONTEXT, ASYNC_AWAIT }