[Gradle] Split KotlinPluginLifecycle and KotlinPluginLifecycleImpl

^KT-58255 Verification Pending
This commit is contained in:
Sebastian Sellmair
2023-05-09 14:13:38 +02:00
committed by Space Team
parent 7910e0d32c
commit 85059ced93
3 changed files with 248 additions and 231 deletions
@@ -8,13 +8,8 @@ package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.*
import org.jetbrains.kotlin.gradle.utils.CompletableFuture
import org.jetbrains.kotlin.gradle.utils.Future
import org.jetbrains.kotlin.gradle.utils.failures
import org.jetbrains.kotlin.gradle.utils.getOrPut
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayDeque
import kotlin.coroutines.*
/*
@@ -156,11 +151,7 @@ internal val Project.kotlinPluginLifecycle: KotlinPluginLifecycle
* some later data to be available. In this case, the Future still will only return 'sane' data.
*/
internal val Project.configurationResult: Future<ProjectConfigurationResult>
get() = configurationResultImpl
private val Project.configurationResultImpl: CompletableFuture<ProjectConfigurationResult>
get() = extraProperties.getOrPut("org.jetbrains.kotlin.gradle.plugin.configurationResult") { CompletableFuture() }
get() = (kotlinPluginLifecycle as KotlinPluginLifecycleImpl).configurationResult
/**
@@ -175,8 +166,7 @@ internal fun Project.startKotlinPluginLifecycle() {
* the currently running coroutine. Throws if this coroutine was not started using a [KotlinPluginLifecycle]
*/
internal suspend fun currentKotlinPluginLifecycle(): KotlinPluginLifecycle {
return coroutineContext[KotlinPluginLifecycleCoroutineContextElement]?.lifecycle
?: error("Missing $KotlinPluginLifecycleCoroutineContextElement in currentCoroutineContext")
return coroutineContext.kotlinPluginLifecycle
}
/**
@@ -224,33 +214,6 @@ internal suspend fun <T> requireCurrentStage(block: suspend () -> T): T {
return requiredStage(currentKotlinPluginLifecycle().stage, block)
}
/**
* Will ensure that the given [block] cannot leave the specified allowed stages [allowed]
* e.g.
*
* ```kotlin
* project.launchInStage(Stage.BeforeFinaliseDsl) {
* withRestrictedStages(Stage.upTo(Stage.FinaliseDsl)) {
* await(Stage.FinaliseDsl) // <- OK, since still in allowed stages
* await(Stage.AfterFinaliseDsl) // <- fails, since not in allowed stages!
* }
* }
* ```
*/
internal suspend fun <T> withRestrictedStages(allowed: Set<Stage>, block: suspend () -> T): T {
val newCoroutineContext = coroutineContext + RestrictedLifecycleStages(currentKotlinPluginLifecycle(), allowed)
return suspendCoroutine { continuation ->
val newContinuation = object : Continuation<T> {
override val context: CoroutineContext
get() = newCoroutineContext
override fun resumeWith(result: Result<T>) {
continuation.resumeWith(result)
}
}
block.startCoroutine(newContinuation)
}
}
/*
Definition of the Lifecycle and its stages
@@ -339,195 +302,3 @@ internal interface KotlinPluginLifecycle {
class IllegalLifecycleException(message: String) : IllegalStateException(message)
}
/*
Implementation
*/
internal class KotlinPluginLifecycleImpl(override val project: Project) : KotlinPluginLifecycle {
private val enqueuedActions: Map<Stage, ArrayDeque<KotlinPluginLifecycle.() -> Unit>> =
Stage.values().associateWith { ArrayDeque() }
private val loopRunning = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
private val isFinishedSuccessfully = AtomicBoolean(false)
private val isFinishedWithFailures = AtomicBoolean(false)
override var stage: Stage = Stage.values.first()
fun start() {
check(!isStarted.getAndSet(true)) {
"${KotlinPluginLifecycle::class.java.name} already started"
}
check(!project.state.executed) {
"${KotlinPluginLifecycle::class.java.name} cannot be started in ProjectState '${project.state}'"
}
loopIfNecessary()
project.whenEvaluated {
/* Check for failures happening during buildscript evaluation */
project.failures.let { failures ->
if (failures.isNotEmpty()) {
finishWithFailures(failures)
return@whenEvaluated
}
}
assert(enqueuedActions.getValue(stage).isEmpty()) { "Expected empty queue from '$stage'" }
stage = stage.nextOrThrow
executeCurrentStageAndScheduleNext()
}
}
private fun executeCurrentStageAndScheduleNext() {
stage.previousOrNull?.let { previousStage ->
assert(enqueuedActions.getValue(previousStage).isEmpty()) {
"Actions from previous stage '$previousStage' have not been executed (stage: '$stage')"
}
}
val failures = project.failures
if (failures.isNotEmpty()) {
finishWithFailures(failures)
return
}
try {
loopIfNecessary()
} catch (t: Throwable) {
finishWithFailures(listOf(t))
throw t
}
stage = stage.nextOrNull ?: run {
finishSuccessfully()
return
}
project.afterEvaluate {
executeCurrentStageAndScheduleNext()
}
}
private fun loopIfNecessary() {
if (loopRunning.getAndSet(true)) return
try {
val queue = enqueuedActions.getValue(stage)
do {
project.state.rethrowFailure()
val action = queue.removeFirstOrNull()
action?.invoke(this)
} while (action != null)
} finally {
loopRunning.set(false)
}
}
private fun finishWithFailures(failures: List<Throwable>) {
assert(failures.isNotEmpty())
assert(isStarted.get())
assert(!isFinishedWithFailures.getAndSet(true))
project.configurationResultImpl.complete(ProjectConfigurationResult.Failure(failures))
}
private fun finishSuccessfully() {
assert(isStarted.get())
assert(!isFinishedSuccessfully.getAndSet(true))
project.configurationResultImpl.complete(ProjectConfigurationResult.Success)
}
fun enqueue(stage: Stage, action: KotlinPluginLifecycle.() -> Unit) {
if (stage < this.stage) {
throw IllegalLifecycleException("Cannot enqueue Action for stage '$stage' in current stage '${this.stage}'")
}
/*
Lifecycle finished: action shall not be enqueued, but just executed right away.
This is desirable, so that .enqueue (and .launch) functions that are scheduled in execution phase
will be executed right away (no suspend necessary or wanted)
*/
if (isFinishedSuccessfully.get()) {
return action()
}
/*
Lifecycle finished, but some exceptions have been thrown.
In this case, an enqueue for future Stages is not allowed, since those will not be executed anymore.
Any enqueue in the current stage will be executed right away (no suspend necessary or wanted).
*/
if (isFinishedWithFailures.get()) {
return if (stage == this.stage) action()
else Unit
}
enqueuedActions.getValue(stage).addLast(action)
if (stage == Stage.EvaluateBuildscript && isStarted.get()) {
loopIfNecessary()
}
}
override fun launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
val lifecycle = this
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
override val context: CoroutineContext = EmptyCoroutineContext +
KotlinPluginLifecycleCoroutineContextElement(lifecycle)
override fun resumeWith(result: Result<Unit>) = result.getOrThrow()
})
enqueue(stage) {
coroutine.resume(Unit)
}
}
override suspend fun await(stage: Stage) {
if (this.stage > stage) return
suspendCoroutine<Unit> { continuation ->
enqueue(stage) {
continuation.resume(Unit)
}
}
}
}
private class KotlinPluginLifecycleCoroutineContextElement(
val lifecycle: KotlinPluginLifecycle,
) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement>
override val key: CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement> = Key
}
private class RestrictedLifecycleStages(
private val lifecycle: KotlinPluginLifecycle,
private val allowedStages: Set<Stage>,
) : CoroutineContext.Element, ContinuationInterceptor {
override val key: CoroutineContext.Key<*> = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = object : Continuation<T> {
override val context: CoroutineContext
get() = continuation.context
override fun resumeWith(result: Result<T>) = when {
result.isFailure -> continuation.resumeWith(result)
lifecycle.stage !in allowedStages -> continuation.resumeWithException(
IllegalLifecycleException(
"Required stage in '$allowedStages', but lifecycle switched to '${lifecycle.stage}'"
)
)
else -> continuation.resumeWith(result)
}
}
init {
if (lifecycle.stage !in allowedStages) {
throw IllegalLifecycleException("Required stage in '${allowedStages}' but lifecycle is currently in '${lifecycle.stage}'")
}
}
}
@@ -0,0 +1,180 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.ProjectConfigurationResult
import org.jetbrains.kotlin.gradle.utils.CompletableFuture
import org.jetbrains.kotlin.gradle.utils.failures
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.*
internal val CoroutineContext.kotlinPluginLifecycle: KotlinPluginLifecycle
get() = this[KotlinPluginLifecycleCoroutineContextElement]?.lifecycle
?: error("Missing $KotlinPluginLifecycleCoroutineContextElement in currentCoroutineContext")
internal class KotlinPluginLifecycleImpl(override val project: Project) : KotlinPluginLifecycle {
val configurationResult = CompletableFuture<ProjectConfigurationResult>()
private val enqueuedActions: Map<KotlinPluginLifecycle.Stage, ArrayDeque<KotlinPluginLifecycle.() -> Unit>> =
KotlinPluginLifecycle.Stage.values().associateWith { ArrayDeque() }
private val loopRunning = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
private val isFinishedSuccessfully = AtomicBoolean(false)
private val isFinishedWithFailures = AtomicBoolean(false)
override var stage: KotlinPluginLifecycle.Stage = KotlinPluginLifecycle.Stage.values.first()
fun start() {
check(!isStarted.getAndSet(true)) {
"${KotlinPluginLifecycle::class.java.name} already started"
}
check(!project.state.executed) {
"${KotlinPluginLifecycle::class.java.name} cannot be started in ProjectState '${project.state}'"
}
loopIfNecessary()
project.whenEvaluated {
/* Check for failures happening during buildscript evaluation */
project.failures.let { failures ->
if (failures.isNotEmpty()) {
finishWithFailures(failures)
return@whenEvaluated
}
}
assert(enqueuedActions.getValue(stage).isEmpty()) { "Expected empty queue from '$stage'" }
stage = stage.nextOrThrow
executeCurrentStageAndScheduleNext()
}
}
private fun executeCurrentStageAndScheduleNext() {
stage.previousOrNull?.let { previousStage ->
assert(enqueuedActions.getValue(previousStage).isEmpty()) {
"Actions from previous stage '$previousStage' have not been executed (stage: '$stage')"
}
}
val failures = project.failures
if (failures.isNotEmpty()) {
finishWithFailures(failures)
return
}
try {
loopIfNecessary()
} catch (t: Throwable) {
finishWithFailures(listOf(t))
throw t
}
stage = stage.nextOrNull ?: run {
finishSuccessfully()
return
}
project.afterEvaluate {
executeCurrentStageAndScheduleNext()
}
}
private fun loopIfNecessary() {
if (loopRunning.getAndSet(true)) return
try {
val queue = enqueuedActions.getValue(stage)
do {
project.state.rethrowFailure()
val action = queue.removeFirstOrNull()
action?.invoke(this)
} while (action != null)
} finally {
loopRunning.set(false)
}
}
private fun finishWithFailures(failures: List<Throwable>) {
assert(failures.isNotEmpty())
assert(isStarted.get())
assert(!isFinishedWithFailures.getAndSet(true))
configurationResult.complete(ProjectConfigurationResult.Failure(failures))
}
private fun finishSuccessfully() {
assert(isStarted.get())
assert(!isFinishedSuccessfully.getAndSet(true))
configurationResult.complete(ProjectConfigurationResult.Success)
}
fun enqueue(stage: KotlinPluginLifecycle.Stage, action: KotlinPluginLifecycle.() -> Unit) {
if (stage < this.stage) {
throw KotlinPluginLifecycle.IllegalLifecycleException("Cannot enqueue Action for stage '$stage' in current stage '${this.stage}'")
}
/*
Lifecycle finished: action shall not be enqueued, but just executed right away.
This is desirable, so that .enqueue (and .launch) functions that are scheduled in execution phase
will be executed right away (no suspend necessary or wanted)
*/
if (isFinishedSuccessfully.get()) {
return action()
}
/*
Lifecycle finished, but some exceptions have been thrown.
In this case, an enqueue for future Stages is not allowed, since those will not be executed anymore.
Any enqueue in the current stage will be executed right away (no suspend necessary or wanted).
*/
if (isFinishedWithFailures.get()) {
return if (stage == this.stage) action()
else Unit
}
enqueuedActions.getValue(stage).addLast(action)
if (stage == KotlinPluginLifecycle.Stage.EvaluateBuildscript && isStarted.get()) {
loopIfNecessary()
}
}
override fun launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
val lifecycle = this
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
override val context: CoroutineContext = EmptyCoroutineContext +
KotlinPluginLifecycleCoroutineContextElement(lifecycle)
override fun resumeWith(result: Result<Unit>) = result.getOrThrow()
})
enqueue(stage) {
coroutine.resume(Unit)
}
}
override suspend fun await(stage: KotlinPluginLifecycle.Stage) {
if (this.stage > stage) return
suspendCoroutine<Unit> { continuation ->
enqueue(stage) {
continuation.resume(Unit)
}
}
}
}
private class KotlinPluginLifecycleCoroutineContextElement(
val lifecycle: KotlinPluginLifecycle,
) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement>
override val key: CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement> = Key
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
import kotlin.coroutines.*
/**
* Will ensure that the given [block] cannot leave the specified allowed stages [allowed]
* e.g.
*
* ```kotlin
* project.launchInStage(Stage.BeforeFinaliseDsl) {
* withRestrictedStages(Stage.upTo(Stage.FinaliseDsl)) {
* await(Stage.FinaliseDsl) // <- OK, since still in allowed stages
* await(Stage.AfterFinaliseDsl) // <- fails, since not in allowed stages!
* }
* }
* ```
*/
internal suspend fun <T> withRestrictedStages(allowed: Set<KotlinPluginLifecycle.Stage>, block: suspend () -> T): T {
val newCoroutineContext = coroutineContext + KotlinPluginLifecycleStageRestriction(currentKotlinPluginLifecycle(), allowed)
return suspendCoroutine { continuation ->
val newContinuation = object : Continuation<T> {
override val context: CoroutineContext
get() = newCoroutineContext
override fun resumeWith(result: Result<T>) {
continuation.resumeWith(result)
}
}
block.startCoroutine(newContinuation)
}
}
private class KotlinPluginLifecycleStageRestriction(
private val lifecycle: KotlinPluginLifecycle,
private val allowedStages: Set<KotlinPluginLifecycle.Stage>,
) : CoroutineContext.Element, ContinuationInterceptor {
override val key: CoroutineContext.Key<*> = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = object : Continuation<T> {
override val context: CoroutineContext
get() = continuation.context
override fun resumeWith(result: Result<T>) = when {
result.isFailure -> continuation.resumeWith(result)
lifecycle.stage !in allowedStages -> continuation.resumeWithException(
IllegalLifecycleException(
"Required stage in '$allowedStages', but lifecycle switched to '${lifecycle.stage}'"
)
)
else -> continuation.resumeWith(result)
}
}
init {
if (lifecycle.stage !in allowedStages) {
throw IllegalLifecycleException("Required stage in '${allowedStages}' but lifecycle is currently in '${lifecycle.stage}'")
}
}
}