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..6a5a4615f6 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -202,6 +202,7 @@ public final class kotlinx/coroutines/CoroutineScopeKt { public static final fun MainScope ()Lkotlinx/coroutines/CoroutineScope; public static final fun cancel (Lkotlinx/coroutines/CoroutineScope;)V public static final fun coroutineScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun ensureActive (Lkotlinx/coroutines/CoroutineScope;)V public static final fun isActive (Lkotlinx/coroutines/CoroutineScope;)Z public static final fun plus (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; } @@ -350,6 +351,8 @@ public final class kotlinx/coroutines/JobKt { public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;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 ensureActive (Lkotlin/coroutines/CoroutineContext;)V + public static final fun ensureActive (Lkotlinx/coroutines/Job;)V public static final fun isActive (Lkotlin/coroutines/CoroutineContext;)Z } diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 178163ca32..b88f5e8a05 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -209,3 +209,19 @@ public inline fun CoroutineScope.cancel() { val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this") job.cancel() } + +/** + * Ensures that current scope is [active][CoroutineScope.isActive]. + * Throws [IllegalStateException] if the context does not have a job in it. + * + * If the job is no longer active, throws [CancellationException]. + * If the job was cancelled, thrown exception contains the original cancellation cause. + * + * This method is a drop-in replacement for the following code, but with more precise exception: + * ``` + * if (!isActive) { + * throw CancellationException() + * } + * ``` + */ +public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index a428de8595..3fe1768657 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -536,6 +536,41 @@ public fun CoroutineContext.cancel(): Unit { this[Job]?.cancel() } +/** + * Ensures that current job is [active][Job.isActive]. + * If the job is no longer active, throws [CancellationException]. + * If the job was cancelled, thrown exception contains the original cancellation cause. + * + * This method is a drop-in replacement for the following code, but with more precise exception: + * ``` + * if (!job.isActive) { + * throw CancellationException() + * } + * ``` + */ +public fun Job.ensureActive(): Unit { + if (!isActive) throw getCancellationException() +} + +/** + * Ensures that job in the current context is [active][Job.isActive]. + * Throws [IllegalStateException] if the context does not have a job in it. + * + * If the job is no longer active, throws [CancellationException]. + * If the job was cancelled, thrown exception contains the original cancellation cause. + * + * This method is a drop-in replacement for the following code, but with more precise exception: + * ``` + * if (!isActive) { + * throw CancellationException() + * } + * ``` + */ +public fun CoroutineContext.ensureActive(): Unit { + val job = get(Job) ?: error("Context cannot be checked for liveness because it does not have a job: $this") + job.ensureActive() +} + /** * @suppress */ diff --git a/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt b/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt new file mode 100644 index 0000000000..716be629e8 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class EnsureActiveTest : TestBase() { + + private val job = Job() + private val scope = CoroutineScope(job + CoroutineExceptionHandler { _, _ -> }) + + @Test + fun testIsActive() = runTest { + expect(1) + scope.launch(Dispatchers.Unconfined) { + ensureActive() + coroutineContext.ensureActive() + coroutineContext[Job]!!.ensureActive() + expect(2) + delay(Long.MAX_VALUE) + } + + expect(3) + job.ensureActive() + scope.ensureActive() + scope.coroutineContext.ensureActive() + job.cancelAndJoin() + finish(4) + } + + @Test + fun testIsCompleted() = runTest { + expect(1) + scope.launch(Dispatchers.Unconfined) { + ensureActive() + coroutineContext.ensureActive() + coroutineContext[Job]!!.ensureActive() + expect(2) + } + + expect(3) + job.complete() + job.join() + assertFailsWith { job.ensureActive() } + assertFailsWith { scope.ensureActive() } + assertFailsWith { scope.coroutineContext.ensureActive() } + finish(4) + } + + + @Test + fun testIsCancelled() = runTest { + expect(1) + scope.launch(Dispatchers.Unconfined) { + ensureActive() + coroutineContext.ensureActive() + coroutineContext[Job]!!.ensureActive() + expect(2) + throw TestException() + } + + expect(3) + checkException { job.ensureActive() } + checkException { scope.ensureActive() } + checkException { scope.coroutineContext.ensureActive() } + finish(4) + } + + private inline fun checkException(block: () -> Unit) { + val result = runCatching(block) + val exception = result.exceptionOrNull() ?: fail() + assertTrue(exception is JobCancellationException) + assertTrue(exception.cause is TestException) + } +}