Skip to content

Commit 21f171c

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 b5d10d4 commit 21f171c

File tree

9 files changed

+194
-9
lines changed

9 files changed

+194
-9
lines changed

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

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

516+
public final class kotlinx/coroutines/experimental/SupervisorKt {
517+
public static final fun SupervisorJob (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
518+
public static synthetic fun SupervisorJob$default (Lkotlinx/coroutines/experimental/Job;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/Job;
519+
public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
520+
}
521+
516522
public abstract interface class kotlinx/coroutines/experimental/ThreadContextElement : kotlin/coroutines/experimental/CoroutineContext$Element {
517523
public abstract fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
518524
public abstract fun updateThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;)Ljava/lang/Object;

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ 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

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ object GlobalScope : CoroutineScope {
155155
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
156156
* context's [Job].
157157
*
158-
* This methods returns as soon as given block and all launched from within the scope children coroutines are completed.
158+
* This function is designed for a _parallel decomposition_ of work. When any child coroutine in this scope fails,
159+
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
160+
* This function returns as soon as given block and all its children coroutines are completed.
159161
* Example of the scope usages looks like this:
160162
*
161163
* ```

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import kotlin.coroutines.experimental.*
2323
* can [cancel] its own children (including all their children recursively) without cancelling itself.
2424
*
2525
* The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
26-
* `Job()` factory function.
26+
* `Job()` factory function. By default, a failure of a any of the job's children leads to an immediately failure
27+
* of its parent and cancellation of the rest of its children. This behavior can be customized using [SupervisorJob].
28+
*
2729
* Conceptually, an execution of the job does not produce a result value. Jobs are launched solely for their
2830
* side-effects. See [Deferred] interface for a job that produces a result.
2931
*
@@ -375,8 +377,16 @@ public interface Job : CoroutineContext.Element {
375377
}
376378

377379
/**
378-
* Creates a new job object in an _active_ state.
379-
* It is optionally a child of a [parent] job.
380+
* Creates a new job object in an active state.
381+
* A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
382+
*
383+
* To handle children failure independently of each other use [SupervisorJob].
384+
*
385+
* If [parent] job is specified, then this job becomes a child job of its parent and
386+
* is cancelled when its parent fails or is cancelled. All this job's children are cancelled in this case, too.
387+
* The invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent.
388+
*
389+
* @param parent an optional parent job.
380390
*/
381391
@Suppress("FunctionName")
382392
public fun Job(parent: Job? = null): Job = JobImpl(parent)

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -611,13 +611,13 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
611611
public override fun cancel(cause: Throwable?): Boolean =
612612
cancelImpl(cause) && handlesException
613613

614-
// parent is cancelling child
614+
// Parent is cancelling child
615615
public final override fun parentCancelled(parentJob: Job) {
616616
cancelImpl(parentJob)
617617
}
618618

619-
// child was cancelled with cause
620-
internal fun childCancelled(cause: Throwable): Boolean =
619+
// Child was cancelled with cause
620+
public open fun childCancelled(cause: Throwable): Boolean =
621621
cancelImpl(cause) && handlesException
622622

623623
// cause is Throwable or Job when cancelChild was invoked
@@ -1195,6 +1195,7 @@ private class Empty(override val isActive: Boolean) : Incomplete {
11951195

11961196
internal class JobImpl(parent: Job? = null) : JobSupport(true) {
11971197
init { initParentJobInternal(parent) }
1198+
override val cancelsParent: Boolean get() = true
11981199
override val onCancelComplete get() = true
11991200
override val handlesException: Boolean get() = false
12001201
}
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
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ class JobBasicCancellationTest : TestBase() {
125125
child.join()
126126
expect(3)
127127
}
128-
129128
parent.join()
130129
finish(4)
131130
}

0 commit comments

Comments
 (0)