Skip to content

Commit

Permalink
Introduce SupervisorJob & supervisorScope
Browse files Browse the repository at this point in the history
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
  • Loading branch information
elizarov committed Sep 25, 2018
1 parent 541a9b6 commit e09da60
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ public final class kotlinx/coroutines/experimental/ScheduledKt {
public static synthetic fun withTimeoutOrNull$default (JLjava/util/concurrent/TimeUnit;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class kotlinx/coroutines/experimental/SupervisorKt {
public static final fun SupervisorJob (Lkotlinx/coroutines/experimental/Job;)Lkotlinx/coroutines/experimental/Job;
public static synthetic fun SupervisorJob$default (Lkotlinx/coroutines/experimental/Job;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/Job;
public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
}

public abstract interface class kotlinx/coroutines/experimental/ThreadContextElement : kotlin/coroutines/experimental/CoroutineContext$Element {
public abstract fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
public abstract fun updateThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
}

/**
* An optional element on the coroutine context to handle uncaught exceptions.
* An optional element in the coroutine context to handle uncaught exceptions.
*
* Normally, uncaught exceptions can only result from coroutines created using [launch][CoroutineScope.launch] builder.
* A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them
* in the resulting [Deferred] object.
*
* By default, when no handler is installed, uncaught exception are handled in the following way:
* * If exception is [CancellationException] then it is ignored
Expand Down
4 changes: 3 additions & 1 deletion common/kotlinx-coroutines-core-common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ object GlobalScope : CoroutineScope {
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* context's [Job].
*
* This methods returns as soon as given block and all launched from within the scope children coroutines are completed.
* This function is designed for a _parallel 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 given block and all its children coroutines are completed.
* Example of the scope usages looks like this:
*
* ```
Expand Down
16 changes: 13 additions & 3 deletions common/kotlinx-coroutines-core-common/src/Job.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import kotlin.coroutines.experimental.*
* can [cancel] its own children (including all their children recursively) without cancelling itself.
*
* The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
* `Job()` factory function.
* `Job()` factory function. By default, a failure of a any of the job's children leads to an immediately failure
* of its parent and cancellation of the rest of its children. This behavior can be customized using [SupervisorJob].
*
* Conceptually, an execution of the job does not produce a result value. Jobs are launched solely for their
* side-effects. See [Deferred] interface for a job that produces a result.
*
Expand Down Expand Up @@ -375,8 +377,16 @@ public interface Job : CoroutineContext.Element {
}

/**
* Creates a new job object in an _active_ state.
* It is optionally a child of a [parent] job.
* Creates a new job object in an active state.
* A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
*
* To handle children failure independently of each other use [SupervisorJob].
*
* If [parent] job is specified, then this job becomes a child job of its parent and
* is cancelled when its parent fails or is cancelled. All this job's children are cancelled in this case, too.
* The invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent.
*
* @param parent an optional parent job.
*/
@Suppress("FunctionName")
public fun Job(parent: Job? = null): Job = JobImpl(parent)
Expand Down
7 changes: 4 additions & 3 deletions common/kotlinx-coroutines-core-common/src/JobSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -607,13 +607,13 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
public override fun cancel(cause: Throwable?): Boolean =
cancelImpl(cause) && handlesException

// parent is cancelling child
// Parent is cancelling child
public final override fun parentCancelled(parentJob: Job) {
cancelImpl(parentJob)
}

// child was cancelled with cause
internal fun childCancelled(cause: Throwable): Boolean =
// Child was cancelled with cause
public open fun childCancelled(cause: Throwable): Boolean =
cancelImpl(cause) && handlesException

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

internal class JobImpl(parent: Job? = null) : JobSupport(true) {
init { initParentJobInternal(parent) }
override val cancelsParent: Boolean get() = true
override val onCancelComplete get() = true
override val handlesException: Boolean get() = false
}
Expand Down
67 changes: 67 additions & 0 deletions common/kotlinx-coroutines-core-common/src/Supervisor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.experimental

import kotlin.coroutines.experimental.*

/**
* Creates a new _supervisor_ job object in an active state.
* Children of a supervisor job can fail independently of each other.
*
* A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children,
* so a supervisor can implement a custom policy for handling failures of its children:
*
* * 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
* of [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
*
* @param parent an optional parent job.
*/
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : Job = SupervisorJobImpl(parent)

/**
* Creates new [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].
*
* 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 details.
*/
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
// todo: optimize implementation to a single allocated object
// todo: fix copy-and-paste with coroutineScope
val owner = SupervisorCoroutine<R>(coroutineContext)
owner.start(CoroutineStart.UNDISPATCHED, owner, block)
owner.join()
if (owner.isCancelled) {
throw owner.getCancellationException().let { it.cause ?: it }
}
val state = owner.state
if (state is CompletedExceptionally) {
throw state.cause
}
@Suppress("UNCHECKED_CAST")
return state as R

}

private class SupervisorJobImpl(parent: Job?) : JobSupport(true) {
init { initParentJobInternal(parent) }
override val cancelsParent: Boolean get() = true
override val onCancelComplete get() = true
override val handlesException: Boolean get() = false
override fun childCancelled(cause: Throwable): Boolean = false
}

private class SupervisorCoroutine<R>(
parentContext: CoroutineContext
) : AbstractCoroutine<R>(parentContext, true) {
override val cancelsParent: Boolean get() = true
override fun childCancelled(cause: Throwable): Boolean = false
}
20 changes: 20 additions & 0 deletions common/kotlinx-coroutines-core-common/test/JobTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,24 @@ class JobTest : TestBase() {
deferred.join()
finish(3)
}

@Test
fun testJobWithParentCancelNormally() {
val parent = Job()
val job = Job(parent)
job.cancel()
assertTrue(job.isCancelled)
assertFalse(parent.isCancelled)
}

@Test
fun testJobWithParentCancelException() {
val parent = Job()
val job = Job(parent)
job.cancel(TestException())
assertTrue(job.isCancelled)
assertTrue(parent.isCancelled)
}

private class TestException : Exception()
}
76 changes: 76 additions & 0 deletions common/kotlinx-coroutines-core-common/test/SupervisorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913

package kotlinx.coroutines.experimental

import kotlin.test.*

class SupervisorTest : TestBase() {
@Test
fun testSupervisorJob() = runTest(
unhandled = listOf(
{ it -> it is TestException2 },
{ it -> it is TestException1 }
)
) {
expect(1)
val supervisor = SupervisorJob()
val job1 = launch(supervisor + CoroutineName("job1")) {
expect(2)
yield() // to second child
expect(4)
throw TestException1()
}
val job2 = launch(supervisor + CoroutineName("job2")) {
expect(3)
throw TestException2()
}
joinAll(job1, job2)
finish(5)
assertTrue(job1.isCancelled)
assertTrue(job2.isCancelled)
}

@Test
fun testSupervisorScope() = runTest(
unhandled = listOf(
{ it -> it is TestException1 },
{ it -> it is TestException2 }
)
) {
val result = supervisorScope {
launch {
throw TestException1()
}
launch {
throw TestException2()
}
"OK"
}
assertEquals("OK", result)
}

@Test
fun testSupervisorWithParentCancelNormally() {
val parent = Job()
val supervisor = SupervisorJob(parent)
supervisor.cancel()
assertTrue(supervisor.isCancelled)
assertFalse(parent.isCancelled)
}

@Test
fun testSupervisorWithParentCancelException() {
val parent = Job()
val supervisor = SupervisorJob(parent)
supervisor.cancel(TestException1())
assertTrue(supervisor.isCancelled)
assertTrue(parent.isCancelled)
}

private class TestException1 : Exception()
private class TestException2 : Exception()
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,10 @@ class JobBasicCancellationTest : TestBase() {
expect(1)
val child = Job(coroutineContext[Job])
expect(2)
assertFalse(child.cancel(IOException()))
assertFalse(child.cancel())
child.join()
assertTrue(child.getCancellationException().cause is IOException)
expect(3)
}

parent.join()
finish(4)
}
Expand Down

0 comments on commit e09da60

Please sign in to comment.