Skip to content

Commit 612f5cc

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 1e0a2f0 commit 612f5cc

File tree

9 files changed

+195
-11
lines changed

9 files changed

+195
-11
lines changed

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

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

514+
public final class kotlinx/coroutines/experimental/SupervisorKt {
515+
public static final fun SupervisorJob (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
516+
public static synthetic fun SupervisorJob$default (Lkotlinx/coroutines/experimental/Job;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/Job;
517+
public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
518+
}
519+
514520
public abstract interface class kotlinx/coroutines/experimental/ThreadContextElement : kotlin/coroutines/experimental/CoroutineContext$Element {
515521
public abstract fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
516522
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
@@ -69,7 +69,11 @@ 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

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

+3-1
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
* ```

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
*
@@ -359,8 +361,16 @@ public interface Job : CoroutineContext.Element {
359361
}
360362

361363
/**
362-
* Creates a new job object in an _active_ state.
363-
* It is optionally a child of a [parent] job.
364+
* Creates a new job object in an active state.
365+
* A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
366+
*
367+
* To handle children failure independently of each other use [SupervisorJob].
368+
*
369+
* If [parent] job is specified, then this job becomes a child job of its parent and
370+
* is cancelled when its parent fails or is cancelled. All this job's children are cancelled in this case, too.
371+
* The invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent.
372+
*
373+
* @param parent an optional parent job.
364374
*/
365375
@Suppress("FunctionName")
366376
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
@@ -607,13 +607,13 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
607607
public override fun cancel(cause: Throwable?): Boolean =
608608
cancelImpl(cause) && handlesException
609609

610-
// parent is reporting failure to a child child
610+
// Parent is reporting failure to a child child
611611
public final override fun parentCancelled(parentJob: Job) {
612612
cancelImpl(parentJob)
613613
}
614614

615-
// child was cancelled with cause
616-
internal fun childCancelled(cause: Throwable): Boolean =
615+
// Child was cancelled with cause
616+
public open fun childCancelled(cause: Throwable): Boolean =
617617
cancelImpl(cause) && handlesException
618618

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

11661166
internal class JobImpl(parent: Job? = null) : JobSupport(true) {
11671167
init { initParentJobInternal(parent) }
1168+
override val cancelsParent: Boolean get() = true
11681169
override val onCancelComplete get() = true
11691170
override val handlesException: Boolean get() = false
11701171
}
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)