Skip to content

Commit

Permalink
Introduce SupervisorJob & supervisorScope
Browse files Browse the repository at this point in the history
Fixes #576
  • Loading branch information
elizarov committed Sep 23, 2018
1 parent 33e7ce4 commit 6ffd983
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,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 @@ -69,17 +69,18 @@ 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
* (because that is the supposed mechanism to cancel the running coroutine)
* * Otherwise:
* * if there is a [Job] in the context, then [Job.cancel] is invoked;
* * all instances of [CoroutineExceptionHandler] found via [ServiceLoader] are invoked;
* * and current thread's [Thread.uncaughtExceptionHandler] is invoked.
*
* See [handleCoroutineException].
* (because that is the supposed mechanism to cancel the running coroutine);
* * Otherwise, if there is a [Job] in the context, then [Job.cancel] is invoked;
* * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] are invoked,
* and current thread's [Thread.uncaughtExceptionHandler] is invoked.
*/
public interface CoroutineExceptionHandler : CoroutineContext.Element {
/**
Expand Down
6 changes: 4 additions & 2 deletions 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 All @@ -169,7 +171,7 @@ object GlobalScope : CoroutineScope {
* 2) If `doSomeWork` throws an exception, then `async` task is cancelled and `loadDataForUI` rethrows that exception.
* 3) If outer scope of `loadDataForUI` is cancelled, both started `async` and `withContext` are cancelled.
*
* Method may throw [JobCancellationException] if the current job was cancelled externally
* Method may throw [CancellationException] if the current job was cancelled externally
* or may throw the corresponding unhandled [Throwable] if there is any unhandled exception in this scope
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
*/
Expand Down
13 changes: 10 additions & 3 deletions common/kotlinx-coroutines-core-common/src/Job.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import kotlin.coroutines.experimental.*
* of parent immediately cancels all its [children].
*
* 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 @@ -369,8 +371,13 @@ 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].
*
* @param parent an optional parent job. If specified, this job becomes a child job of its parent and
* is cancelled when its parent fails or is cancelled.
*/
@Suppress("FunctionName")
public fun Job(parent: Job? = null): Job = JobImpl(parent)
Expand Down
4 changes: 2 additions & 2 deletions common/kotlinx-coroutines-core-common/src/JobSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,8 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
public override fun cancel(cause: Throwable?): Boolean =
fail(cause, cancel = true) && handlesException

// child is reporting failure to the parent
internal fun childFailed(cause: Throwable) =
// child is reporting failure to the parent (supervisor jobs override to ignore it)
internal open fun childFailed(cause: Throwable) =
fail(cause, cancel = false) && handlesException

// parent is cancelling child
Expand Down
62 changes: 62 additions & 0 deletions common/kotlinx-coroutines-core-common/src/Supervisor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 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.
*
* @param parent an optional parent job. If specified, this supervisor job becomes a child job of its parent and
* is cancelled when its parent fails or is cancelled.
*/
@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 onFailComplete get() = true
override val handlesException: Boolean get() = false
override fun childFailed(cause: Throwable): Boolean = false
}

private class SupervisorCoroutine<R>(
parentContext: CoroutineContext
) : AbstractCoroutine<R>(parentContext, true) {
override fun childFailed(cause: Throwable): Boolean = false
}
58 changes: 58 additions & 0 deletions common/kotlinx-coroutines-core-common/test/SupervisorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.isFailed)
assertTrue(job2.isFailed)
}

@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)
}

private class TestException1 : Exception()
private class TestException2 : Exception()
}

0 comments on commit 6ffd983

Please sign in to comment.