-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MBS-12612 Detect and prevent Kotlin daemon fallback strategy #1326
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package com.avito.android.build_checks.kotlin_daemon | ||
|
||
import com.avito.test.gradle.TestProjectGenerator | ||
import com.avito.test.gradle.gradlew | ||
import com.avito.test.gradle.module.KotlinModule | ||
import com.avito.test.gradle.plugin.plugins | ||
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.io.TempDir | ||
import java.io.File | ||
|
||
internal class PreventKotlinDaemonFallbackTest { | ||
|
||
private lateinit var projectDir: File | ||
|
||
@BeforeEach | ||
fun setup(@TempDir tempDir: File) { | ||
this.projectDir = tempDir | ||
} | ||
|
||
@Test | ||
fun `disabled check - uses fallback strategy`() { | ||
generateProject(enableCheck = false) | ||
val result = build(":lib1:compileKotlin") | ||
|
||
result.assertThat() | ||
.buildSuccessful() | ||
.outputContains("Could not connect to kotlin daemon. Using fallback strategy.") | ||
} | ||
|
||
@Test | ||
fun `single fallback - success`() { | ||
generateProject(enableCheck = true) | ||
val result = build(":lib1:compileKotlin") | ||
|
||
result.assertThat() | ||
.buildSuccessful() | ||
} | ||
|
||
@Test | ||
fun `multiple fallbacks - failure`() { | ||
generateProject(enableCheck = true) | ||
val result = build("compileKotlin", expectFailure = true) | ||
|
||
result.assertThat() | ||
.buildFailed() | ||
.outputContains("Kill Kotlin daemon") | ||
} | ||
|
||
private fun generateProject(enableCheck: Boolean) { | ||
TestProjectGenerator( | ||
plugins = plugins { | ||
id("com.avito.android.build-checks") | ||
}, | ||
modules = listOf( | ||
KotlinModule(name = "lib1"), | ||
KotlinModule(name = "lib2"), | ||
), | ||
buildGradleExtra = """ | ||
buildChecks { | ||
enableByDefault = false | ||
preventKotlinDaemonFallback { | ||
enabled = $enableCheck | ||
} | ||
} | ||
""".trimIndent() | ||
).generateIn(projectDir) | ||
} | ||
|
||
private fun build( | ||
task: String, | ||
expectFailure: Boolean = false | ||
) = gradlew( | ||
projectDir, | ||
task, | ||
"-Dkotlin.daemon.jvm.options=invalid_jvm_argument_to_fail_process_startup", | ||
"--rerun-tasks", | ||
expectFailure = expectFailure | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package com.avito.android.build_checks.internal.kotlin_daemon | ||
|
||
import com.avito.android.build_checks.RootProjectChecksExtension | ||
import com.avito.android.build_checks.internal.FailedCheckMessage | ||
import org.gradle.api.GradleException | ||
import org.gradle.api.internal.GradleInternal | ||
import org.gradle.api.internal.tasks.execution.ExecuteTaskBuildOperationType | ||
import org.gradle.api.invocation.Gradle | ||
import org.gradle.execution.RunRootBuildWorkBuildOperationType | ||
import org.gradle.internal.operations.BuildOperationCategory | ||
import org.gradle.internal.operations.BuildOperationDescriptor | ||
import org.gradle.internal.operations.BuildOperationListener | ||
import org.gradle.internal.operations.BuildOperationListenerManager | ||
import org.gradle.internal.operations.OperationFinishEvent | ||
import org.gradle.internal.operations.OperationIdentifier | ||
import org.gradle.internal.operations.OperationProgressEvent | ||
import org.gradle.internal.operations.OperationStartEvent | ||
import java.util.concurrent.atomic.AtomicInteger | ||
|
||
internal class BuildFailer( | ||
private val fallbackCounter: AtomicInteger, | ||
) : BuildOperationListener { | ||
|
||
var cleanupAction: (() -> Unit)? = null | ||
|
||
override fun started(buildOperation: BuildOperationDescriptor, event: OperationStartEvent) { | ||
} | ||
|
||
override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { | ||
} | ||
|
||
override fun finished(buildOperation: BuildOperationDescriptor, event: OperationFinishEvent) { | ||
if (isTaskFinished(event) && fallbackCounter.get() > FALLBACKS_COUNT_THRESHOLD) { | ||
doCleanup() | ||
failBuild() | ||
} | ||
if (isBuildFinished(buildOperation)) { | ||
doCleanup() | ||
} | ||
} | ||
|
||
@Suppress("MaxLineLength") | ||
private fun failBuild() { | ||
throw GradleException( | ||
FailedCheckMessage( | ||
RootProjectChecksExtension::preventKotlinDaemonFallback, | ||
""" | ||
|Kotlin daemon process is not available and most probably won't recover on its own. | ||
|It has incredible impact on build performance and continuing build is not worth it. | ||
|https://youtrack.jetbrains.com/issue/KT-48843 | ||
| | ||
|How to fix | ||
| | ||
|Kill Kotlin daemon process: | ||
| 1. ./gradlew --stop | ||
| 2. Find Kotlin daemon process id (pid): `jps | grep Kotlin` | ||
| 3. kill <pid> | ||
| | ||
|If it doesn't help, check that there are no custom jvm arguments in "kotlin.daemon.jvm.options" property except for Xmx. | ||
""".trimMargin() | ||
).toString() | ||
) | ||
} | ||
|
||
private fun doCleanup() { | ||
val action = requireNotNull(cleanupAction) { | ||
"cleanupAction must be set to unsubscribe Gradle listeners" | ||
} | ||
action() | ||
} | ||
|
||
private fun isTaskFinished(event: OperationFinishEvent): Boolean = | ||
event.result is ExecuteTaskBuildOperationType.Result | ||
|
||
private fun isBuildFinished(operation: BuildOperationDescriptor): Boolean = | ||
operation.details is RunRootBuildWorkBuildOperationType.Details | ||
&& operation.metadata == BuildOperationCategory.RUN_WORK | ||
} | ||
|
||
/** | ||
* To be more confident that daemon process is non recoverable | ||
*/ | ||
private const val FALLBACKS_COUNT_THRESHOLD = 1 | ||
|
||
internal fun Gradle.buildOperationListenerManager(): BuildOperationListenerManager = | ||
(this as GradleInternal).services[BuildOperationListenerManager::class.java] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.avito.android.build_checks.internal.kotlin_daemon | ||
|
||
import org.gradle.internal.logging.events.LogEvent | ||
import org.gradle.internal.logging.events.OutputEvent | ||
import org.gradle.internal.logging.events.OutputEventListener | ||
import java.util.concurrent.atomic.AtomicInteger | ||
|
||
internal class FailureEventListener( | ||
private val fallbacksCounter: AtomicInteger, | ||
) : OutputEventListener { | ||
|
||
override fun onOutput(event: OutputEvent) { | ||
if (isFallbackMessage(event)) { | ||
// Can't fail a build from OutputEventListener. So, only mark it | ||
fallbacksCounter.incrementAndGet() | ||
} | ||
} | ||
|
||
private fun isFallbackMessage(event: OutputEvent): Boolean { | ||
return event is LogEvent | ||
&& event.message.contains("Could not connect to kotlin daemon. Using fallback strategy.") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.avito.android.build_checks.internal.kotlin_daemon | ||
|
||
import org.gradle.api.Project | ||
import org.gradle.internal.logging.LoggingManagerInternal | ||
import org.gradle.kotlin.dsl.support.serviceOf | ||
import java.util.concurrent.atomic.AtomicInteger | ||
|
||
internal class KotlinDaemonFallbackDetector { | ||
|
||
fun register(project: Project) { | ||
if (isDaemonDisabled(project)) { | ||
project.logger.debug("Kotlin daemon fallback detection is disabled due to absence of daemon") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lifecycle? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #1336 |
||
return | ||
} | ||
val fallbacksCounter = AtomicInteger(0) | ||
|
||
val loggingManager = project.gradle.serviceOf<LoggingManagerInternal>() | ||
val listenerManager = project.gradle.buildOperationListenerManager() | ||
|
||
val failureListener = FailureEventListener(fallbacksCounter) | ||
loggingManager.addOutputEventListener(failureListener) | ||
|
||
val buildFailer = BuildFailer(fallbacksCounter) | ||
buildFailer.cleanupAction = { | ||
loggingManager.removeOutputEventListener(failureListener) | ||
listenerManager.removeListener(buildFailer) | ||
} | ||
listenerManager.addListener(buildFailer) | ||
} | ||
|
||
/** | ||
* Copy of internal logic in GradleKotlinCompilerRunner | ||
*/ | ||
private fun isDaemonDisabled(project: Project): Boolean { | ||
val strategy = project.providers.systemProperty("kotlin.compiler.execution.strategy") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be broken if property name will be changed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a test to this case: #1336 |
||
.forUseAtConfigurationTime() | ||
.getOrElse("daemon") | ||
return strategy != "daemon" // "in-process", "out-of-process" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add to check flag to force using the deamon There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see the need for it yet. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like this hacky approach )
but there is a chance that it will be handled differently in the future, just saying
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, I understand and accept this trade-off.
I haven't found other good ways to simulate this but having tests is important.