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 b10a802492..ac23695906 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -89,7 +89,7 @@ public abstract interface class kotlinx/coroutines/ChildJob : kotlinx/coroutines } public final class kotlinx/coroutines/ChildJob$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/ChildJob;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/ChildJob;)V public static fun fold (Lkotlinx/coroutines/ChildJob;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/ChildJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static fun minusKey (Lkotlinx/coroutines/ChildJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -103,7 +103,7 @@ public abstract interface class kotlinx/coroutines/CompletableDeferred : kotlinx } public final class kotlinx/coroutines/CompletableDeferred$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/CompletableDeferred;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/CompletableDeferred;)V public static fun fold (Lkotlinx/coroutines/CompletableDeferred;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/CompletableDeferred;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static fun minusKey (Lkotlinx/coroutines/CompletableDeferred;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -123,7 +123,7 @@ public abstract interface class kotlinx/coroutines/CompletableJob : kotlinx/coro } public final class kotlinx/coroutines/CompletableJob$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/CompletableJob;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/CompletableJob;)V public static fun fold (Lkotlinx/coroutines/CompletableJob;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/CompletableJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static fun minusKey (Lkotlinx/coroutines/CompletableJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -173,9 +173,7 @@ public final class kotlinx/coroutines/CoroutineExceptionHandler$Key : kotlin/cor public final class kotlinx/coroutines/CoroutineExceptionHandlerKt { public static final fun CoroutineExceptionHandler (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/CoroutineExceptionHandler; - public static final fun handleCoroutineException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;Lkotlinx/coroutines/Job;)V - public static synthetic fun handleCoroutineException$default (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;Lkotlinx/coroutines/Job;ILjava/lang/Object;)V - public static final fun handleExceptionViaHandler (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V + public static final fun handleCoroutineException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V } public final class kotlinx/coroutines/CoroutineName : kotlin/coroutines/AbstractCoroutineContextElement { @@ -200,7 +198,8 @@ public abstract interface class kotlinx/coroutines/CoroutineScope { public final class kotlinx/coroutines/CoroutineScopeKt { public static final fun CoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; public static final fun MainScope ()Lkotlinx/coroutines/CoroutineScope; - public static final fun cancel (Lkotlinx/coroutines/CoroutineScope;)V + public static final fun cancel (Lkotlinx/coroutines/CoroutineScope;Ljava/util/concurrent/CancellationException;)V + public static synthetic fun cancel$default (Lkotlinx/coroutines/CoroutineScope;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public static final fun coroutineScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun isActive (Lkotlinx/coroutines/CoroutineScope;)Z public static final fun plus (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; @@ -233,7 +232,7 @@ public abstract interface class kotlinx/coroutines/Deferred : kotlinx/coroutines } public final class kotlinx/coroutines/Deferred$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/Deferred;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/Deferred;)V public static fun fold (Lkotlinx/coroutines/Deferred;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static fun minusKey (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -276,6 +275,10 @@ public final class kotlinx/coroutines/EventLoopKt { public static final fun processNextEventInCurrentThread ()J } +public final class kotlinx/coroutines/ExceptionsKt { + public static final fun CancellationException (Ljava/lang/String;Ljava/lang/Throwable;)Ljava/util/concurrent/CancellationException; +} + public abstract class kotlinx/coroutines/ExecutorCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, java/io/Closeable { public fun ()V public abstract fun close ()V @@ -301,9 +304,9 @@ public abstract interface annotation class kotlinx/coroutines/InternalCoroutines public abstract interface class kotlinx/coroutines/Job : kotlin/coroutines/CoroutineContext$Element { public static final field Key Lkotlinx/coroutines/Job$Key; public abstract fun attachChild (Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; - public abstract fun cancel ()V - public abstract synthetic fun cancel ()Z - public abstract fun cancel (Ljava/lang/Throwable;)Z + public abstract synthetic fun cancel ()V + public abstract synthetic fun cancel (Ljava/lang/Throwable;)Z + public abstract fun cancel (Ljava/util/concurrent/CancellationException;)V public abstract fun getCancellationException ()Ljava/util/concurrent/CancellationException; public abstract fun getChildren ()Lkotlin/sequences/Sequence; public abstract fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; @@ -318,8 +321,9 @@ public abstract interface class kotlinx/coroutines/Job : kotlin/coroutines/Corou } public final class kotlinx/coroutines/Job$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/Job;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/Job;)V public static synthetic fun cancel$default (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;ILjava/lang/Object;)Z + public static synthetic fun cancel$default (Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public static fun fold (Lkotlinx/coroutines/Job;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static synthetic fun invokeOnCompletion$default (Lkotlinx/coroutines/Job;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/DisposableHandle; @@ -337,17 +341,22 @@ public final class kotlinx/coroutines/JobKt { public static final synthetic fun Job (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job; public static synthetic fun Job$default (Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableJob; public static synthetic fun Job$default (Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/Job; - public static final fun cancel (Lkotlin/coroutines/CoroutineContext;)V - public static final synthetic fun cancel (Lkotlin/coroutines/CoroutineContext;)Z - public static final fun cancel (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)Z + public static final synthetic fun cancel (Lkotlin/coroutines/CoroutineContext;)V + public static final synthetic fun cancel (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)Z + public static final fun cancel (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/CancellationException;)V public static synthetic fun cancel$default (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;ILjava/lang/Object;)Z + public static synthetic fun cancel$default (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public static final fun cancelAndJoin (Lkotlinx/coroutines/Job;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun cancelChildren (Lkotlin/coroutines/CoroutineContext;)V - public static final fun cancelChildren (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V - public static final fun cancelChildren (Lkotlinx/coroutines/Job;)V - public static final fun cancelChildren (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;)V + public static final synthetic fun cancelChildren (Lkotlin/coroutines/CoroutineContext;)V + public static final synthetic fun cancelChildren (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V + public static final fun cancelChildren (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/CancellationException;)V + public static final synthetic fun cancelChildren (Lkotlinx/coroutines/Job;)V + public static final synthetic fun cancelChildren (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;)V + public static final fun cancelChildren (Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;)V public static synthetic fun cancelChildren$default (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static synthetic fun cancelChildren$default (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public static final fun cancelFutureOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Ljava/util/concurrent/Future;)V public static final fun cancelFutureOnCompletion (Lkotlinx/coroutines/Job;Ljava/util/concurrent/Future;)Lkotlinx/coroutines/DisposableHandle; public static final fun isActive (Lkotlin/coroutines/CoroutineContext;)Z @@ -356,9 +365,11 @@ public final class kotlinx/coroutines/JobKt { public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlinx/coroutines/Job, kotlinx/coroutines/ParentJob, kotlinx/coroutines/selects/SelectClause0 { public fun (Z)V public final fun attachChild (Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; - public fun cancel ()V - public synthetic fun cancel ()Z - public fun cancel (Ljava/lang/Throwable;)Z + public synthetic fun cancel ()V + public synthetic fun cancel (Ljava/lang/Throwable;)Z + public fun cancel (Ljava/util/concurrent/CancellationException;)V + public final fun cancelCoroutine (Ljava/lang/Throwable;)Z + public fun cancelInternal (Ljava/lang/Throwable;)Z public fun childCancelled (Ljava/lang/Throwable;)Z public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; @@ -371,7 +382,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin 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;)V + protected fun handleJobException (Ljava/lang/Throwable;Z)V public final fun invokeOnCompletion (Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public final fun invokeOnCompletion (ZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public fun isActive ()Z @@ -386,6 +397,8 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public fun plus (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job; public final fun registerSelectClause0 (Lkotlinx/coroutines/selects/SelectInstance;Lkotlin/jvm/functions/Function1;)V public final fun start ()Z + protected final fun toCancellationException (Ljava/lang/Throwable;Ljava/lang/String;)Ljava/util/concurrent/CancellationException; + public static synthetic fun toCancellationException$default (Lkotlinx/coroutines/JobSupport;Ljava/lang/Throwable;Ljava/lang/String;ILjava/lang/Object;)Ljava/util/concurrent/CancellationException; public final fun toDebugString ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } @@ -398,9 +411,9 @@ public abstract class kotlinx/coroutines/MainCoroutineDispatcher : kotlinx/corou public final class kotlinx/coroutines/NonCancellable : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/Job { public static final field INSTANCE Lkotlinx/coroutines/NonCancellable; public fun attachChild (Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; - public fun cancel ()V - public synthetic fun cancel ()Z - public fun cancel (Ljava/lang/Throwable;)Z + public synthetic fun cancel ()V + public synthetic fun cancel (Ljava/lang/Throwable;)Z + public fun cancel (Ljava/util/concurrent/CancellationException;)V public fun getCancellationException ()Ljava/util/concurrent/CancellationException; public fun getChildren ()Lkotlin/sequences/Sequence; public fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; @@ -429,7 +442,7 @@ public abstract interface class kotlinx/coroutines/ParentJob : kotlinx/coroutine } public final class kotlinx/coroutines/ParentJob$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/ParentJob;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/ParentJob;)V public static fun fold (Lkotlinx/coroutines/ParentJob;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun get (Lkotlinx/coroutines/ParentJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public static fun minusKey (Lkotlinx/coroutines/ParentJob;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -493,16 +506,18 @@ public abstract interface class kotlinx/coroutines/channels/ActorScope : kotlinx } public final class kotlinx/coroutines/channels/ActorScope$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/channels/ActorScope;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/channels/ActorScope;)V } public abstract interface class kotlinx/coroutines/channels/BroadcastChannel : kotlinx/coroutines/channels/SendChannel { - public abstract fun cancel (Ljava/lang/Throwable;)Z + public abstract synthetic fun cancel (Ljava/lang/Throwable;)Z + public abstract fun cancel (Ljava/util/concurrent/CancellationException;)V public abstract fun openSubscription ()Lkotlinx/coroutines/channels/ReceiveChannel; } public final class kotlinx/coroutines/channels/BroadcastChannel$DefaultImpls { public static synthetic fun cancel$default (Lkotlinx/coroutines/channels/BroadcastChannel;Ljava/lang/Throwable;ILjava/lang/Object;)Z + public static synthetic fun cancel$default (Lkotlinx/coroutines/channels/BroadcastChannel;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V } public final class kotlinx/coroutines/channels/BroadcastChannelKt { @@ -524,7 +539,7 @@ public abstract interface class kotlinx/coroutines/channels/Channel : kotlinx/co } public final class kotlinx/coroutines/channels/Channel$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/channels/Channel;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/channels/Channel;)V } public final class kotlinx/coroutines/channels/Channel$Factory { @@ -553,6 +568,7 @@ public final class kotlinx/coroutines/channels/ChannelsKt { public static final fun associateByTo (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun associateByTo (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun associateTo (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun cancelConsumed (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/lang/Throwable;)V public static final fun consume (Lkotlinx/coroutines/channels/BroadcastChannel;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static final fun consume (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static final fun consumeEach (Lkotlinx/coroutines/channels/BroadcastChannel;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -672,7 +688,8 @@ public final class kotlinx/coroutines/channels/ClosedSendChannelException : java public final class kotlinx/coroutines/channels/ConflatedBroadcastChannel : kotlinx/coroutines/channels/BroadcastChannel { public fun ()V public fun (Ljava/lang/Object;)V - public fun cancel (Ljava/lang/Throwable;)Z + public synthetic fun cancel (Ljava/lang/Throwable;)Z + public fun cancel (Ljava/util/concurrent/CancellationException;)V public fun close (Ljava/lang/Throwable;)Z public fun getOnSend ()Lkotlinx/coroutines/selects/SelectClause2; public final fun getValue ()Ljava/lang/Object; @@ -697,9 +714,9 @@ public abstract interface class kotlinx/coroutines/channels/ProducerScope : kotl } public abstract interface class kotlinx/coroutines/channels/ReceiveChannel { - public abstract fun cancel ()V - public abstract synthetic fun cancel ()Z - public abstract fun cancel (Ljava/lang/Throwable;)Z + public abstract synthetic fun cancel ()V + public abstract synthetic fun cancel (Ljava/lang/Throwable;)Z + public abstract fun cancel (Ljava/util/concurrent/CancellationException;)V public abstract fun getOnReceive ()Lkotlinx/coroutines/selects/SelectClause1; public abstract fun getOnReceiveOrNull ()Lkotlinx/coroutines/selects/SelectClause1; public abstract fun isClosedForReceive ()Z @@ -711,8 +728,9 @@ public abstract interface class kotlinx/coroutines/channels/ReceiveChannel { } public final class kotlinx/coroutines/channels/ReceiveChannel$DefaultImpls { - public static synthetic fun cancel (Lkotlinx/coroutines/channels/ReceiveChannel;)Z + public static synthetic fun cancel (Lkotlinx/coroutines/channels/ReceiveChannel;)V public static synthetic fun cancel$default (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/lang/Throwable;ILjava/lang/Object;)Z + public static synthetic fun cancel$default (Lkotlinx/coroutines/channels/ReceiveChannel;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V } public abstract interface class kotlinx/coroutines/channels/SendChannel { diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index 22fd5fb8bf..cb190aaf53 100644 --- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt +++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt @@ -47,9 +47,8 @@ public fun CoroutineScope.future( private class ListenableFutureCoroutine( context: CoroutineContext, - private val completion: SettableFuture + private val future: SettableFuture ) : AbstractCoroutine(context), FutureCallback { - /* * We register coroutine as callback to the future this coroutine completes. * But when future is cancelled externally, we'd like to cancel coroutine, @@ -66,12 +65,13 @@ private class ListenableFutureCoroutine( } override fun onCompleted(value: T) { - completion.set(value) + future.set(value) } - override fun onCompletedExceptionally(exception: Throwable) { - if (!completion.setException(exception)) { - handleCoroutineException(parentContext, exception, this) + override fun handleJobException(exception: Throwable, handled: Boolean) { + if (!future.setException(exception) && !handled) { + // prevents loss of exception that was not handled by parent & could not be set to SettableFuture + handleCoroutineException(context, exception) } } } diff --git a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt index 0f1f27a976..bfb5cfd452 100644 --- a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt +++ b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt @@ -46,7 +46,7 @@ class ListenableFutureTest : TestBase() { } @Test - fun testAwaitWithContextCancellation() = runTest(expected = {it is IOException}) { + fun testAwaitWithCancellation() = runTest(expected = {it is TestCancellationException}) { val future = SettableFuture.create() val deferred = async { withContext(Dispatchers.Default) { @@ -54,8 +54,9 @@ class ListenableFutureTest : TestBase() { } } - deferred.cancel(IOException()) - deferred.await() + deferred.cancel(TestCancellationException()) + deferred.await() // throws TCE + expectUnreached() } @Test @@ -258,13 +259,24 @@ class ListenableFutureTest : TestBase() { } @Test - fun testChildException() = runTest { + fun testStructuredException() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { + val result = future(Dispatchers.Unconfined) { + throw TestException("FAIL") + } + result.checkFutureException() + } + + @Test + fun testChildException() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { val result = future(Dispatchers.Unconfined) { // child crashes launch { throw TestException("FAIL") } 42 } - result.checkFutureException() } @@ -295,7 +307,26 @@ class ListenableFutureTest : TestBase() { throw TestException() } } + result.cancel(true) + finish(3) + } + @Test + fun testUnhandledExceptionOnExternalCancellation() = runTest( + unhandled = listOf( + { it -> it is TestException } // exception is unhandled because there is no parent + ) + ) { + expect(1) + // No parent here (NonCancellable), so nowhere to propagate exception + val result = future(NonCancellable + Dispatchers.Unconfined) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(2) + throw TestException() // this exception cannot be handled + } + } result.cancel(true) finish(3) } diff --git a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt index 6c59a08c88..e890b025ee 100644 --- a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt +++ b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.future import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException import java.util.concurrent.* import java.util.function.* import kotlin.coroutines.* @@ -46,20 +47,20 @@ public fun CoroutineScope.future( private class CompletableFutureCoroutine( context: CoroutineContext, - private val completion: CompletableFuture + private val future: CompletableFuture ) : AbstractCoroutine(context), BiConsumer { - override fun accept(value: T?, exception: Throwable?) { cancel() } override fun onCompleted(value: T) { - completion.complete(value) + future.complete(value) } - override fun onCompletedExceptionally(exception: Throwable) { - if (!completion.completeExceptionally(exception)) { - handleCoroutineException(parentContext, exception, this) + override fun handleJobException(exception: Throwable, handled: Boolean) { + if (!future.completeExceptionally(exception) && !handled) { + // prevents loss of exception that was not handled by parent & could not be set to CompletableFuture + handleCoroutineException(context, exception) } } } @@ -70,7 +71,11 @@ private class CompletableFutureCoroutine( */ public fun Deferred.asCompletableFuture(): CompletableFuture { val future = CompletableFuture() - future.whenComplete { _, exception -> cancel(exception) } + future.whenComplete { _, exception -> + cancel(exception?.let { + it as? CancellationException ?: CancellationException("CompletableFuture was completed exceptionally", it) + }) + } invokeOnCompletion { try { future.complete(getCompleted()) diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt b/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt index 2d19f382f2..a331529a05 100644 --- a/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt +++ b/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt @@ -26,7 +26,7 @@ fun main(args: Array) { log("g should not execute this line") } log("Started futures f && g... will not wait -- cancel them!!!") - job.cancel(CancellationException("I don't want it")) + job.cancel() check(f.isCancelled) check(g.isCancelled) log("f result = ${Try { f.get() }}") diff --git a/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt b/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt index ef02aa229a..7038363cb5 100644 --- a/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt +++ b/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt @@ -368,29 +368,39 @@ class FutureTest : TestBase() { } @Test - fun testChildException() = runTest { + fun testStructuredException() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { + val result = future(Dispatchers.Unconfined) { + throw TestException("FAIL") + } + result.checkFutureException() + } + + @Test + fun testChildException() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { val result = future(Dispatchers.Unconfined) { // child crashes launch { throw TestException("FAIL") } 42 } - result.checkFutureException() } @Test - fun testExceptionAggregation() = runTest { + fun testExceptionAggregation() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { val result = future(Dispatchers.Unconfined) { // child crashes launch(start = CoroutineStart.ATOMIC) { throw TestException1("FAIL") } launch(start = CoroutineStart.ATOMIC) { throw TestException2("FAIL") } throw TestException() } - - expect(1) result.checkFutureException(TestException1::class, TestException2::class) - yield() - finish(2) // we are not cancelled + finish(1) } @Test @@ -409,7 +419,9 @@ class FutureTest : TestBase() { } @Test - fun testExceptionOnExternalCompletion() = runTest(expected = {it is TestException}) { + fun testExceptionOnExternalCompletion() = runTest( + expected = { it is TestException } // exception propagates to parent with structured concurrency + ) { expect(1) val result = future(Dispatchers.Unconfined) { try { @@ -419,7 +431,26 @@ class FutureTest : TestBase() { throw TestException() } } + result.complete(Unit) + finish(3) + } + @Test + fun testUnhandledExceptionOnExternalCompletion() = runTest( + unhandled = listOf( + { it -> it is TestException } // exception is unhandled because there is no parent + ) + ) { + expect(1) + // No parent here (NonCancellable), so nowhere to propagate exception + val result = future(NonCancellable + Dispatchers.Unconfined) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(2) + throw TestException() // this exception cannot be handled + } + } result.complete(Unit) finish(3) } diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index 9454bdb797..3bdf003e3c 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt @@ -117,7 +117,7 @@ public abstract class AbstractCoroutine( } internal final override fun handleOnCompletionException(exception: Throwable) { - handleCoroutineException(parentContext, exception, this) + handleCoroutineException(context, exception) } internal override fun nameString(): String { diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index e777a3d572..60b44697aa 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -94,7 +94,6 @@ private open class DeferredCoroutine( parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine(parentContext, active), Deferred, SelectClause1 { - override val cancelsParent: Boolean get() = true override fun getCompleted(): T = getCompletedInternal() as T override suspend fun await(): T = awaitInternal() as T override val onAwait: SelectClause1 get() = this @@ -169,8 +168,9 @@ private open class StandaloneCoroutine( parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine(parentContext, active) { - override val cancelsParent: Boolean get() = true - override fun handleJobException(exception: Throwable) = handleExceptionViaHandler(parentContext, exception) + override fun handleJobException(exception: Throwable, handled: Boolean) { + if (!handled) handleCoroutineException(context, exception) + } } private class LazyStandaloneCoroutine( diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 2fe1cf9e5a..813276f8ed 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -121,6 +121,7 @@ internal open class CancellableContinuationImpl( try { block() } catch (ex: Throwable) { + // Handler should never fail, if it does -- it is an unhandled exception handleCoroutineException( context, CompletionHandlerException("Exception in cancellation handler for $this", ex) diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index b280b319be..d8f9d81607 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -66,7 +66,6 @@ private class CompletableDeferredImpl( parent: Job? ) : JobSupport(true), CompletableDeferred, SelectClause1 { init { initParentJobInternal(parent) } - override val cancelsParent: Boolean get() = true override val onCancelComplete get() = true override fun getCompleted(): T = getCompletedInternal() as T override suspend fun await(): T = awaitInternal() as T diff --git a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt index 927d16867b..bbf3e8eb0d 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt @@ -9,34 +9,16 @@ import kotlin.coroutines.* internal expect fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) /** - * Helper function for coroutine builder implementations to handle uncaught exception in coroutines. + * Helper function for coroutine builder implementations to handle uncaught and unexpected exceptions in coroutines, + * that could not be otherwise handled in a normal way through structured concurrency, saving them to a future, and + * cannot be rethrown. This is a last resort handler to prevent lost exceptions. * - * It tries to handle uncaught exception in the following way: - * If current exception is [CancellationException], it's ignored: [CancellationException] is a normal way to cancel - * coroutine. - * - * If there is a [Job] in the context and it's not a [caller], then [Job.cancel] is invoked. - * If invocation returned `true`, method terminates: now [Job] is responsible for handling an exception. - * Otherwise, If there is [CoroutineExceptionHandler] in the context, it is used. If it throws an exception during handling - * or is absent, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] and [Thread.uncaughtExceptionHandler] are invoked + * If there is [CoroutineExceptionHandler] in the context, then it is used. If it throws an exception during handling + * or is absent, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] and + * [Thread.uncaughtExceptionHandler] are invoked. */ @InternalCoroutinesApi -public fun handleCoroutineException(context: CoroutineContext, exception: Throwable, caller: Job? = null) { - // Ignore CancellationException (they are normal ways to terminate a coroutine) - if (exception is CancellationException) return // nothing to do - // Try propagate exception to parent - val job = context[Job] - @Suppress("DEPRECATION") - if (job !== null && job !== caller && job.cancel(exception)) return // handle by parent - // otherwise -- use exception handlers - handleExceptionViaHandler(context, exception) -} - -/** - * @suppress This is an internal API and it is subject to change. - */ -@InternalCoroutinesApi -public fun handleExceptionViaHandler(context: CoroutineContext, exception: Throwable) { +public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) { // Invoke exception handler from the context if present try { context[CoroutineExceptionHandler]?.let { @@ -47,7 +29,6 @@ public fun handleExceptionViaHandler(context: CoroutineContext, exception: Throw handleCoroutineExceptionImpl(context, handlerException(exception, t)) return } - // If handler is not present in the context or exception was thrown, fallback to the global handler handleCoroutineExceptionImpl(context, exception) } diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 74a46cfb38..2c78d08b95 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -196,10 +196,12 @@ public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job()) /** - * Cancels this scope, including its job and all its children. + * Cancels this scope, including its job and all its children with an optional cancellation [cause]. + * A cause can be used to specify an error message or to provide other details on + * a cancellation reason for debugging purposes. * Throws [IllegalStateException] if the scope does not have a job in it. - **/ -public fun CoroutineScope.cancel() { + */ +public fun CoroutineScope.cancel(cause: CancellationException? = null) { val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this") - job.cancel() + job.cancel(cause) } diff --git a/kotlinx-coroutines-core/common/src/Exceptions.common.kt b/kotlinx-coroutines-core/common/src/Exceptions.common.kt index 3e9457287e..e02089327b 100644 --- a/kotlinx-coroutines-core/common/src/Exceptions.common.kt +++ b/kotlinx-coroutines-core/common/src/Exceptions.common.kt @@ -12,6 +12,9 @@ public expect class CompletionHandlerException(message: String, cause: Throwable public expect open class CancellationException(message: String?) : IllegalStateException +@Suppress("FunctionName") +public expect fun CancellationException(message: String?, cause: Throwable?) : CancellationException + internal expect class JobCancellationException( message: String, cause: Throwable?, @@ -22,4 +25,7 @@ internal expect class JobCancellationException( internal expect class DispatchException(message: String, cause: Throwable) : RuntimeException -internal expect fun Throwable.addSuppressedThrowable(other: Throwable) \ No newline at end of file +internal expect fun Throwable.addSuppressedThrowable(other: Throwable) + +// For use in tests +internal expect val RECOVER_STACK_TRACES: Boolean \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index a428de8595..f33e1715a5 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -154,27 +154,25 @@ public interface Job : CoroutineContext.Element { */ public fun start(): Boolean + /** - * @suppress + * Cancels this job with an optional cancellation [cause]. + * A cause can be used to specify an error message or to provide other details on + * a cancellation reason for debugging purposes. + * See [Job] documentation for full explanation of cancellation machinery. */ - @Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION") - @Deprecated(level = DeprecationLevel.HIDDEN, message = "Left here for binary compatibility") - @JvmName("cancel") - public fun cancel0(): Boolean = cancel(null) + public fun cancel(cause: CancellationException? = null) /** - * Cancels this job. - * See [Job] documentation for full explanation of cancellation machinery. + * @suppress This method implements old version of JVM ABI. Use [cancel]. */ - public fun cancel(): Unit + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + public fun cancel() = cancel(null) /** - * @suppress + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. */ - @ObsoleteCoroutinesApi - @Deprecated(level = DeprecationLevel.WARNING, message = "Use CompletableDeferred.completeExceptionally(cause) or Job.cancel() instead", - replaceWith = ReplaceWith("cancel()") - ) + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun cancel(cause: Throwable? = null): Boolean // ------------ parent-child ------------ @@ -360,7 +358,7 @@ public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent) /** @suppress Binary compatibility only */ @Suppress("FunctionName") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") @JvmName("Job") public fun Job0(parent: Job? = null): Job = Job(parent) @@ -479,21 +477,26 @@ public suspend fun Job.cancelAndJoin() { } /** - * @suppress + * Cancels all [children][Job.children] jobs of this coroutine using [Job.cancel] for all of them + * with an optional cancellation [cause]. + * Unlike [Job.cancel] on this job as a whole, the state of this job itself is not affected. */ -@ObsoleteCoroutinesApi -@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancelChildren() without cause", replaceWith = ReplaceWith("cancelChildren()")) -public fun Job.cancelChildren(cause: Throwable? = null) { - @Suppress("DEPRECATION") +public fun Job.cancelChildren(cause: CancellationException? = null) { children.forEach { it.cancel(cause) } } /** - * Cancels all [children][Job.children] jobs of this coroutine using [Job.cancel] for all of them. - * Unlike [Job.cancel] on this job as a whole, the state of this job itself is not affected. + * @suppress This method implements old version of JVM ABI. Use [cancel]. + */ +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") +public fun Job.cancelChildren() = cancelChildren(null) + +/** + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [Job.cancelChildren]. */ -public fun Job.cancelChildren() { - children.forEach { it.cancel() } +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") +public fun Job.cancelChildren(cause: Throwable? = null) { + children.forEach { (it as? JobSupport)?.cancelInternal(cause) } } // -------------------- CoroutineContext extensions -------------------- @@ -517,47 +520,49 @@ public fun Job.cancelChildren() { public val CoroutineContext.isActive: Boolean get() = this[Job]?.isActive == true - /** - * @suppress + * Cancels [Job] of this context with an optional cancellation cause. + * See [Job.cancel] for details. */ -@Suppress("unused") -@JvmName("cancel") -@Deprecated(message = "Binary compatibility", level = DeprecationLevel.HIDDEN) -public fun CoroutineContext.cancel0(): Boolean { - this[Job]?.cancel() - return true +public fun CoroutineContext.cancel(cause: CancellationException? = null) { + this[Job]?.cancel(cause) } /** - * Cancels [Job] of this context. See [Job.cancel] for details. + * @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancel]. */ -public fun CoroutineContext.cancel(): Unit { - this[Job]?.cancel() -} +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") +public fun CoroutineContext.cancel() = cancel(null) /** - * @suppress + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancel]. */ -@ObsoleteCoroutinesApi -@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancel() without cause", replaceWith = ReplaceWith("cancel()")) +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun CoroutineContext.cancel(cause: Throwable? = null): Boolean = @Suppress("DEPRECATION") - this[Job]?.cancel(cause) ?: false + (this[Job] as? JobSupport)?.cancelInternal(cause) ?: false /** - * Cancels all children of the [Job] in this context, without touching the the state of this job itself. + * Cancels all children of the [Job] in this context, without touching the the state of this job itself + * with an optional cancellation cause. See [Job.cancel]. * It does not do anything if there is no job in the context or it has no children. */ -public fun CoroutineContext.cancelChildren() { - this[Job]?.children?.forEach { it.cancel() } +public fun CoroutineContext.cancelChildren(cause: CancellationException? = null) { + this[Job]?.children?.forEach { it.cancel(cause) } } -@ObsoleteCoroutinesApi -@Deprecated(level = DeprecationLevel.WARNING, message = "Use cancelChildren() without cause", replaceWith = ReplaceWith("cancelChildren()")) +/** + * @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancelChildren]. + */ +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") +public fun CoroutineContext.cancelChildren() = cancelChildren(null) + +/** + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancelChildren]. + */ +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun CoroutineContext.cancelChildren(cause: Throwable? = null) { - @Suppress("DEPRECATION") - this[Job]?.children?.forEach { it.cancel(cause) } + this[Job]?.children?.forEach { (it as? JobSupport)?.cancelInternal(cause) } } /** diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index e79bd4b6f7..43034ae2a8 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -220,9 +220,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // cancelled job final state else -> CompletedExceptionally(finalException) } - // Now handle exception if parent can't handle it - if (finalException != null && !cancelParent(finalException)) { - handleJobException(finalException) + // Now handle the final exception + if (finalException != null) { + val handledByParent = cancelParent(finalException) + handleJobException(finalException, handledByParent) } // Then CAS to completed state -> it must succeed require(_state.compareAndSet(state, finalState.boxIncomplete())) { "Unexpected state: ${_state.value}, expected: $state, update: $finalState" } @@ -369,16 +370,17 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren public final override fun getCancellationException(): CancellationException { val state = this.state return when (state) { - is Finishing -> state.rootCause?.toCancellationException("Job is cancelling") + is Finishing -> state.rootCause?.toCancellationException("$classSimpleName is cancelling") ?: error("Job is still new or active: $this") is Incomplete -> error("Job is still new or active: $this") - is CompletedExceptionally -> state.cause.toCancellationException("Job was cancelled") - else -> JobCancellationException("Job has completed normally", null, this) + is CompletedExceptionally -> state.cause.toCancellationException() + else -> JobCancellationException("$classSimpleName has completed normally", null, this) } } - private fun Throwable.toCancellationException(message: String): CancellationException = - this as? CancellationException ?: JobCancellationException(message, this, this@JobSupport) + protected fun Throwable.toCancellationException(message: String? = null): CancellationException = + this as? CancellationException ?: + JobCancellationException(message ?: "$classSimpleName was cancelled", this, this@JobSupport) /** * Returns the cause that signals the completion of this job -- it returns the original @@ -564,14 +566,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren */ internal open val onCancelComplete: Boolean get() = false - // external cancel without cause, never invoked implicitly from internal machinery - public override fun cancel() { - @Suppress("DEPRECATION") - cancel(null) // must delegate here, because some classes override cancel(x) + // external cancel with cause, never invoked implicitly from internal machinery + public override fun cancel(cause: CancellationException?) { + cancelInternal(cause) // must delegate here, because some classes override cancelInternal(x) } + // HIDDEN in Job interface. Invoked only by legacy compiled code. // external cancel with (optional) cause, never invoked implicitly from internal machinery + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Added since 1.2.0 for binary compatibility with versions <= 1.1.x") public override fun cancel(cause: Throwable?): Boolean = + cancelInternal(cause) + + // It is overridden in channel-linked implementation + // Note: Boolean result is used only in HIDDEN DEPRECATED functions that were public in versions <= 1.1.x + public open fun cancelInternal(cause: Throwable?): Boolean = cancelImpl(cause) && handlesException // Parent is cancelling child @@ -580,9 +588,17 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } // Child was cancelled with cause + // It is overridden in supervisor implementations to ignore child cancellation public open fun childCancelled(cause: Throwable): Boolean = cancelImpl(cause) && handlesException + /** + * Makes this [Job] cancelled with a specified [cause]. + * It is used in [AbstractCoroutine]-derived classes when there is an internal failure. + */ + public fun cancelCoroutine(cause: Throwable?) = + cancelImpl(cause) + // cause is Throwable or ParentJob when cancelChild was invoked // returns true is exception was handled, false otherwise private fun cancelImpl(cause: Any?): Boolean { @@ -891,24 +907,29 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren * * @suppress **This is unstable API and it is subject to change.* */ - protected open val cancelsParent: Boolean get() = false + protected open val cancelsParent: Boolean get() = true /** * Returns `true` for jobs that handle their exceptions via [handleJobException] or integrate them * into the job's result via [onCompletionInternal]. The only instance of the [Job] that does not - * handle its exceptions is [JobImpl]. + * 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 /** + * Handles the final job [exception] after it was reported to the by the parent, + * where [handled] is `true` when parent had already handled exception and `false` otherwise. + * * This method is invoked **exactly once** when the final exception of the job is determined * and before it becomes complete. At the moment of invocation the job and all its children are complete. * + * Note, [handled] is always `true` when [exception] is [CancellationException]. + * * @suppress **This is unstable API and it is subject to change.* */ - protected open fun handleJobException(exception: Throwable) {} + protected open fun handleJobException(exception: Throwable, handled: Boolean) {} private fun cancelParent(cause: Throwable): Boolean { // CancellationException is considered "normal" and parent is not cancelled when child produces it. @@ -1184,7 +1205,6 @@ private class Empty(override val isActive: Boolean) : Incomplete { internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob { init { initParentJobInternal(parent) } - override val cancelsParent: Boolean get() = true override val onCancelComplete get() = true override val handlesException: Boolean get() = false override fun complete() = makeCompleting(Unit) diff --git a/kotlinx-coroutines-core/common/src/NonCancellable.kt b/kotlinx-coroutines-core/common/src/NonCancellable.kt index a0dcb3ba3c..3a4faeed8e 100644 --- a/kotlinx-coroutines-core/common/src/NonCancellable.kt +++ b/kotlinx-coroutines-core/common/src/NonCancellable.kt @@ -92,15 +92,13 @@ public object NonCancellable : AbstractCoroutineContextElement(Job), Job { * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi - @Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE") - override fun cancel(): Unit { - } + override fun cancel(cause: CancellationException?) {} /** * Always returns `false`. - * @suppress **This an internal API and should not be used from general code.** + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. */ - @InternalCoroutinesApi + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") override fun cancel(cause: Throwable?): Boolean = false // never handles exceptions /** diff --git a/kotlinx-coroutines-core/common/src/Supervisor.kt b/kotlinx-coroutines-core/common/src/Supervisor.kt index d7443b6129..dd88e232f2 100644 --- a/kotlinx-coroutines-core/common/src/Supervisor.kt +++ b/kotlinx-coroutines-core/common/src/Supervisor.kt @@ -33,7 +33,7 @@ public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobIm /** @suppress Binary compatibility only */ @Suppress("FunctionName") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") @JvmName("SupervisorJob") public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent) diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index d84ec10559..6ffb97c705 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -85,9 +85,13 @@ private open class TimeoutCoroutine( override val defaultResumeMode: Int get() = MODE_DIRECT override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame)?.callerFrame override fun getStackTraceElement(): StackTraceElement? = (uCont as? CoroutineStackFrame)?.getStackTraceElement() + + override val cancelsParent: Boolean + get() = false // it throws exception to parent instead of cancelling it + @Suppress("LeakingThis", "Deprecation") override fun run() { - cancel(TimeoutCancellationException(time, this)) + cancelCoroutine(TimeoutCancellationException(time, this)) } @Suppress("UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index d05ea29ecb..0835243e30 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -655,12 +655,16 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( return true } - public override fun cancel(cause: Throwable?): Boolean = + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + override fun cancel(cause: Throwable?): Boolean = + cancelInternal(cause) + + override fun cancel(cause: CancellationException?) { + cancelInternal(cause) + } + + private fun cancelInternal(cause: Throwable?): Boolean = close(cause).also { - @Suppress("DEPRECATION") - for (sub in subscribers) sub.cancel(cause) + for (sub in subscribers) sub.cancelInternal(cause) } // result is `OFFER_SUCCESS | OFFER_FAILED | Closed` @@ -201,7 +208,7 @@ internal class ArrayBroadcastChannel( override val isBufferAlwaysFull: Boolean get() = error("Should not be used") override val isBufferFull: Boolean get() = error("Should not be used") - override fun cancel(cause: Throwable?): Boolean = + override fun cancelInternal(cause: Throwable?): Boolean = close(cause).also { closed -> if (closed) broadcastChannel.updateHead(removeSub = this) clearBuffer() diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 4e76369065..d2f7fe6447 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -90,23 +90,29 @@ private open class BroadcastCoroutine( protected val _channel: BroadcastChannel, active: Boolean ) : AbstractCoroutine(parentContext, active), ProducerScope, BroadcastChannel by _channel { - override val cancelsParent: Boolean get() = true override val isActive: Boolean get() = super.isActive override val channel: SendChannel get() = this - override fun cancel(cause: Throwable?): Boolean { - val wasCancelled = _channel.cancel(cause) - @Suppress("DEPRECATION") - if (wasCancelled) super.cancel(cause) // cancel the job - return wasCancelled + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + final override fun cancel(cause: Throwable?): Boolean = + cancelInternal(cause) + + final override fun cancel(cause: CancellationException?) { + cancelInternal(cause) + } + + override fun cancelInternal(cause: Throwable?): Boolean { + _channel.cancel(cause?.toCancellationException()) // cancel the channel + cancelCoroutine(cause) // cancel the job + return true // does not matter - result is used in DEPRECATED functions only } override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) { val cause = (state as? CompletedExceptionally)?.cause val processed = _channel.close(cause) - if (cause != null && !processed && suppressed) handleExceptionViaHandler(context, cause) + if (cause != null && !processed && suppressed) handleCoroutineException(context, cause) } } diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt index 6c5d8abc8e..bdb06b74d3 100644 --- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt @@ -30,14 +30,19 @@ public interface BroadcastChannel : SendChannel { public fun openSubscription(): ReceiveChannel /** - * Cancels reception of remaining elements from this channel. This function closes the channel with + * Cancels reception of remaining elements from this channel with an optional cause. + * This function closes the channel with * the specified cause (unless it was already closed), removes all buffered sent elements from it, * and [cancels][ReceiveChannel.cancel] all open subscriptions. - * This function returns `true` if the channel was not closed previously, or `false` otherwise. - * - * A channel that was cancelled with non-null [cause] is called a _failed_ channel. Attempts to send or - * receive on a failed channel throw the specified [cause] exception. + * A cause can be used to specify an error message or to provide other details on + * a cancellation reason for debugging purposes. */ + public fun cancel(cause: CancellationException? = null) + + /** + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. + */ + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only") public fun cancel(cause: Throwable? = null): Boolean } diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index dbec12f09d..351bd49e79 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -241,8 +241,10 @@ public interface ReceiveChannel { public operator fun iterator(): ChannelIterator /** - * Cancels reception of remaining elements from this channel. This function closes the channel - * and removes all buffered sent elements from it. + * Cancels reception of remaining elements from this channel with an optional [cause]. + * This function closes the channel and removes all buffered sent elements from it. + * A cause can be used to specify an error message or to provide other details on + * a cancellation reason for debugging purposes. * * Immediately after invocation of this function [isClosedForReceive] and * [isClosedForSend][SendChannel.isClosedForSend] @@ -250,21 +252,18 @@ public interface ReceiveChannel { * afterwards will throw [ClosedSendChannelException], while attempts to receive will throw * [ClosedReceiveChannelException]. */ - public fun cancel(): Unit + public fun cancel(cause: CancellationException? = null) /** - * @suppress + * @suppress This method implements old version of JVM ABI. Use [cancel]. */ - @Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION") - @Deprecated(level = DeprecationLevel.HIDDEN, message = "Left here for binary compatibility") - @JvmName("cancel") - public fun cancel0(): Boolean = cancel(null) + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + public fun cancel() = cancel(null) /** - * @suppress + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. */ - @ObsoleteCoroutinesApi - @Deprecated(level = DeprecationLevel.WARNING, message = "Use cancel without cause", replaceWith = ReplaceWith("cancel()")) + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun cancel(cause: Throwable? = null): Boolean } diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt index f69c69b34a..9382600af8 100644 --- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt @@ -13,19 +13,23 @@ internal open class ChannelCoroutine( protected val _channel: Channel, active: Boolean ) : AbstractCoroutine(parentContext, active), Channel by _channel { - override val cancelsParent: Boolean get() = true - val channel: Channel get() = this override fun cancel() { - cancel(null) + cancelInternal(null) } - override fun cancel0(): Boolean = cancel(null) + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + final override fun cancel(cause: Throwable?): Boolean = + cancelInternal(cause) + + final override fun cancel(cause: CancellationException?) { + cancelInternal(cause) + } - override fun cancel(cause: Throwable?): Boolean { - val wasCancelled = _channel.cancel(cause) - if (wasCancelled) super.cancel(cause) // cancel the job - return wasCancelled + override fun cancelInternal(cause: Throwable?): Boolean { + _channel.cancel(cause?.toCancellationException()) // cancel the channel + cancelCoroutine(cause) // cancel the job + return true // does not matter - result is used in DEPRECATED functions only } } diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index 05cb342d06..17c19ff1d3 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -61,9 +61,15 @@ public suspend inline fun BroadcastChannel.consumeEach(action: (E) -> Uni */ @ObsoleteCoroutinesApi public fun ReceiveChannel<*>.consumes(): CompletionHandler = { cause: Throwable? -> - @Suppress("DEPRECATION") - cancel(cause) - } + cancelConsumed(cause) +} + +@PublishedApi +internal fun ReceiveChannel<*>.cancelConsumed(cause: Throwable?) { + cancel(cause?.let { + it as? CancellationException ?: CancellationException("Channel was consumed, consumer had failed", it) + }) +} /** * Returns a [CompletionHandler] that invokes [cancel][ReceiveChannel.cancel] on all the @@ -79,8 +85,7 @@ public fun consumesAll(vararg channels: ReceiveChannel<*>): CompletionHandler = var exception: Throwable? = null for (channel in channels) try { - @Suppress("DEPRECATION") - channel.cancel(cause) + channel.cancelConsumed(cause) } catch (e: Throwable) { if (exception == null) { exception = e @@ -115,8 +120,7 @@ public inline fun ReceiveChannel.consume(block: ReceiveChannel.() - cause = e throw e } finally { - @Suppress("DEPRECATION") - cancel(cause) + cancelConsumed(cause) } } @@ -363,7 +367,7 @@ public suspend inline fun ReceiveChannel.indexOfFirst(predicate: (E) -> B * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @ObsoleteCoroutinesApi -public inline suspend fun ReceiveChannel.indexOfLast(predicate: (E) -> Boolean): Int { +public suspend inline fun ReceiveChannel.indexOfLast(predicate: (E) -> Boolean): Int { var lastIndex = -1 var index = 0 consumeEach { diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index bde65f2cd7..a4f8a852fe 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -202,10 +202,23 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { } /** - * Closes this broadcast channel. Same as [close]. + * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. */ + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public override fun cancel(cause: Throwable?): Boolean = close(cause) + /** + * Cancels this conflated broadcast channel with an optional cause, same as [close]. + * This function closes the channel with + * the specified cause (unless it was already closed), + * and [cancels][ReceiveChannel.cancel] all open subscriptions. + * A cause can be used to specify an error message or to provide other details on + * a cancellation reason for debugging purposes. + */ + public override fun cancel(cause: CancellationException?) { + close(cause) + } + /** * Sends the value to all subscribed receives and stores this value as the most recent state for * future subscribers. This implementation never suspends. @@ -268,11 +281,10 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { block.startCoroutineUnintercepted(receiver = this, completion = select.completion) } - @Suppress("DEPRECATION") private class Subscriber( private val broadcastChannel: ConflatedBroadcastChannel ) : ConflatedChannel(), ReceiveChannel { - override fun cancel(cause: Throwable?): Boolean = + override fun cancelInternal(cause: Throwable?): Boolean = close(cause).also { closed -> if (closed) broadcastChannel.closeSubscriber(this) } diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 930b14f23c..e456d82d0d 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -97,6 +97,6 @@ private class ProducerCoroutine( override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) { val cause = (state as? CompletedExceptionally)?.cause val processed = _channel.close(cause) - if (cause != null && !processed && suppressed) handleExceptionViaHandler(context, cause) + if (cause != null && !processed && suppressed) handleCoroutineException(context, cause) } } diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 56a32bc075..5ffaa61e71 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -19,6 +19,9 @@ internal open class ScopeCoroutine( final override fun getStackTraceElement(): StackTraceElement? = null override val defaultResumeMode: Int get() = MODE_DIRECT + override val cancelsParent: Boolean + get() = false // it throws exception to parent instead of cancelling it + @Suppress("UNCHECKED_CAST") internal override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) { if (state is CompletedExceptionally) { diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 472a5ee037..2ec7fc6481 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -309,10 +309,13 @@ internal class SelectBuilderImpl( @PublishedApi internal fun handleBuilderException(e: Throwable) { - if (trySelect(null)) + if (trySelect(null)) { resumeWithException(e) - else + } else { + // Cannot handle this exception -- builder was already resumed with a different exception, + // so treat it as "unhandled exception" handleCoroutineException(context, e) + } } override val isSelected: Boolean get() = state !== this diff --git a/kotlinx-coroutines-core/common/test/AbstractCoroutineTest.kt b/kotlinx-coroutines-core/common/test/AbstractCoroutineTest.kt index c2eddbf0e1..5f6dbd10cc 100644 --- a/kotlinx-coroutines-core/common/test/AbstractCoroutineTest.kt +++ b/kotlinx-coroutines-core/common/test/AbstractCoroutineTest.kt @@ -86,7 +86,7 @@ class AbstractCoroutineTest : TestBase() { expect(2) coroutine.start() expect(4) - coroutine.cancel(TestException1()) + coroutine.cancelCoroutine(TestException1()) expect(7) coroutine.resumeWithException(TestException2()) finish(10) diff --git a/kotlinx-coroutines-core/common/test/AsyncTest.kt b/kotlinx-coroutines-core/common/test/AsyncTest.kt index f0c1c0d274..6fd4ebbe04 100644 --- a/kotlinx-coroutines-core/common/test/AsyncTest.kt +++ b/kotlinx-coroutines-core/common/test/AsyncTest.kt @@ -52,16 +52,20 @@ class AsyncTest : TestBase() { } @Test - fun testCancellationWithCause() = runTest(expected = { it is TestException }) { + fun testCancellationWithCause() = runTest { expect(1) val d = async(NonCancellable, start = CoroutineStart.ATOMIC) { - finish(3) + expect(3) yield() } - expect(2) - d.cancel(TestException()) - d.await() + d.cancel(TestCancellationException("TEST")) + try { + d.await() + } catch (e: TestCancellationException) { + finish(4) + assertEquals("TEST", e.message) + } } @Test @@ -155,7 +159,7 @@ class AsyncTest : TestBase() { throw TestException() } expect(1) - deferred.cancel(TestException()) + deferred.cancel() try { deferred.await() } catch (e: TestException) { diff --git a/kotlinx-coroutines-core/common/test/AwaitTest.kt b/kotlinx-coroutines-core/common/test/AwaitTest.kt index b86c7852f9..0949b62c8c 100644 --- a/kotlinx-coroutines-core/common/test/AwaitTest.kt +++ b/kotlinx-coroutines-core/common/test/AwaitTest.kt @@ -37,11 +37,11 @@ class AwaitTest : TestBase() { fun testAwaitAllLazy() = runTest { expect(1) val d = async(start = CoroutineStart.LAZY) { - expect(2); + expect(2) 1 } val d2 = async(start = CoroutineStart.LAZY) { - expect(3); + expect(3) 2 } assertEquals(listOf(1, 2), awaitAll(d, d2)) @@ -203,9 +203,9 @@ class AwaitTest : TestBase() { @Test fun testAwaitAllFullyCompletedExceptionally() = runTest { val d1 = CompletableDeferred(parent = null) - .apply { cancel(TestException()) } + .apply { completeExceptionally(TestException()) } val d2 = CompletableDeferred(parent = null) - .apply { cancel(TestException()) } + .apply { completeExceptionally(TestException()) } val job = async { expect(3) } expect(1) try { diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt index 80c43cfffd..00f719e632 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt @@ -2,6 +2,8 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 + package kotlinx.coroutines import kotlin.coroutines.* @@ -77,10 +79,18 @@ class CancellableContinuationHandlersTest : TestBase() { } @Test - fun testExceptionInHandler() = runTest({it is CompletionHandlerException}) { - suspendCancellableCoroutine { c -> - c.invokeOnCancellation { throw AssertionError() } - c.cancel() + fun testExceptionInHandler() = runTest( + unhandled = listOf({ it -> it is CompletionHandlerException }) + ) { + expect(1) + try { + suspendCancellableCoroutine { c -> + c.invokeOnCancellation { throw AssertionError() } + c.cancel() + } + } catch (e: CancellationException) { + expect(2) } + finish(3) } } diff --git a/kotlinx-coroutines-core/common/test/CompletableDeferredTest.kt b/kotlinx-coroutines-core/common/test/CompletableDeferredTest.kt index 967853b75c..999ff86297 100644 --- a/kotlinx-coroutines-core/common/test/CompletableDeferredTest.kt +++ b/kotlinx-coroutines-core/common/test/CompletableDeferredTest.kt @@ -64,9 +64,9 @@ class CompletableDeferredTest : TestBase() { @Test fun testCancelWithException() { val c = CompletableDeferred() - assertEquals(true, c.cancel(TestException())) + assertEquals(true, c.completeExceptionally(TestException())) checkCancelWithException(c) - assertEquals(false, c.cancel(TestException())) + assertEquals(false, c.completeExceptionally(TestException())) checkCancelWithException(c) } @@ -111,7 +111,7 @@ class CompletableDeferredTest : TestBase() { val c = CompletableDeferred(parent) checkFresh(c) assertEquals(true, parent.isActive) - assertEquals(true, c.cancel(TestException())) + assertEquals(true, c.completeExceptionally(TestException())) checkCancelWithException(c) assertEquals(false, parent.isActive) assertEquals(true, parent.isCancelled) diff --git a/kotlinx-coroutines-core/common/test/CoroutineScopeTest.kt b/kotlinx-coroutines-core/common/test/CoroutineScopeTest.kt index 4339ae036f..c46f41a073 100644 --- a/kotlinx-coroutines-core/common/test/CoroutineScopeTest.kt +++ b/kotlinx-coroutines-core/common/test/CoroutineScopeTest.kt @@ -120,9 +120,14 @@ class CoroutineScopeTest : TestBase() { try { callJobScoped() expectUnreached() - } catch (e: CancellationException) { + } catch (e: JobCancellationException) { expect(5) - assertNull(e.cause) + if (RECOVER_STACK_TRACES) { + val cause = e.cause as JobCancellationException // shall be recovered JCE + assertNull(cause.cause) + } else { + assertNull(e.cause) + } } } repeat(3) { yield() } // let everything to start properly diff --git a/kotlinx-coroutines-core/common/test/CoroutinesTest.kt b/kotlinx-coroutines-core/common/test/CoroutinesTest.kt index b2ca7279a6..534cfd61ce 100644 --- a/kotlinx-coroutines-core/common/test/CoroutinesTest.kt +++ b/kotlinx-coroutines-core/common/test/CoroutinesTest.kt @@ -313,7 +313,9 @@ class CoroutinesTest : TestBase() { } @Test - fun testNotCancellableChildWithExceptionCancelled() = runTest(expected = { it is IllegalArgumentException }) { + fun testNotCancellableChildWithExceptionCancelled() = runTest( + expected = { it is TestException } + ) { expect(1) // CoroutineStart.ATOMIC makes sure it will not get cancelled for it starts executing val d = async(NonCancellable, start = CoroutineStart.ATOMIC) { @@ -323,8 +325,8 @@ class CoroutinesTest : TestBase() { } expect(2) // now cancel with some other exception - d.cancel(IllegalArgumentException()) - // now await to see how it got crashed -- IAE should have been suppressed by TestException + d.cancel(TestCancellationException()) + // now await to see how it got crashed -- TestCancellationException should have been suppressed by TestException expect(3) d.await() } diff --git a/kotlinx-coroutines-core/common/test/JobTest.kt b/kotlinx-coroutines-core/common/test/JobTest.kt index d6fadbeb43..04d3c9e012 100644 --- a/kotlinx-coroutines-core/common/test/JobTest.kt +++ b/kotlinx-coroutines-core/common/test/JobTest.kt @@ -203,7 +203,7 @@ class JobTest : TestBase() { fun testJobWithParentCancelException() { val parent = Job() val job = Job(parent) - job.cancel(TestException()) + job.completeExceptionally(TestException()) assertTrue(job.isCancelled) assertTrue(parent.isCancelled) } diff --git a/kotlinx-coroutines-core/common/test/NonCancellableTest.kt b/kotlinx-coroutines-core/common/test/NonCancellableTest.kt index 25a6a4734f..07c3f9b725 100644 --- a/kotlinx-coroutines-core/common/test/NonCancellableTest.kt +++ b/kotlinx-coroutines-core/common/test/NonCancellableTest.kt @@ -29,8 +29,13 @@ class NonCancellableTest : TestBase() { try { job.await() expectUnreached() - } catch (e: CancellationException) { - assertNull(e.cause) + } catch (e: JobCancellationException) { + if (RECOVER_STACK_TRACES) { + val cause = e.cause as JobCancellationException // shall be recovered JCE + assertNull(cause.cause) + } else { + assertNull(e.cause) + } finish(6) } } @@ -51,13 +56,14 @@ class NonCancellableTest : TestBase() { } yield() - deferred.cancel(TestException()) + deferred.cancel(TestCancellationException("TEST")) expect(3) assertTrue(deferred.isCancelled) try { deferred.await() expectUnreached() - } catch (e: TestException) { + } catch (e: TestCancellationException) { + assertEquals("TEST", e.message) finish(6) } } @@ -118,8 +124,13 @@ class NonCancellableTest : TestBase() { try { job.await() expectUnreached() - } catch (e: CancellationException) { - assertNull(e.cause) + } catch (e: JobCancellationException) { + if (RECOVER_STACK_TRACES) { + val cause = e.cause as JobCancellationException // shall be recovered JCE + assertNull(cause.cause) + } else { + assertNull(e.cause) + } finish(7) } } diff --git a/kotlinx-coroutines-core/common/test/SupervisorTest.kt b/kotlinx-coroutines-core/common/test/SupervisorTest.kt index 130623bfec..7ed6a1d7b0 100644 --- a/kotlinx-coroutines-core/common/test/SupervisorTest.kt +++ b/kotlinx-coroutines-core/common/test/SupervisorTest.kt @@ -168,7 +168,7 @@ class SupervisorTest : TestBase() { } expect(1) yield() - parent.cancel(TestException1()) + parent.completeExceptionally(TestException1()) try { deferred.await() expectUnreached() @@ -190,7 +190,7 @@ class SupervisorTest : TestBase() { fun testSupervisorWithParentCancelException() { val parent = Job() val supervisor = SupervisorJob(parent) - supervisor.cancel(TestException1()) + supervisor.completeExceptionally(TestException1()) assertTrue(supervisor.isCancelled) assertTrue(parent.isCancelled) } diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index f705c79e73..9c162ff54f 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -28,8 +28,10 @@ public class TestException(message: String? = null) : Throwable(message), NonRec public class TestException1(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestException3(message: String? = null) : Throwable(message), NonRecoverableThrowable +public class TestCancellationException(message: String? = null) : CancellationException(message), NonRecoverableThrowable public class TestRuntimeException(message: String? = null) : RuntimeException(message), NonRecoverableThrowable public class RecoverableTestException(message: String? = null) : RuntimeException(message) +public class RecoverableTestCancellationException(message: String? = null) : CancellationException(message) public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher diff --git a/kotlinx-coroutines-core/common/test/WithContextTest.kt b/kotlinx-coroutines-core/common/test/WithContextTest.kt index 6db3349c35..be6bde4a09 100644 --- a/kotlinx-coroutines-core/common/test/WithContextTest.kt +++ b/kotlinx-coroutines-core/common/test/WithContextTest.kt @@ -198,7 +198,7 @@ class WithContextTest : TestBase() { } @Test - fun testRunSelfCancellationWithException() = runTest(unhandled = listOf({e -> e is AssertionError})) { + fun testRunSelfCancellationWithException() = runTest { expect(1) var job: Job? = null job = launch(Job()) { @@ -208,13 +208,12 @@ class WithContextTest : TestBase() { require(isActive) expect(5) job!!.cancel() - require(job!!.cancel(AssertionError())) // cancel again, no success here require(!isActive) - throw TestException() // but throw a different exception + throw TestException() // but throw an exception } } catch (e: Throwable) { expect(7) - // make sure TestException, not CancellationException or AssertionError is thrown + // make sure TestException, not CancellationException is thrown assertTrue(e is TestException, "Caught $e") } } @@ -228,7 +227,7 @@ class WithContextTest : TestBase() { } @Test - fun testRunSelfCancellation() = runTest(unhandled = listOf({e -> e is AssertionError})) { + fun testRunSelfCancellation() = runTest { expect(1) var job: Job? = null job = launch(Job()) { @@ -238,14 +237,13 @@ class WithContextTest : TestBase() { require(isActive) expect(5) job!!.cancel() // cancel itself - require(job!!.cancel(AssertionError())) require(!isActive) "OK".wrap() } expectUnreached() } catch (e: Throwable) { expect(7) - // make sure JCE is thrown + // make sure CancellationException is thrown assertTrue(e is CancellationException, "Caught $e") } } diff --git a/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt index 0b3a22241c..9867ead560 100644 --- a/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt @@ -162,7 +162,9 @@ class ArrayBroadcastChannelTest : TestBase() { val channel = BroadcastChannel(1) // launch generator (for later) in this context launch { - for (x in 1..5) channel.send(x) + for (x in 1..5) { + channel.send(x) + } channel.close() } // start consuming @@ -188,10 +190,10 @@ class ArrayBroadcastChannelTest : TestBase() { } @Test - fun testCancelWithCause() = runTest({ it is TestException }) { + fun testCancelWithCause() = runTest({ it is TestCancellationException }) { val channel = BroadcastChannel(1) val subscription = channel.openSubscription() - subscription.cancel(TestException()) + subscription.cancel(TestCancellationException()) subscription.receiveOrNull() } diff --git a/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt index 79f73aed97..56a1c5a541 100644 --- a/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt @@ -139,9 +139,9 @@ class ArrayChannelTest : TestBase() { } @Test - fun testCancelWithCause() = runTest({ it is TestException }) { + fun testCancelWithCause() = runTest({ it is TestCancellationException }) { val channel = Channel(5) - channel.cancel(TestException()) + channel.cancel(TestCancellationException()) channel.receiveOrNull() } } diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 61b5fc738a..666b706499 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -78,9 +78,9 @@ class ConflatedChannelTest : TestBase() { } @Test - fun testCancelWithCause() = runTest({ it is TestException }) { + fun testCancelWithCause() = runTest({ it is TestCancellationException }) { val channel = Channel(Channel.CONFLATED) - channel.cancel(TestException()) + channel.cancel(TestCancellationException()) channel.receiveOrNull() } } diff --git a/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt index 763ed9bcde..700ea96c46 100644 --- a/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt @@ -35,9 +35,9 @@ class LinkedListChannelTest : TestBase() { } @Test - fun testCancelWithCause() = runTest({ it is TestException }) { + fun testCancelWithCause() = runTest({ it is TestCancellationException }) { val channel = Channel(Channel.UNLIMITED) - channel.cancel(TestException()) + channel.cancel(TestCancellationException()) channel.receiveOrNull() } } diff --git a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt index 9e01e81cd0..f286ba5d24 100644 --- a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt @@ -65,7 +65,7 @@ class ProduceTest : TestBase() { expectUnreached() } catch (e: Throwable) { expect(6) - check(e is TestException) + check(e is TestCancellationException) throw e } expectUnreached() @@ -73,11 +73,11 @@ class ProduceTest : TestBase() { expect(1) check(c.receive() == 1) expect(4) - c.cancel(TestException()) + c.cancel(TestCancellationException()) try { assertNull(c.receiveOrNull()) expectUnreached() - } catch (e: TestException) { + } catch (e: TestCancellationException) { expect(5) } yield() // to produce diff --git a/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt index 7d8b421c02..60bccef04d 100644 --- a/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt @@ -277,9 +277,9 @@ class RendezvousChannelTest : TestBase() { } @Test - fun testCancelWithCause() = runTest({ it is TestException }) { + fun testCancelWithCause() = runTest({ it is TestCancellationException }) { val channel = Channel(Channel.RENDEZVOUS) - channel.cancel(TestException()) + channel.cancel(TestCancellationException()) channel.receiveOrNull() } } diff --git a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt index 682058d3c0..465699e27c 100644 --- a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt +++ b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.channels +import kotlinx.coroutines.* import kotlinx.coroutines.selects.* enum class TestChannelKind { @@ -59,8 +60,13 @@ private class ChannelViaBroadcast( override suspend fun receiveOrNull(): E? = sub.receiveOrNull() override fun poll(): E? = sub.poll() override fun iterator(): ChannelIterator = sub.iterator() - override fun cancel(): Unit = sub.cancel() - override fun cancel(cause: Throwable?): Boolean = sub.cancel(cause) + + override fun cancel(cause: CancellationException?) = sub.cancel(cause) + + // implementing hidden method anyway, so can cast to an internal class + @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") + override fun cancel(cause: Throwable?): Boolean = (sub as AbstractChannel).cancelInternal(cause) + override val onReceive: SelectClause1 get() = sub.onReceive override val onReceiveOrNull: SelectClause1 diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/js/src/Exceptions.kt index 431ff165aa..84e9e453cf 100644 --- a/kotlinx-coroutines-core/js/src/Exceptions.kt +++ b/kotlinx-coroutines-core/js/src/Exceptions.kt @@ -23,6 +23,13 @@ public actual class CompletionHandlerException public actual constructor( */ public actual open class CancellationException actual constructor(message: String?) : IllegalStateException(message) +/** + * Creates a cancellation exception with a specified message and [cause]. + */ +@Suppress("FunctionName") +public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException = + CancellationException(message.withCause(cause)) + /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed * without cause, or with a cause or exception that is not [CancellationException] @@ -47,8 +54,15 @@ internal actual class DispatchException actual constructor(message: String, caus internal fun IllegalStateException(message: String, cause: Throwable?) = IllegalStateException(message.withCause(cause)) -private fun String.withCause(cause: Throwable?) = - if (cause == null) this else "$this; caused by $cause" +private fun String?.withCause(cause: Throwable?) = + when { + cause == null -> this + this == null -> "caused by $cause" + else -> "$this; caused by $cause" + } @Suppress("NOTHING_TO_INLINE") -internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } \ No newline at end of file +internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } + +// For use in tests +internal actual val RECOVER_STACK_TRACES: Boolean = false \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index 033452a3a1..449c61b950 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.kt @@ -58,6 +58,9 @@ private class BlockingCoroutine( private val blockedThread: Thread, private val eventLoop: EventLoop? ) : AbstractCoroutine(parentContext, true) { + override val cancelsParent: Boolean + get() = false // it throws exception to parent instead of cancelling it + override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) { // wake up blocked thread if (Thread.currentThread() != blockedThread) @@ -72,7 +75,7 @@ private class BlockingCoroutine( try { while (true) { @Suppress("DEPRECATION") - if (Thread.interrupted()) throw InterruptedException().also { cancel(it) } + if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) } val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE // note: process next even may loose unpark flag, so check if completed before parking if (isCompleted) break diff --git a/kotlinx-coroutines-core/jvm/src/Debug.kt b/kotlinx-coroutines-core/jvm/src/Debug.kt index 38a9d05177..3c750daea0 100644 --- a/kotlinx-coroutines-core/jvm/src/Debug.kt +++ b/kotlinx-coroutines-core/jvm/src/Debug.kt @@ -48,8 +48,9 @@ public interface CopyableThrowable where T : Throwable, T : CopyableThrowable * Creates a copy of the current instance. * For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one. * Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call. + * An exception can opt-out of copying by returning `null` from this function. */ - public fun createCopy(): T + public fun createCopy(): T? } /** @@ -77,8 +78,9 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value -> } } +// Note: stack-trace recovery is enabled only in debug mode @JvmField -internal val RECOVER_STACKTRACES = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) +internal actual val RECOVER_STACK_TRACES = DEBUG && systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) // internal debugging tools diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 0138cfe467..f6cc75b7b3 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -2,6 +2,8 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("FunctionName") + package kotlinx.coroutines /** @@ -23,6 +25,13 @@ public actual class CompletionHandlerException actual constructor( */ public actual typealias CancellationException = java.util.concurrent.CancellationException +/** + * Creates a cancellation exception with a specified message and [cause]. + */ +@Suppress("FunctionName") +public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException = + CancellationException(message).apply { initCause(cause) } + /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed * without cause, or with a cause or exception that is not [CancellationException] @@ -32,7 +41,7 @@ internal actual class JobCancellationException public actual constructor( message: String, cause: Throwable?, @JvmField internal actual val job: Job -) : CancellationException(message) { +) : CancellationException(message), CopyableThrowable { init { if (cause != null) initCause(cause) @@ -51,6 +60,17 @@ internal actual class JobCancellationException public actual constructor( return this } + override fun createCopy(): JobCancellationException? { + if (DEBUG) { + return JobCancellationException(message!!, this, job) + } + + /* + * In non-debug mode we don't copy JCE for speed as it does not have the stack trace anyway. + */ + return null + } + override fun toString(): String = "${super.toString()}; job=$job" @Suppress("DEPRECATION") diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index cc5a51893d..f24eb6892b 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -128,12 +128,16 @@ private open class ActorCoroutine( active: Boolean ) : ChannelCoroutine(parentContext, channel, active), ActorScope { override fun onCancellation(cause: Throwable?) { - @Suppress("DEPRECATION") - _channel.cancel(cause) + _channel.cancel(cause?.let { + it as? CancellationException ?: CancellationException("$classSimpleName was cancelled", it) + }) } override val cancelsParent: Boolean get() = true - override fun handleJobException(exception: Throwable) = handleExceptionViaHandler(parentContext, exception) + + override fun handleJobException(exception: Throwable, handled: Boolean) { + if (!handled) handleCoroutineException(context, exception) + } } private class LazyActorCoroutine( diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt index 53c4d5dea8..8d11c6fdb7 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt @@ -20,7 +20,7 @@ private val exceptionCtors: WeakHashMap, Ctor> = WeakHashMa internal fun tryCopyException(exception: E): E? { // Fast path for CopyableThrowable if (exception is CopyableThrowable<*>) { - return runCatching { exception.createCopy() as E }.getOrNull() + return runCatching { exception.createCopy() as E? }.getOrNull() } // Use cached ctor if found cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor -> diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index 9457130164..4727b26d5b 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -12,9 +12,7 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* internal actual fun recoverStackTrace(exception: E): E { - if (recoveryDisabled(exception)) { - return exception - } + if (recoveryDisabled(exception)) return exception // No unwrapping on continuation-less path: exception is not reported multiple times via slow paths val copy = tryCopyException(exception) ?: return exception return copy.sanitizeStackTrace() @@ -41,10 +39,7 @@ private fun E.sanitizeStackTrace(): E { } internal actual fun recoverStackTrace(exception: E, continuation: Continuation<*>): E { - if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) { - return exception - } - + if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) return exception return recoverFromStackFrame(exception, continuation) } @@ -146,26 +141,23 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin } internal actual fun unwrap(exception: E): E { - if (recoveryDisabled(exception)) { - return exception - } - + if (recoveryDisabled(exception)) return exception val cause = exception.cause // Fast-path to avoid array cloning if (cause == null || cause.javaClass != exception.javaClass) { return exception } - + // Slow path looks for artificial frames in a stack-trace if (exception.stackTrace.any { it.isArtificial() }) { @Suppress("UNCHECKED_CAST") - return exception.cause as? E ?: exception + return cause as E } else { return exception } } private fun recoveryDisabled(exception: E) = - !RECOVER_STACKTRACES || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable + !RECOVER_STACK_TRACES || exception is NonRecoverableThrowable private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque { val stack = ArrayDeque() diff --git a/kotlinx-coroutines-core/jvm/test/AwaitJvmTest.kt b/kotlinx-coroutines-core/jvm/test/AwaitJvmTest.kt index 8f8ef09644..c6b57c8f6b 100644 --- a/kotlinx-coroutines-core/jvm/test/AwaitJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AwaitJvmTest.kt @@ -12,7 +12,7 @@ class AwaitJvmTest : TestBase() { // This test is to make sure that handlers installed on the second deferred do not leak val d1 = CompletableDeferred() val d2 = CompletableDeferred() - d1.cancel(TestException()) // first is crashed + d1.completeExceptionally(TestException()) // first is crashed val iterations = 3_000_000 * stressTestMultiplier for (iter in 1..iterations) { try { diff --git a/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt b/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt index b3619aaefa..0d1a7c6c3f 100644 --- a/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines import org.junit.* import org.junit.Test -import java.io.* import java.util.concurrent.* import kotlin.test.* @@ -71,14 +70,15 @@ class JoinStressTest : TestBase() { exceptionalJob.await() } catch (e: TestException) { 0 - } catch (e: IOException) { + } catch (e: TestException1) { 1 } } val canceller = async(pool + NonCancellable) { barrier.await() - exceptionalJob.cancel(IOException()) + // cast for test purposes only + (exceptionalJob as AbstractCoroutine<*>).cancelInternal(TestException1()) } barrier.await() diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index d16d9723fb..db5c53ae80 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -62,13 +62,20 @@ public actual open class TestBase actual constructor() { */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun error(message: Any, cause: Throwable? = null): Nothing { - val exception = IllegalStateException(message.toString(), cause) + throw makeError(message, cause) + } + + private fun makeError(message: Any, cause: Throwable? = null): IllegalStateException = + IllegalStateException(message.toString(), cause).also { + setError(it) + } + + private fun setError(exception: Throwable) { error.compareAndSet(null, exception) - throw exception } private fun printError(message: String, cause: Throwable) { - error.compareAndSet(null, cause) + setError(cause) println("$message: $cause") cause.printStackTrace(System.out) println("--- Detected at ---") @@ -118,21 +125,35 @@ public actual open class TestBase actual constructor() { initPoolsBeforeTest() threadsBefore = currentThreads() originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - Thread.setDefaultUncaughtExceptionHandler({ t, e -> - println("Uncaught exception in thread $t: $e") + Thread.setDefaultUncaughtExceptionHandler { t, e -> + println("Exception in thread $t: $e") // The same message as in default handler e.printStackTrace() uncaughtExceptions.add(e) - }) + } } @After fun onCompletion() { - error.get()?.let { throw it } - check(actionIndex.get() == 0 || finished.get()) { "Expecting that 'finish(...)' was invoked, but it was not" } + // onCompletion should not throw exceptions before it finishes all cleanup, so that other tests always + // start in a clear, restored state + if (actionIndex.get() != 0 && !finished.get()) { + makeError("Expecting that 'finish(...)' was invoked, but it was not") + } + // Shutdown all thread pools shutdownPoolsAfterTest() - checkTestThreads(threadsBefore) + // Check that that are now leftover threads + runCatching { + checkTestThreads(threadsBefore) + }.onFailure { + setError(it) + } + // Restore original uncaught exception handler Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler) - assertTrue(uncaughtExceptions.isEmpty(), "Expected no uncaught exceptions, but got $uncaughtExceptions") + if (uncaughtExceptions.isNotEmpty()) { + makeError("Expected no uncaught exceptions, but got $uncaughtExceptions") + } + // The very last action -- throw error if any was detected + error.get()?.let { throw it } } fun initPoolsBeforeTest() { diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/JobBasicCancellationTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/JobBasicCancellationTest.kt index 9d347b9423..28d85fe341 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/JobBasicCancellationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/JobBasicCancellationTest.kt @@ -149,8 +149,8 @@ class JobBasicCancellationTest : TestBase() { @Test fun testConsecutiveCancellation() { val deferred = CompletableDeferred() - assertTrue(deferred.cancel(IndexOutOfBoundsException())) - assertFalse(deferred.cancel(AssertionError())) // second cancelled is too late + assertTrue(deferred.completeExceptionally(IndexOutOfBoundsException())) + assertFalse(deferred.completeExceptionally(AssertionError())) // second is too late val cause = deferred.getCancellationException().cause!! assertTrue(cause is IndexOutOfBoundsException) assertNull(cause.cause) diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt index bcc90b57c9..746909a61e 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/JobExceptionHandlingTest.kt @@ -44,11 +44,12 @@ class JobExceptionHandlingTest : TestBase() { expect(1) yield() - deferred.cancel(IOException()) + deferred.cancel(TestCancellationException("TEST")) try { deferred.await() expectUnreached() - } catch (e: IOException) { + } catch (e: TestCancellationException) { + assertEquals("TEST", e.message) assertTrue(e.suppressed.isEmpty()) finish(3) } @@ -64,7 +65,7 @@ class JobExceptionHandlingTest : TestBase() { expect(1) yield() - parent.cancel(IOException()) + parent.completeExceptionally(IOException()) try { deferred.await() expectUnreached() @@ -100,35 +101,6 @@ class JobExceptionHandlingTest : TestBase() { checkException(exception) } - @Test - fun testConsecutiveCancellation() { - /* - * Root parent: JobImpl() - * Child: throws IOException - * Launcher: cancels child with AE and then cancels it with NPE - * Result: AE with suppressed NPE and IOE - */ - val exception = runBlock { - val job = Job() - val child = launch(job, start = ATOMIC) { - expect(2) - throw IOException() - } - - expect(1) - child.cancel(ArithmeticException()) - child.cancel(NullPointerException()) - job.join() - finish(3) - } - - assertTrue(exception is ArithmeticException) - val suppressed = exception.suppressed - assertEquals(2, suppressed.size) - checkException(suppressed[0]) - checkException(suppressed[1]) - } - @Test fun testExceptionOnChildCancellation() { /* @@ -216,7 +188,7 @@ class JobExceptionHandlingTest : TestBase() { yield() expect(4) - job.cancel(IOException()) + job.completeExceptionally(IOException()) } expect(1) diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt index c074e87c9d..56da2aac8c 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt @@ -99,11 +99,11 @@ class ProduceExceptionsTest : TestBase() { var channel: ReceiveChannel? = null channel = produce(NonCancellable) { expect(2) - channel!!.cancel(TestException()) + channel!!.cancel(TestCancellationException()) try { send(1) // Not a ClosedForSendException - } catch (e: TestException) { + } catch (e: TestCancellationException) { expect(3) throw e } @@ -113,7 +113,7 @@ class ProduceExceptionsTest : TestBase() { yield() try { channel.receive() - } catch (e: TestException) { + } catch (e: TestCancellationException) { assertTrue(e.suppressed.isEmpty()) finish(4) } @@ -148,7 +148,7 @@ class ProduceExceptionsTest : TestBase() { val job = Job() val channel = produce(job) { expect(2) - job.cancel(TestException2()) + job.completeExceptionally(TestException2()) try { send(1) } catch (e: CancellationException) { // Not a TestException2 diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt index d922edef0c..748f0c1697 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt @@ -111,7 +111,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() { sendFromScope() } catch (e: Exception) { verifyStackTrace(e, - "kotlinx.coroutines.RecoverableTestException\n" + + "kotlinx.coroutines.RecoverableTestCancellationException\n" + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + "\t(Coroutine boundary)\n" + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + @@ -120,7 +120,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() { "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendFromScope\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:29)\n" + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:109)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "Caused by: kotlinx.coroutines.RecoverableTestCancellationException\n" + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") } @@ -129,8 +129,37 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() { yield() expect(2) // Cancel is an analogue of `produce` failure, just a shorthand - channel.cancel(RecoverableTestException()) + channel.cancel(RecoverableTestCancellationException()) finish(3) deferred.await() } + + // See https://github.com/Kotlin/kotlinx.coroutines/issues/950 + @Test + fun testCancelledOffer() = runTest { + expect(1) + val job = Job() + val actor = actor(job, Channel.UNLIMITED) { + consumeEach { + expectUnreached() // is cancelled before offer + } + } + job.cancel() + try { + actor.offer(1) + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@3af42ad0\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:186)\n" + + "\tat kotlinx.coroutines.channels.ChannelCoroutine.offer(ChannelCoroutine.kt)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testCancelledOffer\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:150)\n" + + "Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@3af42ad0\n", + // ... java.lang.* stuff and JobSupport.* snipped here ... + "\tat kotlinx.coroutines.Job\$DefaultImpls.cancel\$default(Job.kt:164)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testCancelledOffer\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:148)" + ) + finish(2) + } + } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt index 51e71d5138..15884332b4 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt @@ -5,13 +5,15 @@ import kotlin.test.* public fun verifyStackTrace(e: Throwable, vararg traces: String) { val stacktrace = toStackTrace(e) + val normalizedActual = stacktrace.normalizeStackTrace() traces.forEach { - assertTrue( - stacktrace.trimStackTrace().contains(it.trimStackTrace()), - "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" - ) + val normalizedExpected = it.normalizeStackTrace() + if (!normalizedActual.contains(normalizedExpected)) { + // A more readable error message would be produced by assertEquals + assertEquals(normalizedExpected, normalizedActual, "Actual trace does not contain expected one") + } } - + // Check "Caused by" counts val causes = stacktrace.count("Caused by") assertNotEquals(0, causes) assertEquals(traces.map { it.count("Caused by") }.sum(), causes) @@ -23,14 +25,16 @@ public fun toStackTrace(t: Throwable): String { return sw.toString() } -public fun String.trimStackTrace(): String { - return applyBackspace(trimIndent().replace(Regex(":[0-9]+"), "") - .replace("kotlinx_coroutines_core_main", "") // yay source sets - .replace("kotlinx_coroutines_core", "")) -} +public fun String.normalizeStackTrace(): String = + applyBackspace() + .replace(Regex(":[0-9]+"), "") // remove line numbers + .replace("kotlinx_coroutines_core_main", "") // yay source sets + .replace("kotlinx_coroutines_core", "") + .replace(Regex("@[0-9a-f]+"), "") // remove hex addresses in debug toStrings + .lines().joinToString("\n") // normalize line separators -public fun applyBackspace(line: String): String { - val array = line.toCharArray() +public fun String.applyBackspace(): String { + val array = toCharArray() val stack = CharArray(array.size) var stackSize = -1 for (c in array) { @@ -40,7 +44,6 @@ public fun applyBackspace(line: String): String { --stackSize } } - return String(stack, 0, stackSize) } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/SuppressionTests.kt b/kotlinx-coroutines-core/jvm/test/exceptions/SuppressionTests.kt index e5f96f78d1..6ccd73d337 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/SuppressionTests.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/SuppressionTests.kt @@ -12,25 +12,6 @@ import kotlin.test.* @Suppress("DEPRECATION") class SuppressionTests : TestBase() { - - @Test - fun testCancellationTransparency() = runTest { - val deferred = async(NonCancellable, start = CoroutineStart.ATOMIC) { - expect(2) - throw ArithmeticException() - } - - expect(1) - deferred.cancel(TestException("Message")) - - try { - deferred.await() - } catch (e: TestException) { - checkException(e.suppressed[0]) - finish(3) - } - } - @Test fun testNotificationsWithException() = runTest { expect(1) @@ -72,7 +53,7 @@ class SuppressionTests : TestBase() { expect(2) coroutine.start() expect(4) - coroutine.cancel(ArithmeticException()) + coroutine.cancelInternal(ArithmeticException()) expect(7) coroutine.resumeWithException(IOException()) finish(10) @@ -88,7 +69,7 @@ class SuppressionTests : TestBase() { } launch { - val exception = RecoverableTestException() + val exception = RecoverableTestCancellationException() channel.cancel(exception) throw exception } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt index 6760927a80..2995466390 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.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.coroutines.* import kotlin.test.* @@ -24,59 +23,61 @@ class WithContextCancellationStressTest : TestBase() { @Test @Suppress("DEPRECATION") - fun testConcurrentCancellation() = runBlocking { - var ioException = 0 - var arithmeticException = 0 - var aioobException = 0 + fun testConcurrentFailure() = runBlocking { + var eCnt = 0 + var e1Cnt = 0 + var e2Cnt = 0 repeat(iterations) { val barrier = CyclicBarrier(4) val ctx = pool + NonCancellable + var e1 = false + var e2 = false val jobWithContext = async(ctx) { withContext(wrapperDispatcher(coroutineContext)) { - barrier.await() - throw IOException() - } - } + launch { + barrier.await() + e1 = true + throw TestException1() + } - val cancellerJob = async(ctx) { - barrier.await() - jobWithContext.cancel(ArithmeticException()) - } + launch { + barrier.await() + e2 = true + throw TestException2() + } - val cancellerJob2 = async(ctx) { - barrier.await() - jobWithContext.cancel(ArrayIndexOutOfBoundsException()) + barrier.await() + throw TestException() + } } barrier.await() - val aeCancelled = cancellerJob.await() - val aioobCancelled = cancellerJob2.await() try { jobWithContext.await() - } catch (e: Exception) { + } catch (e: Throwable) { when (e) { - is IOException -> { - ++ioException - e.checkSuppressed(aeException = aeCancelled, aioobException = aioobCancelled) + is TestException -> { + eCnt++ + e.checkSuppressed(e1 = e1, e2 = e2) } - is ArithmeticException -> { - ++arithmeticException - e.checkSuppressed(ioException = true, aioobException = aioobCancelled) + is TestException1 -> { + e1Cnt++ + e.checkSuppressed(ex = true, e2 = e2) } - is ArrayIndexOutOfBoundsException -> { - ++aioobException - e.checkSuppressed(ioException = true, aeException = aeCancelled) + is TestException2 -> { + e2Cnt++ + e.checkSuppressed(ex = true, e1 = e1) } else -> error("Unexpected exception $e") } } } - require(ioException > 0) { "At least one IOException expected" } - require(arithmeticException > 0) { "At least one ArithmeticException expected" } - require(aioobException > 0) { "At least one ArrayIndexOutOfBoundsException expected" } + require(eCnt > 0) { "At least one TestException expected" } + require(e1Cnt > 0) { "At least one TestException1 expected" } + require(e2Cnt > 0) { "At least one TestException2 expected" } } private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { @@ -89,29 +90,19 @@ class WithContextCancellationStressTest : TestBase() { } private fun Throwable.checkSuppressed( - ioException: Boolean = false, - aeException: Boolean = false, - aioobException: Boolean = false + ex: Boolean = false, + e1: Boolean = false, + e2: Boolean = false ) { val suppressed: Array = suppressed - - try { - if (ioException) { - assertTrue(suppressed.any { it is IOException }, "IOException should be present: $this") - } - - if (aeException) { - assertTrue(suppressed.any { it is ArithmeticException }, "ArithmeticException should be present: $this") - } - - if (aioobException) { - assertTrue( - suppressed.any { it is ArrayIndexOutOfBoundsException }, - "ArrayIndexOutOfBoundsException should be present: $this" - ) - } - } catch (e: Throwable) { - val a =2 + if (ex) { + assertTrue(suppressed.any { it is TestException }, "TestException should be present: $this") + } + if (e1) { + assertTrue(suppressed.any { it is TestException1 }, "TestException1 should be present: $this") + } + if (e2) { + assertTrue(suppressed.any { it is TestException2 }, "TestException2 should be present: $this") } } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt index 19448b7bd7..124e5569ab 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextExceptionHandlingTest.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.* import org.junit.Test import org.junit.runner.* import org.junit.runners.* -import java.io.* import kotlin.coroutines.* import kotlin.test.* @@ -41,30 +40,29 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testCancellationWithException() = runTest { /* - * context cancelled with TE + * context cancelled with TCE * block itself throws TE2 - * Result: TE with suppressed TE2 + * Result: TE (CancellationException is always ignored) */ - val cancellationCause = TestException() + val cancellationCause = TestCancellationException() runCancellation(cancellationCause, TestException2()) { e -> - assertTrue(e is TestException) + assertTrue(e is TestException2) assertNull(e.cause) val suppressed = e.suppressed - assertEquals(suppressed.size, 1) - assertTrue(suppressed[0] is TestException2) + assertTrue(suppressed.isEmpty()) } } @Test fun testSameException() = runTest { /* - * context cancelled with TE - * block itself throws the same TE - * Result: TE + * context cancelled with TCE + * block itself throws the same TCE + * Result: TCE */ - val cancellationCause = TestException() + val cancellationCause = TestCancellationException() runCancellation(cancellationCause, cancellationCause) { e -> - assertTrue(e is TestException) + assertTrue(e is TestCancellationException) assertNull(e.cause) val suppressed = e.suppressed assertTrue(suppressed.isEmpty()) @@ -74,11 +72,11 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testSameCancellation() = runTest { /* - * context cancelled with CancellationException - * block itself throws the same CE - * Result: CE + * context cancelled with TestCancellationException + * block itself throws the same TCE + * Result: TCE */ - val cancellationCause = CancellationException() + val cancellationCause = TestCancellationException() runCancellation(cancellationCause, cancellationCause) { e -> assertSame(e, cancellationCause) assertNull(e.cause) @@ -107,11 +105,11 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testConflictingCancellation() = runTest { /* - * context cancelled with TE + * context cancelled with TCE * block itself throws CE(TE) * Result: TE (because cancellation exception is always ignored and not handled) */ - val cancellationCause = TestException() + val cancellationCause = TestCancellationException() val thrown = CancellationException() thrown.initCause(TestException()) runCancellation(cancellationCause, thrown) { e -> @@ -127,7 +125,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { * block itself throws CE * Result: TE */ - val cancellationCause = TestException() + val cancellationCause = TestCancellationException() val thrown = CancellationException() runCancellation(cancellationCause, thrown) { e -> assertSame(cancellationCause, e) @@ -139,12 +137,12 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testConflictingCancellation3() = runTest { /* - * context cancelled with CE - * block itself throws CE - * Result: CE + * context cancelled with TCE + * block itself throws TCE + * Result: TCE */ - val cancellationCause = CancellationException() - val thrown = CancellationException() + val cancellationCause = TestCancellationException() + val thrown = TestCancellationException() runCancellation(cancellationCause, thrown) { e -> assertSame(cancellationCause, e) assertNull(e.cause) @@ -154,7 +152,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testThrowingCancellation() = runTest { - val thrown = CancellationException() + val thrown = TestCancellationException() runThrowing(thrown) { e -> assertSame(thrown, e) } @@ -163,7 +161,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testThrowingCancellationWithCause() = runTest { // Exception are never unwrapped, so if CE(TE) is thrown then it is the cancellation cause - val thrown = CancellationException() + val thrown = TestCancellationException() thrown.initCause(TestException()) runThrowing(thrown) { e -> assertSame(thrown, e) @@ -173,14 +171,16 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testCancel() = runTest { runOnlyCancellation(null) { e -> - assertNull(e.cause) + val cause = e.cause as JobCancellationException // shall be recovered JCE + assertNull(cause.cause) assertTrue(e.suppressed.isEmpty()) + assertTrue(cause.suppressed.isEmpty()) } } @Test fun testCancelWithCause() = runTest { - val cause = TestException() + val cause = TestCancellationException() runOnlyCancellation(cause) { e -> assertSame(cause, e) assertTrue(e.suppressed.isEmpty()) @@ -189,7 +189,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testCancelWithCancellationException() = runTest { - val cause = CancellationException() + val cause = TestCancellationException() runThrowing(cause) { e -> assertSame(cause, e) assertNull(e.cause) @@ -207,7 +207,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { } private suspend fun runCancellation( - cancellationCause: Throwable?, + cancellationCause: CancellationException?, thrownException: Throwable, exceptionChecker: (Throwable) -> Unit ) { @@ -260,7 +260,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { } private suspend fun runOnlyCancellation( - cancellationCause: Throwable?, + cancellationCause: CancellationException?, exceptionChecker: (Throwable) -> Unit ) { expect(1) diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index c53831ac11..afeed7a508 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -52,6 +52,9 @@ private class BlockingCoroutine( parentContext: CoroutineContext, private val eventLoop: EventLoop? ) : AbstractCoroutine(parentContext, true) { + override val cancelsParent: Boolean + get() = false // it throws exception to parent instead of cancelling it + @Suppress("UNCHECKED_CAST") fun joinBlocking(): T { try { diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index 561dc75681..372e2cfbea 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -23,6 +23,13 @@ public actual class CompletionHandlerException public actual constructor( */ public actual open class CancellationException actual constructor(message: String?) : IllegalStateException(message) +/** + * Creates a cancellation exception with a specified message and [cause]. + */ +@Suppress("FunctionName") +public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException = + CancellationException(message.withCause(cause)) + /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed * without cause, or with a cause or exception that is not [CancellationException] @@ -47,8 +54,15 @@ internal actual class DispatchException actual constructor(message: String, caus internal fun IllegalStateException(message: String, cause: Throwable?) = IllegalStateException(message.withCause(cause)) -private fun String.withCause(cause: Throwable?) = - if (cause == null) this else "$this; caused by $cause" +private fun String?.withCause(cause: Throwable?) = + when { + cause == null -> this + this == null -> "caused by $cause" + else -> "$this; caused by $cause" + } @Suppress("NOTHING_TO_INLINE") internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } + +// For use in tests +internal actual val RECOVER_STACK_TRACES: Boolean = false diff --git a/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt index 467b4d84ca..a94e7cdbda 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt @@ -60,10 +60,6 @@ private class PublisherCoroutine( ) : AbstractCoroutine(parentContext, true), ProducerScope, Subscription, SelectClause2> { override val channel: SendChannel get() = this - // cancelsParent == true ensure that error is always reported to the parent, so that parent cannot complete - // without receiving reported error. - override val cancelsParent: Boolean get() = true - // Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked private val mutex = Mutex(locked = true) @@ -76,7 +72,7 @@ private class PublisherCoroutine( override val isClosedForSend: Boolean get() = isCompleted override val isFull: Boolean = mutex.isLocked - override fun close(cause: Throwable?): Boolean = cancel(cause) + override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) override fun invokeOnClose(handler: (Throwable?) -> Unit) = throw UnsupportedOperationException("PublisherCoroutine doesn't support invokeOnClose") @@ -136,8 +132,9 @@ private class PublisherCoroutine( subscriber.onNext(elem) } catch (e: Throwable) { // If onNext fails with exception, then we cancel coroutine (with this exception) and then rethrow it - // to abort the corresponding send/offer invocation - cancel(e) + // to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery, + // this failure is essentially equivalent to a failure of a child coroutine. + cancelCoroutine(e) unlockAndCheckCompleted() throw e } @@ -181,7 +178,7 @@ private class PublisherCoroutine( // If the parent had failed to handle our exception (handleJobException was invoked), then // we must not loose this exception if (shouldHandleException && cause != null) { - handleExceptionViaHandler(parentContext, cause) + handleCoroutineException(context, cause) } } else { try { @@ -192,7 +189,7 @@ private class PublisherCoroutine( subscriber.onComplete() } } catch (e: Throwable) { - handleExceptionViaHandler(parentContext, e) + handleCoroutineException(context, e) } } } @@ -204,7 +201,7 @@ private class PublisherCoroutine( override fun request(n: Long) { if (n <= 0) { // Specification requires IAE for n <= 0 - cancel(IllegalArgumentException("non-positive subscription request $n")) + cancelCoroutine(IllegalArgumentException("non-positive subscription request $n")) return } while (true) { // lock-free loop for nRequested @@ -247,8 +244,8 @@ private class PublisherCoroutine( // so here we just raise a flag (and it need NOT be volatile!) to handle this exception. // This way we defer decision to handle this exception based on our ability to send this exception // to the subscriber (see doLockedSignalCompleted) - override fun handleJobException(exception: Throwable) { - shouldHandleException = true + override fun handleJobException(exception: Throwable, handled: Boolean) { + if (!handled) shouldHandleException = true } override fun onCompletedExceptionally(exception: Throwable) { @@ -263,6 +260,6 @@ private class PublisherCoroutine( // Specification requires that after cancellation publisher stops signalling // This flag distinguishes subscription cancellation request from the job crash cancelled = true - super.cancel() + super.cancel(null) } } diff --git a/reactive/kotlinx-coroutines-reactor/src/Mono.kt b/reactive/kotlinx-coroutines-reactor/src/Mono.kt index 97c37f23ac..49d3fa169d 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Mono.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Mono.kt @@ -42,8 +42,6 @@ private class MonoCoroutine( parentContext: CoroutineContext, private val sink: MonoSink ) : AbstractCoroutine(parentContext, true), Disposable { - override val cancelsParent: Boolean get() = true - var disposed = false override fun onCompleted(value: T) { diff --git a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt index eb3b256d0e..21026afec7 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt @@ -40,7 +40,6 @@ private class RxCompletableCoroutine( parentContext: CoroutineContext, private val subscriber: CompletableEmitter ) : AbstractCoroutine(parentContext, true) { - override val cancelsParent: Boolean get() = true override fun onCompleted(value: Unit) { if (!subscriber.isDisposed) subscriber.onComplete() } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt b/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt index 43fb3b4b88..6615414073 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt @@ -43,7 +43,6 @@ private class RxMaybeCoroutine( parentContext: CoroutineContext, private val subscriber: MaybeEmitter ) : AbstractCoroutine(parentContext, true) { - override val cancelsParent: Boolean get() = true override fun onCompleted(value: T) { if (!subscriber.isDisposed) { if (value == null) subscriber.onComplete() else subscriber.onSuccess(value) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt index a834896e6d..96490b1e06 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt @@ -57,7 +57,6 @@ private class RxObservableCoroutine( private val subscriber: ObservableEmitter ) : AbstractCoroutine(parentContext, true), ProducerScope, SelectClause2> { override val channel: SendChannel get() = this - override val cancelsParent: Boolean get() = true // Mutex is locked when while subscriber.onXXX is being invoked private val mutex = Mutex() @@ -66,7 +65,7 @@ private class RxObservableCoroutine( override val isClosedForSend: Boolean get() = isCompleted override val isFull: Boolean = mutex.isLocked - override fun close(cause: Throwable?): Boolean = cancel(cause) + override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) override fun invokeOnClose(handler: (Throwable?) -> Unit) = throw UnsupportedOperationException("RxObservableCoroutine doesn't support invokeOnClose") @@ -111,13 +110,12 @@ private class RxObservableCoroutine( try { subscriber.onNext(elem) } catch (e: Throwable) { - try { - if (!cancel(e)) - handleCoroutineException(context, e, this) - } finally { - doLockedSignalCompleted() - } - throw getCancellationException() + // If onNext fails with exception, then we cancel coroutine (with this exception) and then rethrow it + // to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery, + // this failure is essentially equivalent to a failure of a child coroutine. + cancelCoroutine(e) + doLockedSignalCompleted() + throw e } /* There is no sense to check for `isActive` before doing `unlock`, because cancellation/completion might @@ -143,7 +141,8 @@ private class RxObservableCoroutine( else subscriber.onComplete() } catch (e: Throwable) { - handleCoroutineException(context, e, this) + // Unhandled exception (cannot handle in other way, since we are already complete) + handleCoroutineException(context, e) } } } finally { diff --git a/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt b/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt index 2891ea8532..bc60d8e80a 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt @@ -40,7 +40,6 @@ private class RxSingleCoroutine( parentContext: CoroutineContext, private val subscriber: SingleEmitter ) : AbstractCoroutine(parentContext, true) { - override val cancelsParent: Boolean get() = true override fun onCompleted(value: T) { if (!subscriber.isDisposed) subscriber.onSuccess(value) }