Skip to content

Commit 100c204

Browse files
committed
Introduce SupervisorJob & supervisorScope
This change also fixes propagation of cancellation for Job() constructor. When both Job() and SupervisorJob() are cancelled with exception (fail), they cancel their parent, too. So we have similar behavior between: * Job() and coroutineScope { ... } * SupervisorJob() and supervisorScope { ... } Fixes #576
1 parent 6685fd0 commit 100c204

File tree

9 files changed

+207
-20
lines changed

9 files changed

+207
-20
lines changed

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

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

481+
public final class kotlinx/coroutines/experimental/SupervisorKt {
482+
public static final fun SupervisorJob (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
483+
public static synthetic fun SupervisorJob$default (Lkotlinx/coroutines/experimental/Job;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/Job;
484+
public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
485+
}
486+
481487
public abstract interface class kotlinx/coroutines/experimental/ThreadContextElement : kotlin/coroutines/experimental/CoroutineContext$Element {
482488
public abstract fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
483489
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
@@ -68,17 +68,18 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
6868
}
6969

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

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import kotlin.coroutines.experimental.*
1919
* of parent lead to an immediate cancellation of all its [children] and vice versa.
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
*
@@ -346,8 +348,16 @@ public interface Job : CoroutineContext.Element {
346348
}
347349

348350
/**
349-
* Creates a new job object in an _active_ state.
350-
* It is optionally a child of a [parent] job.
351+
* Creates a new job object in an active state.
352+
* A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
353+
*
354+
* To handle children failure independently of each other use [SupervisorJob].
355+
*
356+
* If [parent] job is specified, then this job becomes a child job of its parent and
357+
* is cancelled when its parent fails or is cancelled. All this job's children are cancelled in this case, too.
358+
* The invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent.
359+
*
360+
* @param parent an optional parent job.
351361
*/
352362
@Suppress("FunctionName")
353363
public fun Job(parent: Job? = null): Job = JobImpl(parent)

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -603,13 +603,19 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
603603
public override fun cancel(cause: Throwable?): Boolean =
604604
cancelImpl(cause) && handlesException
605605

606-
// parent is reporting failure to a child child
606+
/**
607+
* Parent is reporting failure to a child child
608+
* @suppress **This is internal API and it is subject to change.**
609+
*/
607610
public final override fun parentCancelled(parentJob: Job) {
608611
cancelImpl(parentJob)
609612
}
610613

611-
// child was cancelled with cause
612-
internal fun childCancelled(cause: Throwable): Boolean =
614+
/**
615+
* Child was cancelled with cause.
616+
* @suppress **This is internal API and it is subject to change.**
617+
*/
618+
public open fun childCancelled(cause: Throwable): Boolean =
613619
cancelImpl(cause) && handlesException
614620

615621
// cause is Throwable or Job when cancelChild was invoked
@@ -693,7 +699,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
693699

694700
// Performs promotion of incomplete coroutine state to NodeList for the purpose of
695701
// converting coroutine state to Failing, returns null when need to retry
696-
private fun getOrPromoteFailingList(state: Incomplete): NodeList? = state.list ?:
702+
private fun getOrPromoteFailingList(state: Incomplete): NodeList? = state.list ?:
697703
when (state) {
698704
is Empty -> NodeList() // we can allocate new empty list that'll get integrated into Failing state
699705
is JobNode<*> -> {
@@ -1161,6 +1167,7 @@ private class Empty(override val isActive: Boolean) : Incomplete {
11611167

11621168
internal class JobImpl(parent: Job? = null) : JobSupport(true) {
11631169
init { initParentJobInternal(parent) }
1170+
override val cancelsParent: Boolean get() = true
11641171
override val onCancelComplete get() = true
11651172
override val handlesException: Boolean get() = false
11661173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 or cancellation 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+
* If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its
20+
* parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of
21+
* of [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
22+
*
23+
* @param parent an optional parent job.
24+
*/
25+
@Suppress("FunctionName")
26+
public fun SupervisorJob(parent: Job? = null) : Job = SupervisorJobImpl(parent)
27+
28+
/**
29+
* Creates new [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
30+
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
31+
* context's [Job] with [SupervisorJob].
32+
*
33+
* A failure of a child does not cause this scope to fail and does not affect its other children,
34+
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
35+
*/
36+
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
37+
// todo: optimize implementation to a single allocated object
38+
// todo: fix copy-and-paste with coroutineScope
39+
val owner = SupervisorCoroutine<R>(coroutineContext)
40+
owner.start(CoroutineStart.UNDISPATCHED, owner, block)
41+
owner.join()
42+
if (owner.isCancelled) {
43+
throw owner.getCancellationException().let { it.cause ?: it }
44+
}
45+
val state = owner.state
46+
if (state is CompletedExceptionally) {
47+
throw state.cause
48+
}
49+
@Suppress("UNCHECKED_CAST")
50+
return state as R
51+
52+
}
53+
54+
private class SupervisorJobImpl(parent: Job?) : JobSupport(true) {
55+
init { initParentJobInternal(parent) }
56+
override val cancelsParent: Boolean get() = true
57+
override val onCancelComplete get() = true
58+
override val handlesException: Boolean get() = false
59+
override fun childCancelled(cause: Throwable): Boolean = false
60+
}
61+
62+
private class SupervisorCoroutine<R>(
63+
parentContext: CoroutineContext
64+
) : AbstractCoroutine<R>(parentContext, true) {
65+
override val cancelsParent: Boolean get() = true
66+
override fun childCancelled(cause: Throwable): Boolean = false
67+
}

common/kotlinx-coroutines-core-common/test/JobTest.kt

+20
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,24 @@ class JobTest : TestBase() {
188188
deferred.join()
189189
finish(3)
190190
}
191+
192+
@Test
193+
fun testJobWithParentCancelNormally() {
194+
val parent = Job()
195+
val job = Job(parent)
196+
job.cancel()
197+
assertTrue(job.isCancelled)
198+
assertFalse(parent.isCancelled)
199+
}
200+
201+
@Test
202+
fun testJobWithParentCancelException() {
203+
val parent = Job()
204+
val job = Job(parent)
205+
job.cancel(TestException())
206+
assertTrue(job.isCancelled)
207+
assertTrue(parent.isCancelled)
208+
}
209+
210+
private class TestException : Exception()
191211
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.isCancelled)
34+
assertTrue(job2.isCancelled)
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+
@Test
57+
fun testSupervisorWithParentCancelNormally() {
58+
val parent = Job()
59+
val supervisor = SupervisorJob(parent)
60+
supervisor.cancel()
61+
assertTrue(supervisor.isCancelled)
62+
assertFalse(parent.isCancelled)
63+
}
64+
65+
@Test
66+
fun testSupervisorWithParentCancelException() {
67+
val parent = Job()
68+
val supervisor = SupervisorJob(parent)
69+
supervisor.cancel(TestException1())
70+
assertTrue(supervisor.isCancelled)
71+
assertTrue(parent.isCancelled)
72+
}
73+
74+
private class TestException1 : Exception()
75+
private class TestException2 : Exception()
76+
}

core/kotlinx-coroutines-core/test/exceptions/JobBasicCancellationTest.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,10 @@ class JobBasicCancellationTest : TestBase() {
121121
expect(1)
122122
val child = Job(coroutineContext[Job])
123123
expect(2)
124-
assertFalse(child.cancel(IOException()))
124+
assertFalse(child.cancel())
125125
child.join()
126-
assertTrue(child.getCancellationException().cause is IOException)
127126
expect(3)
128127
}
129-
130128
parent.join()
131129
finish(4)
132130
}

0 commit comments

Comments
 (0)