From 3fa715ea82c7892e46670ddc64fa119b513c2b25 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 29 Jul 2022 17:20:00 +0200 Subject: [PATCH 1/3] Introduce Job.parent API * The API is crucial for debugger extension and deadlock detection * It enables allows more fluent coroutines hierarchy inspection, e.g. capability to build a list of roots and procrss them top-bottom separately Fixes #3201 --- .../api/kotlinx-coroutines-core.api | 3 +++ kotlinx-coroutines-core/common/src/Job.kt | 18 +++++++++++++++++- .../common/src/JobSupport.kt | 3 +++ .../common/src/NonCancellable.kt | 8 ++++++++ .../common/src/internal/Scopes.kt | 1 - .../common/test/JobStatesTest.kt | 7 ++++++- kotlinx-coroutines-core/common/test/JobTest.kt | 5 ++++- 7 files changed, 41 insertions(+), 4 deletions(-) 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..72fdda749e 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 a 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 coroutines, 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) From 91f418b175f23a67235b39915aca1f2b29cd30d7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 29 Jul 2022 18:49:23 +0300 Subject: [PATCH 2/3] Update kotlinx-coroutines-core/common/src/Job.kt Co-authored-by: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> --- kotlinx-coroutines-core/common/src/Job.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 72fdda749e..a781038908 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -118,7 +118,7 @@ public interface Job : CoroutineContext.Element { // ------------ state query ------------ /** - * Returns a parent of the current job if the parent-child relationship + * 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 From ecd3e4d78147338dd8065b3156f53aba1d935c12 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 29 Jul 2022 18:49:50 +0300 Subject: [PATCH 3/3] Update kotlinx-coroutines-core/common/src/Job.kt Co-authored-by: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> --- kotlinx-coroutines-core/common/src/Job.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index a781038908..81fe978b19 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -125,7 +125,7 @@ public interface Job : CoroutineContext.Element { * as the job is transitioned to its final state, whether it is cancelled or completed, * and all job children are completed. * - * For coroutines, its corresponding job completes as soon as the coroutine itself + * 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.