diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 01f236fef6..457ede648f 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -360,6 +360,7 @@ public abstract interface class kotlinx/coroutines/Job : kotlin/coroutines/Corou public abstract fun getCancellationException ()Ljava/util/concurrent/CancellationException; public abstract fun getChildren ()Lkotlin/sequences/Sequence; public abstract fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; + public abstract fun getParent ()Lkotlinx/coroutines/Job; public abstract fun invokeOnCompletion (Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public abstract fun invokeOnCompletion (ZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public abstract fun isActive ()Z @@ -443,6 +444,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public final fun getCompletionExceptionOrNull ()Ljava/lang/Throwable; public final fun getKey ()Lkotlin/coroutines/CoroutineContext$Key; public final fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; + public fun getParent ()Lkotlinx/coroutines/Job; protected fun handleJobException (Ljava/lang/Throwable;)Z protected final fun initParentJob (Lkotlinx/coroutines/Job;)V public final fun invokeOnCompletion (Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; @@ -485,6 +487,7 @@ public final class kotlinx/coroutines/NonCancellable : kotlin/coroutines/Abstrac public fun getCancellationException ()Ljava/util/concurrent/CancellationException; public fun getChildren ()Lkotlin/sequences/Sequence; public fun getOnJoin ()Lkotlinx/coroutines/selects/SelectClause0; + public fun getParent ()Lkotlinx/coroutines/Job; public fun invokeOnCompletion (Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public fun invokeOnCompletion (ZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; public fun isActive ()Z diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 31d90eeef0..81fe978b19 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -31,7 +31,7 @@ import kotlin.jvm.* * It is completed by calling [CompletableJob.complete]. * * Conceptually, an execution of a 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. + * side effects. See [Deferred] interface for a job that produces a result. * * ### Job states * @@ -117,6 +117,22 @@ public interface Job : CoroutineContext.Element { // ------------ state query ------------ + /** + * Returns the parent of the current job if the parent-child relationship + * is established or `null` if the job has no parent or was successfully completed. + * + * Accesses to this property are not idempotent, the property becomes `null` as soon + * as the job is transitioned to its final state, whether it is cancelled or completed, + * and all job children are completed. + * + * For a coroutine, its corresponding job completes as soon as the coroutine itself + * and all its children are complete. + * + * @see [Job] state transitions for additional details. + */ + @ExperimentalCoroutinesApi + public val parent: Job? + /** * Returns `true` when this job is active -- it was already started and has not completed nor was cancelled yet. * The job that is waiting for its [children] to complete is still considered to be active if it diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index f7adba3c9e..428e861839 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -132,6 +132,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren get() = _parentHandle.value set(value) { _parentHandle.value = value } + override val parent: Job? + get() = parentHandle?.parent + // ------------ initialization ------------ /** diff --git a/kotlinx-coroutines-core/common/src/NonCancellable.kt b/kotlinx-coroutines-core/common/src/NonCancellable.kt index c278109224..9fb72dddbc 100644 --- a/kotlinx-coroutines-core/common/src/NonCancellable.kt +++ b/kotlinx-coroutines-core/common/src/NonCancellable.kt @@ -29,6 +29,14 @@ public object NonCancellable : AbstractCoroutineContextElement(Job), Job { private const val message = "NonCancellable can be used only as an argument for 'withContext', direct usages of its API are prohibited" + /** + * Always returns `null`. + * @suppress **This an internal API and should not be used from general code.** + */ + @Deprecated(level = DeprecationLevel.WARNING, message = message) + override val parent: Job? + get() = null + /** * Always returns `true`. * @suppress **This an internal API and should not be used from general code.** diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index ad8d86ed5a..e0a183028d 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -21,7 +21,6 @@ internal open class ScopeCoroutine( final override fun getStackTraceElement(): StackTraceElement? = null final override val isScopedCoroutine: Boolean get() = true - internal val parent: Job? get() = parentHandle?.parent override fun afterCompletion(state: Any?) { // Resume in a cancellable way by default when resuming from another context diff --git a/kotlinx-coroutines-core/common/test/JobStatesTest.kt b/kotlinx-coroutines-core/common/test/JobStatesTest.kt index dfcb462cba..739401f6a0 100644 --- a/kotlinx-coroutines-core/common/test/JobStatesTest.kt +++ b/kotlinx-coroutines-core/common/test/JobStatesTest.kt @@ -16,6 +16,7 @@ class JobStatesTest : TestBase() { @Test public fun testNormalCompletion() = runTest { expect(1) + val parent = coroutineContext.job val job = launch(start = CoroutineStart.LAZY) { expect(2) // launches child @@ -28,23 +29,27 @@ class JobStatesTest : TestBase() { assertFalse(job.isActive) assertFalse(job.isCompleted) assertFalse(job.isCancelled) + assertSame(parent, job.parent) // New -> Active job.start() assertTrue(job.isActive) assertFalse(job.isCompleted) assertFalse(job.isCancelled) + assertSame(parent, job.parent) // Active -> Completing yield() // scheduled & starts child expect(3) assertTrue(job.isActive) assertFalse(job.isCompleted) assertFalse(job.isCancelled) + assertSame(parent, job.parent) // Completing -> Completed yield() finish(5) assertFalse(job.isActive) assertTrue(job.isCompleted) assertFalse(job.isCancelled) + assertNull(job.parent) } @Test @@ -159,4 +164,4 @@ class JobStatesTest : TestBase() { assertTrue(job.isCompleted) assertTrue(job.isCancelled) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/JobTest.kt b/kotlinx-coroutines-core/common/test/JobTest.kt index 04d3c9e012..f009c7e55d 100644 --- a/kotlinx-coroutines-core/common/test/JobTest.kt +++ b/kotlinx-coroutines-core/common/test/JobTest.kt @@ -12,6 +12,7 @@ class JobTest : TestBase() { @Test fun testState() { val job = Job() + assertNull(job.parent) assertTrue(job.isActive) job.cancel() assertTrue(!job.isActive) @@ -210,11 +211,13 @@ class JobTest : TestBase() { @Test fun testIncompleteJobState() = runTest { + val parent = coroutineContext.job val job = launch { coroutineContext[Job]!!.invokeOnCompletion { } } - + assertSame(parent, job.parent) job.join() + assertNull(job.parent) assertTrue(job.isCompleted) assertFalse(job.isActive) assertFalse(job.isCancelled)