From 1d044524c6be8ec7d9d3ca6ceb04d20980b9797c Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 12 Feb 2024 15:56:44 +0100 Subject: [PATCH] Revisit SupervisorScope, supervisorScope, and coroutineScope docs Seems like a lot of the information was outdated. Fixes #3725 --- .../common/src/CoroutineScope.kt | 14 ++++---- .../common/src/Supervisor.kt | 18 +++++----- .../common/test/SupervisorTest.kt | 33 ++++++++++++++++++- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 6b045d20b4..2d37d15bb7 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -220,13 +220,13 @@ public object GlobalScope : CoroutineScope { /** * Creates a [CoroutineScope] and calls the specified suspend block with this scope. - * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides - * the context's [Job]. + * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, using the + * [Job] from that context as the parent for a new [Job]. * * This function is designed for _concurrent decomposition_ of work. When any child coroutine in this scope fails, - * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]). - * This function returns as soon as the given block and all its children coroutines are completed. - * A usage example of a scope looks like this: + * this scope fails, cancelling all the other children (for a different behavior, see [supervisorScope]). + * This function returns as soon as the given block and all its child coroutines are completed. + * A usage of a scope looks like this: * * ``` * suspend fun showSomeData() = coroutineScope { @@ -248,8 +248,8 @@ public object GlobalScope : CoroutineScope { * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled. * 4) If the `async` block fails, `withContext` will be cancelled. * - * The method may throw a [CancellationException] if the current job was cancelled externally - * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope + * The method may throw a [CancellationException] if the current job was cancelled externally, + * rethrow the exception thrown by [block], or throw an unhandled [Throwable] if there is one * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope). */ public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R { diff --git a/kotlinx-coroutines-core/common/src/Supervisor.kt b/kotlinx-coroutines-core/common/src/Supervisor.kt index 8036b1741e..730050b5ab 100644 --- a/kotlinx-coroutines-core/common/src/Supervisor.kt +++ b/kotlinx-coroutines-core/common/src/Supervisor.kt @@ -20,11 +20,8 @@ import kotlin.jvm.* * - A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context. * - A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value. * - * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its - * parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of - * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent. - * - * @param parent an optional parent job. + * If a [parent] job is specified, then this supervisor job becomes a child job of [parent] and is cancelled when the + * parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. */ @Suppress("FunctionName") public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent) @@ -36,15 +33,16 @@ public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobIm public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent) /** - * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope. - * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides - * context's [Job] with [SupervisorJob]. + * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend [block] with this scope. + * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, using the + * [Job] from that context as the parent for the new [SupervisorJob]. * This function returns as soon as the given block and all its child coroutines are completed. * * Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children, * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details. - * A failure of the scope itself (exception thrown in the [block] or external cancellation) fails the scope with all its children, - * but does not cancel parent job. + * + * If an exception happened in [block], then the supervisor job is failed and all its children are cancelled. + * If the current coroutine was cancelled, then both the supervisor job itself and all its children are cancelled. * * The method may throw a [CancellationException] if the current job was cancelled externally, * or rethrow an exception thrown by the given [block]. diff --git a/kotlinx-coroutines-core/common/test/SupervisorTest.kt b/kotlinx-coroutines-core/common/test/SupervisorTest.kt index 8866f80432..1fb0ff9a06 100644 --- a/kotlinx-coroutines-core/common/test/SupervisorTest.kt +++ b/kotlinx-coroutines-core/common/test/SupervisorTest.kt @@ -83,10 +83,12 @@ class SupervisorTest : TestBase() { @Test fun testThrowingSupervisorScope() = runTest { + var childJob: Job? = null + var supervisorJob: Job? = null try { expect(1) supervisorScope { - async { + childJob = async { try { delay(Long.MAX_VALUE) } finally { @@ -96,9 +98,13 @@ class SupervisorTest : TestBase() { expect(2) yield() + supervisorJob = coroutineContext.job throw TestException2() } } catch (e: Throwable) { + assertIs(e) + assertTrue(childJob!!.isCancelled) + assertTrue(supervisorJob!!.isCancelled) finish(4) } } @@ -155,6 +161,31 @@ class SupervisorTest : TestBase() { } } + /** + * Tests that [supervisorScope] cancels all its children when the current coroutine is cancelled. + */ + @Test + fun testSupervisorScopeExternalCancellation() = runTest { + var childJob: Job? = null + val job = launch { + supervisorScope { + childJob = launch(start = CoroutineStart.UNDISPATCHED) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(2) + } + } + } + } + while (childJob == null) yield() + expect(1) + job.cancel() + assertTrue(childJob!!.isCancelled) + job.join() + finish(3) + } + @Test fun testAsyncCancellation() = runTest { val parent = SupervisorJob()