From 6ffd983354946e41d409d374a5f686739d4af75d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sun, 23 Sep 2018 19:16:04 +0300 Subject: [PATCH] Introduce SupervisorJob & supervisorScope Fixes #576 --- .../kotlinx-coroutines-core.txt | 6 ++ .../src/CoroutineExceptionHandler.kt | 17 ++--- .../src/CoroutineScope.kt | 6 +- .../kotlinx-coroutines-core-common/src/Job.kt | 13 +++- .../src/JobSupport.kt | 4 +- .../src/Supervisor.kt | 62 +++++++++++++++++++ .../test/SupervisorTest.kt | 58 +++++++++++++++++ 7 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 common/kotlinx-coroutines-core-common/src/Supervisor.kt create mode 100644 common/kotlinx-coroutines-core-common/test/SupervisorTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 67a48629ef..69c76917c6 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -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; diff --git a/common/kotlinx-coroutines-core-common/src/CoroutineExceptionHandler.kt b/common/kotlinx-coroutines-core-common/src/CoroutineExceptionHandler.kt index cba8439e06..e95da5e7aa 100644 --- a/common/kotlinx-coroutines-core-common/src/CoroutineExceptionHandler.kt +++ b/common/kotlinx-coroutines-core-common/src/CoroutineExceptionHandler.kt @@ -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 { /** diff --git a/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt b/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt index c1f394fc2e..be435c2587 100644 --- a/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt +++ b/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt @@ -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: * * ``` @@ -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). */ diff --git a/common/kotlinx-coroutines-core-common/src/Job.kt b/common/kotlinx-coroutines-core-common/src/Job.kt index 79568e2ad6..87feb74906 100644 --- a/common/kotlinx-coroutines-core-common/src/Job.kt +++ b/common/kotlinx-coroutines-core-common/src/Job.kt @@ -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. * @@ -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) diff --git a/common/kotlinx-coroutines-core-common/src/JobSupport.kt b/common/kotlinx-coroutines-core-common/src/JobSupport.kt index 00f87df51e..269e7afceb 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -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 diff --git a/common/kotlinx-coroutines-core-common/src/Supervisor.kt b/common/kotlinx-coroutines-core-common/src/Supervisor.kt new file mode 100644 index 0000000000..02f4099c34 --- /dev/null +++ b/common/kotlinx-coroutines-core-common/src/Supervisor.kt @@ -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 supervisorScope(block: suspend CoroutineScope.() -> R): R { + // todo: optimize implementation to a single allocated object + // todo: fix copy-and-paste with coroutineScope + val owner = SupervisorCoroutine(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( + parentContext: CoroutineContext +) : AbstractCoroutine(parentContext, true) { + override fun childFailed(cause: Throwable): Boolean = false +} diff --git a/common/kotlinx-coroutines-core-common/test/SupervisorTest.kt b/common/kotlinx-coroutines-core-common/test/SupervisorTest.kt new file mode 100644 index 0000000000..8d8fdf5648 --- /dev/null +++ b/common/kotlinx-coroutines-core-common/test/SupervisorTest.kt @@ -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() +} \ No newline at end of file