Use connect-and-lease when using daemon in JPS & Gradle
Relates to KT-15562 "Service is dying". This commit includes multiple changes: 1. JPS & Gradle daemon clients are refactored to use `connectAndLease` from `KotlinCompilerClient`. `connectAndLease` was introduced in previous commits 2. `withKotlin` was removed because `connectAndLease` already covers retrying on connection error 3. Gradle flag files creation is changed: * client-alive flag file lives as long as Gradle instance lives, * session-alive flag file lives until the end of a build.
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2010-2017 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.gradle
|
||||
|
||||
import org.jetbrains.kotlin.compilerRunner.COULD_NOT_CONNECT_TO_DAEMON_MESSAGE
|
||||
import org.jetbrains.kotlin.compilerRunner.CREATED_SESSION_FILE_PREFIX
|
||||
import org.jetbrains.kotlin.compilerRunner.DELETED_SESSION_FILE_PREFIX
|
||||
import org.jetbrains.kotlin.compilerRunner.EXISTING_SESSION_FILE_PREFIX
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
// todo: test client file creation/deletion
|
||||
// todo: test daemon start (does not start every build)
|
||||
// todo: test daemon shutdown when gradle daemon dies
|
||||
class KotlinDaemonAdvaced : BaseGradleIT() {
|
||||
companion object {
|
||||
private const val GRADLE_VERSION = "2.10"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDaemonMultiproject() {
|
||||
fun String.findAllStringsPrefixed(prefix: String): Array<String> {
|
||||
val result = arrayListOf<String>()
|
||||
val regex = ("$prefix([^\\r\\n]*)").toRegex()
|
||||
for (match in regex.findAll(this)) {
|
||||
result.add(match.groupValues[1])
|
||||
}
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
val project = Project("multiprojectWithDependency", GRADLE_VERSION)
|
||||
val strategyCLIArg = "-Dkotlin.compiler.execution.strategy=daemon"
|
||||
|
||||
fun checkAfterNonIncrementalBuild(output: String) {
|
||||
val createdSessions = output.findAllStringsPrefixed(CREATED_SESSION_FILE_PREFIX)
|
||||
assert(createdSessions.size == 1) { "Created multiple sessions per build ${createdSessions.joinToString()}" }
|
||||
|
||||
val existingSessions = output.findAllStringsPrefixed(EXISTING_SESSION_FILE_PREFIX)
|
||||
Assert.assertArrayEquals("Existing sessions don't match created sessions for two module projects", createdSessions, existingSessions)
|
||||
|
||||
val deletedSessions = output.findAllStringsPrefixed(DELETED_SESSION_FILE_PREFIX)
|
||||
Assert.assertArrayEquals("Deleted sessions don't match created sessions", createdSessions, deletedSessions)
|
||||
}
|
||||
|
||||
project.build("build", strategyCLIArg) {
|
||||
assertNotContains(COULD_NOT_CONNECT_TO_DAEMON_MESSAGE)
|
||||
checkAfterNonIncrementalBuild(output)
|
||||
}
|
||||
|
||||
project.build("clean", "build", strategyCLIArg) {
|
||||
assertNotContains(COULD_NOT_CONNECT_TO_DAEMON_MESSAGE)
|
||||
checkAfterNonIncrementalBuild(output)
|
||||
}
|
||||
|
||||
project.build("build", strategyCLIArg) {
|
||||
val createdSessions = output.findAllStringsPrefixed(CREATED_SESSION_FILE_PREFIX)
|
||||
Assert.assertArrayEquals("Sessions should not be created (incremental build)", createdSessions, emptyArray())
|
||||
|
||||
val existingSessions = output.findAllStringsPrefixed(EXISTING_SESSION_FILE_PREFIX)
|
||||
Assert.assertArrayEquals("Sessions should not be existing (incremental build)", existingSessions, emptyArray())
|
||||
|
||||
val deletedSessions = output.findAllStringsPrefixed(DELETED_SESSION_FILE_PREFIX)
|
||||
Assert.assertArrayEquals("Sessions should not be deleted (incremental build)", deletedSessions, emptyArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
-53
@@ -24,33 +24,30 @@ import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2MetadataCompilerArguments
|
||||
import org.jetbrains.kotlin.com.intellij.openapi.util.io.FileUtil
|
||||
import org.jetbrains.kotlin.config.Services
|
||||
import org.jetbrains.kotlin.daemon.client.CompileServiceSession
|
||||
import org.jetbrains.kotlin.daemon.common.*
|
||||
import org.jetbrains.kotlin.daemon.common.CompilationResultCategory
|
||||
import org.jetbrains.kotlin.gradle.plugin.ParentLastURLClassLoader
|
||||
import org.jetbrains.kotlin.gradle.plugin.kotlinDebug
|
||||
import org.jetbrains.kotlin.incremental.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
|
||||
import java.io.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
internal const val KOTLIN_COMPILER_EXECUTION_STRATEGY_PROPERTY = "kotlin.compiler.execution.strategy"
|
||||
internal const val DAEMON_EXECUTION_STRATEGY = "daemon"
|
||||
internal const val IN_PROCESS_EXECUTION_STRATEGY = "in-process"
|
||||
internal const val OUT_OF_PROCESS_EXECUTION_STRATEGY = "out-of-process"
|
||||
const val CREATED_CLIENT_FILE_PREFIX = "Created client-is-alive flag file: "
|
||||
const val EXISTING_CLIENT_FILE_PREFIX = "Existing client-is-alive flag file: "
|
||||
const val CREATED_SESSION_FILE_PREFIX = "Created session-is-alive flag file: "
|
||||
const val EXISTING_SESSION_FILE_PREFIX = "Existing session-is-alive flag file: "
|
||||
const val DELETED_SESSION_FILE_PREFIX = "Deleted session-is-alive flag file: "
|
||||
const val COULD_NOT_CONNECT_TO_DAEMON_MESSAGE = "Could not connect to Kotlin compile daemon"
|
||||
|
||||
internal class GradleCompilerRunner(private val project: Project) : KotlinCompilerRunner<GradleCompilerEnvironment>() {
|
||||
override val log = GradleKotlinLogger(project.logger)
|
||||
private val flagFile = run {
|
||||
val dir = File(project.rootProject.buildDir, "kotlin").apply { mkdirs() }
|
||||
File(dir, "daemon-is-alive").apply {
|
||||
if (!exists()) {
|
||||
createNewFile()
|
||||
deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runJvmCompiler(
|
||||
sourcesToCompile: List<File>,
|
||||
@@ -132,25 +129,43 @@ internal class GradleCompilerRunner(private val project: Project) : KotlinCompil
|
||||
}
|
||||
|
||||
override fun compileWithDaemon(compilerClassName: String, compilerArgs: CommonCompilerArguments, environment: GradleCompilerEnvironment): ExitCode? {
|
||||
val exitCode = if (environment is GradleIncrementalCompilerEnvironment) {
|
||||
incrementalCompilationWithDaemon(environment)
|
||||
}
|
||||
else {
|
||||
nonIncrementalCompilationWithDaemon(compilerClassName, environment)
|
||||
}
|
||||
exitCode?.let {
|
||||
withDaemon(environment, retryOnConnectionError = true) { daemon, sessionId ->
|
||||
daemon.clearJarCache()
|
||||
val connection = getDaemonConnection(environment)
|
||||
if (connection == null) {
|
||||
if (environment is GradleIncrementalCompilerEnvironment) {
|
||||
log.warn("Could not perform incremental compilation: $COULD_NOT_CONNECT_TO_DAEMON_MESSAGE")
|
||||
}
|
||||
logFinish(DAEMON_EXECUTION_STRATEGY)
|
||||
else {
|
||||
log.warn(COULD_NOT_CONNECT_TO_DAEMON_MESSAGE)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val (daemon, sessionId) = connection
|
||||
val exitCode = try {
|
||||
val res = if (environment is GradleIncrementalCompilerEnvironment) {
|
||||
incrementalCompilationWithDaemon(daemon, sessionId, environment)
|
||||
} else {
|
||||
nonIncrementalCompilationWithDaemon(daemon, sessionId, compilerClassName, environment)
|
||||
}
|
||||
exitCodeFromProcessExitCode(res.get())
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
log.warn("Compilation with Kotlin compile daemon was not successful")
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
// todo: can we clear cache on the end of session?
|
||||
daemon.clearJarCache()
|
||||
logFinish(DAEMON_EXECUTION_STRATEGY)
|
||||
return exitCode
|
||||
}
|
||||
|
||||
private fun nonIncrementalCompilationWithDaemon(
|
||||
daemon: CompileService,
|
||||
sessionId: Int,
|
||||
compilerClassName: String,
|
||||
environment: GradleCompilerEnvironment
|
||||
): ExitCode? {
|
||||
): CompileService.CallResult<Int> {
|
||||
val targetPlatform = when (compilerClassName) {
|
||||
K2JVM_COMPILER -> CompileService.TargetPlatform.JVM
|
||||
K2JS_COMPILER -> CompileService.TargetPlatform.JS
|
||||
@@ -166,21 +181,15 @@ internal class GradleCompilerRunner(private val project: Project) : KotlinCompil
|
||||
reportSeverity = reportSeverity(verbose),
|
||||
requestedCompilationResults = emptyArray())
|
||||
val servicesFacade = GradleCompilerServicesFacadeImpl(project, environment.messageCollector)
|
||||
|
||||
val res = withDaemon(environment, retryOnConnectionError = true) { daemon, sessionId ->
|
||||
val argsArray = ArgumentUtils.convertArgumentsToStringList(environment.compilerArgs).toTypedArray()
|
||||
daemon.compile(sessionId, argsArray, compilationOptions, servicesFacade, compilationResults = null)
|
||||
}
|
||||
|
||||
val exitCode = res?.get()?.let { exitCodeFromProcessExitCode(it) }
|
||||
if (exitCode == null) {
|
||||
log.warn("Could not perform non-incremental compilation with Kotlin compile daemon, " +
|
||||
"non-incremental compilation in fallback mode will be performed")
|
||||
}
|
||||
return exitCode
|
||||
val argsArray = ArgumentUtils.convertArgumentsToStringList(environment.compilerArgs).toTypedArray()
|
||||
return daemon.compile(sessionId, argsArray, compilationOptions, servicesFacade, compilationResults = null)
|
||||
}
|
||||
|
||||
private fun incrementalCompilationWithDaemon(environment: GradleIncrementalCompilerEnvironment): ExitCode? {
|
||||
private fun incrementalCompilationWithDaemon(
|
||||
daemon: CompileService,
|
||||
sessionId: Int,
|
||||
environment: GradleIncrementalCompilerEnvironment
|
||||
): CompileService.CallResult<Int> {
|
||||
val knownChangedFiles = environment.changedFiles as? ChangedFiles.Known
|
||||
|
||||
val verbose = environment.compilerArgs.verbose
|
||||
@@ -198,18 +207,8 @@ internal class GradleCompilerRunner(private val project: Project) : KotlinCompil
|
||||
targetPlatform = CompileService.TargetPlatform.JVM
|
||||
)
|
||||
val servicesFacade = GradleIncrementalCompilerServicesFacadeImpl(project, environment)
|
||||
|
||||
val res = withDaemon(environment, retryOnConnectionError = true) { daemon, sessionId ->
|
||||
val argsArray = ArgumentUtils.convertArgumentsToStringList(environment.compilerArgs).toTypedArray()
|
||||
daemon.compile(sessionId, argsArray, compilationOptions, servicesFacade, GradleCompilationResults(project))
|
||||
}
|
||||
|
||||
val exitCode = res?.get()?.let { exitCodeFromProcessExitCode(it) }
|
||||
if (exitCode == null) {
|
||||
log.warn("Could not perform incremental compilation with Kotlin compile daemon, " +
|
||||
"non-incremental compilation in fallback mode will be performed")
|
||||
}
|
||||
return exitCode
|
||||
val argsArray = ArgumentUtils.convertArgumentsToStringList(environment.compilerArgs).toTypedArray()
|
||||
return daemon.compile(sessionId, argsArray, compilationOptions, servicesFacade, GradleCompilationResults(project))
|
||||
}
|
||||
|
||||
private fun reportCategories(verbose: Boolean): Array<Int> =
|
||||
@@ -286,10 +285,61 @@ internal class GradleCompilerRunner(private val project: Project) : KotlinCompil
|
||||
log.debug("Finished executing kotlin compiler using $strategy strategy")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getDaemonConnection(environment: GradleCompilerEnvironment): DaemonConnection {
|
||||
override fun getDaemonConnection(environment: GradleCompilerEnvironment): CompileServiceSession? {
|
||||
val compilerId = CompilerId.makeCompilerId(environment.compilerClasspath)
|
||||
return newDaemonConnection(compilerId, flagFile, environment)
|
||||
val clientIsAliveFlagFile = getOrCreateClientFlagFile(project)
|
||||
val sessionIsAliveFlagFile = getOrCreateSessionFlagFile(project)
|
||||
return newDaemonConnection(compilerId, clientIsAliveFlagFile, sessionIsAliveFlagFile, environment)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// created once per gradle instance
|
||||
// when gradle daemon dies, kotlin daemon should die too
|
||||
// however kotlin daemon (if it idles enough) can die before gradle daemon dies
|
||||
@Volatile
|
||||
private var clientIsAliveFlagFile: File? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreateClientFlagFile(project: Project): File {
|
||||
val log = project.logger
|
||||
if (clientIsAliveFlagFile == null || !clientIsAliveFlagFile!!.exists()) {
|
||||
val projectName = project.rootProject.name.normalizeForFlagFile()
|
||||
clientIsAliveFlagFile = FileUtil.createTempFile("kotlin-compiler-in-$projectName-", ".alive")
|
||||
log.kotlinDebug { CREATED_CLIENT_FILE_PREFIX + clientIsAliveFlagFile!!.relativeToRoot(project) }
|
||||
}
|
||||
else {
|
||||
log.kotlinDebug { EXISTING_CLIENT_FILE_PREFIX + clientIsAliveFlagFile!!.relativeToRoot(project) }
|
||||
}
|
||||
|
||||
return clientIsAliveFlagFile!!
|
||||
}
|
||||
|
||||
private fun String.normalizeForFlagFile(): String {
|
||||
val validChars = ('a'..'z') + ('0'..'9') + "-_"
|
||||
return filter { it in validChars }
|
||||
}
|
||||
|
||||
// session is created per build
|
||||
@Volatile
|
||||
private var sessionFlagFile: File? = null
|
||||
|
||||
// session files are deleted at org.jetbrains.kotlin.gradle.plugin.KotlinGradleBuildServices.buildFinished
|
||||
@Synchronized
|
||||
private fun getOrCreateSessionFlagFile(project: Project): File {
|
||||
val log = project.logger
|
||||
if (sessionFlagFile == null || !sessionFlagFile!!.exists()) {
|
||||
val sessionFilesDir = sessionsDir(project).apply { mkdirs() }
|
||||
sessionFlagFile = FileUtil.createTempFile(sessionFilesDir, "kotlin-compiler-", ".salive")
|
||||
log.kotlinDebug { CREATED_SESSION_FILE_PREFIX + sessionFlagFile!!.relativeToRoot(project) }
|
||||
}
|
||||
else {
|
||||
log.kotlinDebug { EXISTING_SESSION_FILE_PREFIX + sessionFlagFile!!.relativeToRoot(project) }
|
||||
}
|
||||
|
||||
return sessionFlagFile!!
|
||||
}
|
||||
|
||||
internal fun sessionsDir(project: Project): File =
|
||||
File(File(project.rootProject.buildDir, "kotlin"), "sessions")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -24,9 +24,12 @@ import org.gradle.api.logging.Logging
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandler
|
||||
import org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem
|
||||
import org.jetbrains.kotlin.compilerRunner.DELETED_SESSION_FILE_PREFIX
|
||||
import org.jetbrains.kotlin.compilerRunner.GradleCompilerRunner
|
||||
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||
import org.jetbrains.kotlin.incremental.BuildCacheStorage
|
||||
import org.jetbrains.kotlin.incremental.multiproject.ArtifactDifferenceRegistryProvider
|
||||
import org.jetbrains.kotlin.incremental.relativeToRoot
|
||||
import java.io.File
|
||||
|
||||
internal class KotlinGradleBuildServices private constructor(gradle: Gradle): BuildAdapter() {
|
||||
@@ -91,6 +94,22 @@ internal class KotlinGradleBuildServices private constructor(gradle: Gradle): Bu
|
||||
log.kotlinDebug("Skipping kotlin cleanup since compiler wasn't called")
|
||||
}
|
||||
|
||||
val rootProject = gradle.rootProject
|
||||
val sessionsDir = GradleCompilerRunner.sessionsDir(rootProject)
|
||||
if (sessionsDir.exists()) {
|
||||
val sessionFiles = sessionsDir.listFiles()
|
||||
|
||||
// it is expected that only one session file per build exists
|
||||
// afaik is is not possible to run multiple gradle builds in one project since gradle locks some dirs
|
||||
if (sessionFiles.size > 1) {
|
||||
log.warn("w: Detected multiple Kotlin daemon sessions at ${sessionsDir.relativeToRoot(rootProject)}")
|
||||
}
|
||||
for (file in sessionFiles) {
|
||||
file.delete()
|
||||
log.kotlinDebug { DELETED_SESSION_FILE_PREFIX + file.relativeToRoot(rootProject) }
|
||||
}
|
||||
}
|
||||
|
||||
if (kotlinCompilerCalled) {
|
||||
startMemory?.let { startMemoryCopy ->
|
||||
getUsedMemoryKb()?.let { endMemory ->
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.jetbrains.kotlin.incremental
|
||||
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
|
||||
internal fun File.isJavaFile() =
|
||||
@@ -38,3 +39,6 @@ internal fun File.relativeOrCanonical(base: File): String =
|
||||
|
||||
internal fun Iterable<File>.pathsAsStringRelativeTo(base: File): String =
|
||||
map { it.relativeOrCanonical(base) }.sorted().joinToString()
|
||||
|
||||
internal fun File.relativeToRoot(project: Project): String =
|
||||
relativeOrCanonical(project.rootProject.rootDir)
|
||||
Reference in New Issue
Block a user