Skip to content

Commit

Permalink
Revisit SupervisorScope, supervisorScope, and coroutineScope docs
Browse files Browse the repository at this point in the history
Seems like a lot of the information was outdated.

Fixes #3725
  • Loading branch information
dkhalanskyjb committed Feb 15, 2024
1 parent 17bae3f commit 1d04452
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 18 deletions.
14 changes: 7 additions & 7 deletions kotlinx-coroutines-core/common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
Expand Down
18 changes: 8 additions & 10 deletions kotlinx-coroutines-core/common/src/Supervisor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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].
Expand Down
33 changes: 32 additions & 1 deletion kotlinx-coroutines-core/common/test/SupervisorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -96,9 +98,13 @@ class SupervisorTest : TestBase() {

expect(2)
yield()
supervisorJob = coroutineContext.job
throw TestException2()
}
} catch (e: Throwable) {
assertIs<TestException2>(e)
assertTrue(childJob!!.isCancelled)
assertTrue(supervisorJob!!.isCancelled)
finish(4)
}
}
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 1d04452

Please sign in to comment.