-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MBS-12612 Detekt and prevent Kotlin daemon fallback strategy (#1326)
- Loading branch information
Eugene Krivobokov
authored
Feb 4, 2022
1 parent
2df5810
commit 50044d5
Showing
10 changed files
with
278 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
...st/kotlin/com/avito/android/build_checks/kotlin_daemon/PreventKotlinDaemonFallbackTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
...ecks/src/main/kotlin/com/avito/android/build_checks/internal/kotlin_daemon/BuildFailer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
23 changes: 23 additions & 0 deletions
23
...main/kotlin/com/avito/android/build_checks/internal/kotlin_daemon/FailureEventListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...lin/com/avito/android/build_checks/internal/kotlin_daemon/KotlinDaemonFallbackDetector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters