Skip to content

Commit b9773ec

Browse files
committed
Introduce SupervisorJob & supervisorScope
Fixes #576
1 parent 33e7ce4 commit b9773ec

File tree

6 files changed

+149
-13
lines changed

6 files changed

+149
-13
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+6
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,12 @@ public final class kotlinx/coroutines/experimental/ScheduledKt {
486486
public static synthetic fun withTimeoutOrNull$default (JLjava/util/concurrent/TimeUnit;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
487487
}
488488

489+
public final class kotlinx/coroutines/experimental/SupervisorKt {
490+
public static final fun SupervisorJob (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
491+
public static synthetic fun SupervisorJob$default (Lkotlinx/coroutines/experimental/Job;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/Job;
492+
public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
493+
}
494+
489495
public abstract interface class kotlinx/coroutines/experimental/ThreadContextElement : kotlin/coroutines/experimental/CoroutineContext$Element {
490496
public abstract fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
491497
public abstract fun updateThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;)Ljava/lang/Object;

common/kotlinx-coroutines-core-common/src/CoroutineExceptionHandler.kt

+9-8
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,18 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
6969
}
7070

7171
/**
72-
* An optional element on the coroutine context to handle uncaught exceptions.
72+
* An optional element in the coroutine context to handle uncaught exceptions.
73+
*
74+
* Normally, uncaught exceptions can only result from coroutines created using [launch][CoroutineScope.launch] builder.
75+
* A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them
76+
* in the resulting [Deferred] object.
7377
*
7478
* By default, when no handler is installed, uncaught exception are handled in the following way:
7579
* * If exception is [CancellationException] then it is ignored
76-
* (because that is the supposed mechanism to cancel the running coroutine)
77-
* * Otherwise:
78-
* * if there is a [Job] in the context, then [Job.cancel] is invoked;
79-
* * all instances of [CoroutineExceptionHandler] found via [ServiceLoader] are invoked;
80-
* * and current thread's [Thread.uncaughtExceptionHandler] is invoked.
81-
*
82-
* See [handleCoroutineException].
80+
* (because that is the supposed mechanism to cancel the running coroutine);
81+
* * Otherwise, if there is a [Job] in the context, then [Job.cancel] is invoked;
82+
* * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] are invoked,
83+
* and current thread's [Thread.uncaughtExceptionHandler] is invoked.
8384
*/
8485
public interface CoroutineExceptionHandler : CoroutineContext.Element {
8586
/**

common/kotlinx-coroutines-core-common/src/CoroutineScope.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ object GlobalScope : CoroutineScope {
146146
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
147147
* context's [Job].
148148
*
149-
* This methods returns as soon as given block and all launched from within the scope children coroutines are completed.
149+
* This function is designed for a _parallel decomposition_ of work. When any child coroutine in this scope fails,
150+
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
151+
* This function returns as soon as given block and all its children coroutines are completed.
150152
* Example of the scope usages looks like this:
151153
*
152154
* ```
@@ -169,7 +171,7 @@ object GlobalScope : CoroutineScope {
169171
* 2) If `doSomeWork` throws an exception, then `async` task is cancelled and `loadDataForUI` rethrows that exception.
170172
* 3) If outer scope of `loadDataForUI` is cancelled, both started `async` and `withContext` are cancelled.
171173
*
172-
* Method may throw [JobCancellationException] if the current job was cancelled externally
174+
* Method may throw [CancellationException] if the current job was cancelled externally
173175
* or may throw the corresponding unhandled [Throwable] if there is any unhandled exception in this scope
174176
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
175177
*/

common/kotlinx-coroutines-core-common/src/Job.kt

+10-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import kotlin.coroutines.experimental.*
1919
* of parent immediately cancels all its [children].
2020
*
2121
* The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
22-
* `Job()` factory function.
22+
* `Job()` factory function. By default, a failure of a any of the job's children leads to an immediately failure
23+
* of its parent and cancellation of the rest of its children. This behavior can be customized using [SupervisorJob].
24+
*
2325
* Conceptually, an execution of the job does not produce a result value. Jobs are launched solely for their
2426
* side-effects. See [Deferred] interface for a job that produces a result.
2527
*
@@ -369,8 +371,13 @@ public interface Job : CoroutineContext.Element {
369371
}
370372

371373
/**
372-
* Creates a new job object in an _active_ state.
373-
* It is optionally a child of a [parent] job.
374+
* Creates a new job object in an active state.
375+
* A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
376+
*
377+
* To handle children failure independently of each other use [SupervisorJob].
378+
*
379+
* @param parent an optional parent job. If specified, this job becomes a child job of its parent and
380+
* is cancelled when its parent fails or is cancelled.
374381
*/
375382
@Suppress("FunctionName")
376383
public fun Job(parent: Job? = null): Job = JobImpl(parent)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.experimental
6+
7+
import kotlin.coroutines.experimental.*
8+
9+
/**
10+
* Creates a new _supervisor_ job object in an active state.
11+
* Children of a supervisor job can fail independently of each other.
12+
*
13+
* A failure of a child does not cause the supervisor job to fail and does not affect its other children,
14+
* so a supervisor can implement a custom policy for handling failures of its children:
15+
*
16+
* * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context.
17+
* * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value.
18+
*
19+
* @param parent an optional parent job. If specified, this supervisor job becomes a child job of its parent and
20+
* is cancelled when its parent fails or is cancelled.
21+
*/
22+
@Suppress("FunctionName")
23+
public fun SupervisorJob(parent: Job? = null) : Job = SupervisorJobImpl(parent)
24+
25+
/**
26+
* Creates new [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
27+
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
28+
* context's [Job] with [SupervisorJob].
29+
*
30+
* A failure of a child does not cause this scope to fail and does not affect its other children,
31+
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
32+
*/
33+
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
34+
// todo: optimize implementation to a single allocated object
35+
// todo: fix copy-and-paste with coroutineScope
36+
val owner = SupervisorCoroutine<R>(coroutineContext)
37+
owner.start(CoroutineStart.UNDISPATCHED, owner, block)
38+
owner.join()
39+
if (owner.isCancelled) {
40+
throw owner.getCancellationException().let { it.cause ?: it }
41+
}
42+
val state = owner.state
43+
if (state is CompletedExceptionally) {
44+
throw state.cause
45+
}
46+
@Suppress("UNCHECKED_CAST")
47+
return state as R
48+
49+
}
50+
51+
private class SupervisorJobImpl(parent: Job?) : JobSupport(true) {
52+
init { initParentJobInternal(parent) }
53+
override val onFailComplete get() = true
54+
override val handlesException: Boolean get() = false
55+
override fun childFailed(cause: Throwable): Boolean = false
56+
}
57+
58+
private class SupervisorCoroutine<R>(
59+
parentContext: CoroutineContext
60+
) : AbstractCoroutine<R>(parentContext, true) {
61+
override fun childFailed(cause: Throwable): Boolean = false
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913
6+
7+
package kotlinx.coroutines.experimental
8+
9+
import kotlin.test.*
10+
11+
class SupervisorTest : TestBase() {
12+
@Test
13+
fun testSupervisorJob() = runTest(
14+
unhandled = listOf(
15+
{ it -> it is TestException2 },
16+
{ it -> it is TestException1 }
17+
)
18+
) {
19+
expect(1)
20+
val supervisor = SupervisorJob()
21+
val job1 = launch(supervisor + CoroutineName("job1")) {
22+
expect(2)
23+
yield() // to second child
24+
expect(4)
25+
throw TestException1()
26+
}
27+
val job2 = launch(supervisor + CoroutineName("job2")) {
28+
expect(3)
29+
throw TestException2()
30+
}
31+
joinAll(job1, job2)
32+
finish(5)
33+
assertTrue(job1.isFailed)
34+
assertTrue(job2.isFailed)
35+
}
36+
37+
@Test
38+
fun testSupervisorScope() = runTest(
39+
unhandled = listOf(
40+
{ it -> it is TestException1 },
41+
{ it -> it is TestException2 }
42+
)
43+
) {
44+
val result = supervisorScope {
45+
launch {
46+
throw TestException1()
47+
}
48+
launch {
49+
throw TestException2()
50+
}
51+
"OK"
52+
}
53+
assertEquals("OK", result)
54+
}
55+
56+
private class TestException1 : Exception()
57+
private class TestException2 : Exception()
58+
}

0 commit comments

Comments
 (0)