Skip to content

Commit

Permalink
MBS-12612 Detekt and prevent Kotlin daemon fallback strategy (#1326)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugene Krivobokov authored Feb 4, 2022
1 parent 2df5810 commit 50044d5
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 2 deletions.
31 changes: 31 additions & 0 deletions docs/content/projects/BuildChecks.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,39 @@ This check automatically detects the issue:
}
```

#### Prevent Kotlin daemon fallback strategy

Sometimes Kotlin daemon fails and can't recover on its own.
It has incredible impact on build performance and continuing build is not worth it.
This is a workaround for [KT-48843](https://youtrack.jetbrains.com/issue/KT-48843). See there more details.

=== "Kotlin"
`build.gradle.kts`

```kotlin
buildChecks {
preventKotlinDaemonFallback {
enabled = true // disabled by default
}
}
```

=== "Groovy"
`build.gradle`

```groovy
buildChecks {
preventKotlinDaemonFallback {
enabled = true // disabled by default
}
}
```

#### Gradle properties

???+ warning
This check is deprecated and will be removed.

--8<--
avito-disclaimer.md
--8<--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal class ConfigurationCacheCompatibilityTest {
javaVersion {
version = org.gradle.api.JavaVersion.VERSION_11
}
preventKotlinDaemonFallback {
enabled = true
}
}
""".trimIndent()
).generateIn(projectDir)
Expand Down
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
Expand Up @@ -10,6 +10,7 @@ import com.avito.android.build_checks.internal.BuildEnvironmentInfo
import com.avito.android.build_checks.internal.CheckAndroidSdkVersionTask
import com.avito.android.build_checks.internal.MacOSLocalhostResolvingTask
import com.avito.android.build_checks.internal.RootTaskCreator
import com.avito.android.build_checks.internal.kotlin_daemon.KotlinDaemonFallbackDetector
import com.avito.android.build_checks.internal.params.GradlePropertiesChecker
import com.avito.android.build_checks.internal.unique_app_res.UniqueAppResourcesTaskCreator
import com.avito.android.build_checks.internal.unique_r.UniqueRClassesTaskCreator
Expand Down Expand Up @@ -75,6 +76,9 @@ public open class BuildParamCheckPlugin : Plugin<Project> {
if (checks.hasInstance<RootProjectCheck.GradleProperties>()) {
GradlePropertiesChecker(project, envInfo).check()
}
if (checks.hasInstance<RootProjectCheck.PreventKotlinDaemonFallback>()) {
KotlinDaemonFallbackDetector().register(project)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.avito.android.build_checks.RootProjectChecksExtension.RootProjectChec
import com.avito.android.build_checks.RootProjectChecksExtension.RootProjectCheck.GradleProperties
import com.avito.android.build_checks.RootProjectChecksExtension.RootProjectCheck.JavaVersion
import com.avito.android.build_checks.RootProjectChecksExtension.RootProjectCheck.MacOSLocalhost
import com.avito.android.build_checks.RootProjectChecksExtension.RootProjectCheck.PreventKotlinDaemonFallback
import org.gradle.api.Action
import kotlin.reflect.full.createInstance

Expand All @@ -27,6 +28,9 @@ public open class RootProjectChecksExtension : BuildChecksExtension() {
public fun gradleProperties(action: Action<GradleProperties>): Unit =
register(GradleProperties(), action)

public fun preventKotlinDaemonFallback(action: Action<PreventKotlinDaemonFallback>): Unit =
register(PreventKotlinDaemonFallback(), action)

public sealed class RootProjectCheck : Check {

public override var enabled: Boolean = true
Expand Down Expand Up @@ -56,6 +60,10 @@ public open class RootProjectChecksExtension : BuildChecksExtension() {
override var enabled: Boolean = false
}

public open class PreventKotlinDaemonFallback : RootProjectCheck() {
override var enabled: Boolean = false
}

override fun equals(other: Any?): Boolean {
return this.javaClass == other?.javaClass
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ ERROR: '$checkExtensionName' build check is failed.
${message.trimIndent()}
This check can be disabled in an extension:
This check can be skipped in an extension:
$extensionName {
$checkExtensionName {
enabled = false
enabled = ...
}
}
See https://avito-tech.github.io/avito-android/projects/BuildChecks (search '$checkExtensionName')
Expand Down
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")
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")
.forUseAtConfigurationTime()
.getOrElse("daemon")
return strategy != "daemon" // "in-process", "out-of-process"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.avito.android.stats.SeriesName
import com.avito.android.stats.statsd
import org.gradle.api.Project

// TODO: Will be removed in MBS-12691
internal class GradlePropertiesChecker(
private val project: Project,
private val envInfo: BuildEnvironmentInfo
Expand Down

0 comments on commit 50044d5

Please sign in to comment.