[JS IR] Use a guard file for preventing incremental caches corruption

If a developer interrupts (kill gradle process) a compilation process or
 an internal error interrupts the compilation process,
 the incremental cache files may be corrupted.
 The patch creates a special guard file, which allows detecting
 if the previous compilation was not successful,
 and in case of any issues drops the incremental cache files.

^KT-56581 Fixed
This commit is contained in:
Alexander Korepanov
2023-02-15 11:05:20 +01:00
committed by Space Team
parent 527fc68778
commit e107de6f36
3 changed files with 97 additions and 12 deletions
@@ -61,10 +61,7 @@ import org.jetbrains.kotlin.incremental.js.IncrementalNextRoundChecker
import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer
import org.jetbrains.kotlin.ir.backend.js.* import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity
import org.jetbrains.kotlin.ir.backend.js.ic.CacheUpdater import org.jetbrains.kotlin.ir.backend.js.ic.*
import org.jetbrains.kotlin.ir.backend.js.ic.DirtyFileState
import org.jetbrains.kotlin.ir.backend.js.ic.JsExecutableProducer
import org.jetbrains.kotlin.ir.backend.js.ic.ModuleArtifact
import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerIr import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerIr
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.CompilationOutputsBuilt import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.CompilationOutputsBuilt
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer
@@ -298,20 +295,30 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
messageCollector.report(INFO, "Produce executable: $outputDirPath") messageCollector.report(INFO, "Produce executable: $outputDirPath")
messageCollector.report(INFO, "Cache directory: ${arguments.cacheDirectory}") messageCollector.report(INFO, "Cache directory: ${arguments.cacheDirectory}")
if (icCaches.isNotEmpty()) { if (icCaches != null) {
val beforeIc2Js = System.currentTimeMillis() val beforeIc2Js = System.currentTimeMillis()
// We use one cache directory for both caches: JS AST and JS code.
// This guard MUST be unlocked after a successful preparing icCaches (see prepareIcCaches()).
// Do not use IncrementalCacheGuard::acquire() - it may drop an entire cache here, and
// it breaks the logic from JsExecutableProducer(), therefore use IncrementalCacheGuard::tryAcquire() instead
// TODO: One day, when we will lower IR and produce JS AST per module,
// think about using different directories for JS AST and JS code.
icCaches.cacheGuard.tryAcquire()
val jsExecutableProducer = JsExecutableProducer( val jsExecutableProducer = JsExecutableProducer(
mainModuleName = moduleName, mainModuleName = moduleName,
moduleKind = moduleKind, moduleKind = moduleKind,
sourceMapsInfo = SourceMapsInfo.from(configurationJs), sourceMapsInfo = SourceMapsInfo.from(configurationJs),
caches = icCaches, caches = icCaches.artifacts,
relativeRequirePath = true relativeRequirePath = true
) )
val (outputs, rebuiltModules) = jsExecutableProducer.buildExecutable(arguments.irPerModule, outJsProgram = false) val (outputs, rebuiltModules) = jsExecutableProducer.buildExecutable(arguments.irPerModule, outJsProgram = false)
outputs.writeAll(outputDir, outputName, arguments.generateDts, moduleName, moduleKind) outputs.writeAll(outputDir, outputName, arguments.generateDts, moduleName, moduleKind)
icCaches.cacheGuard.release()
messageCollector.report(INFO, "Executable production duration (IC): ${System.currentTimeMillis() - beforeIc2Js}ms") messageCollector.report(INFO, "Executable production duration (IC): ${System.currentTimeMillis() - beforeIc2Js}ms")
for ((event, duration) in jsExecutableProducer.getStopwatchLaps()) { for ((event, duration) in jsExecutableProducer.getStopwatchLaps()) {
messageCollector.report(INFO, " $event: ${(duration / 1e6).toInt()}ms") messageCollector.report(INFO, " $event: ${(duration / 1e6).toInt()}ms")
@@ -609,6 +616,8 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
) )
} }
class IcCachesArtifacts(val artifacts: List<ModuleArtifact>, val cacheGuard: IncrementalCacheGuard)
private fun prepareIcCaches( private fun prepareIcCaches(
arguments: K2JSCompilerArguments, arguments: K2JSCompilerArguments,
messageCollector: MessageCollector, messageCollector: MessageCollector,
@@ -617,11 +626,17 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
friendLibraries: List<String>, friendLibraries: List<String>,
configurationJs: CompilerConfiguration, configurationJs: CompilerConfiguration,
mainCallArguments: List<String>? mainCallArguments: List<String>?
): List<ModuleArtifact> { ): IcCachesArtifacts? {
val cacheDirectory = arguments.cacheDirectory val cacheDirectory = arguments.cacheDirectory
// TODO: Use JS IR IC infrastructure for WASM? // TODO: Use JS IR IC infrastructure for WASM?
val icCaches = if (!arguments.wasm && cacheDirectory != null) { if (!arguments.wasm && cacheDirectory != null) {
val cacheGuard = IncrementalCacheGuard(cacheDirectory).also {
if (it.acquire() == IncrementalCacheGuard.AcquireStatus.CACHE_CLEARED) {
messageCollector.report(INFO, "Cache guard file detected, cache directory '$cacheDirectory' cleared")
}
}
messageCollector.report(INFO, "") messageCollector.report(INFO, "")
messageCollector.report(INFO, "Building cache:") messageCollector.report(INFO, "Building cache:")
messageCollector.report(INFO, "to: $outputDir") messageCollector.report(INFO, "to: $outputDir")
@@ -650,6 +665,8 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
) )
val artifacts = cacheUpdater.actualizeCaches() val artifacts = cacheUpdater.actualizeCaches()
cacheGuard.release()
messageCollector.report(INFO, "IC rebuilt overall time: ${System.currentTimeMillis() - start}ms") messageCollector.report(INFO, "IC rebuilt overall time: ${System.currentTimeMillis() - start}ms")
for ((event, duration) in cacheUpdater.getStopwatchLastLaps()) { for ((event, duration) in cacheUpdater.getStopwatchLastLaps()) {
messageCollector.report(INFO, " $event: ${(duration / 1e6).toInt()}ms") messageCollector.report(INFO, " $event: ${(duration / 1e6).toInt()}ms")
@@ -678,9 +695,9 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
} }
} }
artifacts return IcCachesArtifacts(artifacts, cacheGuard)
} else emptyList() }
return icCaches return null
} }
override fun setupPlatformSpecificArgumentsAndServices( override fun setupPlatformSpecificArgumentsAndServices(
@@ -0,0 +1,33 @@
/*
* 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.ir.backend.js.ic
import java.io.File
class IncrementalCacheGuard(cacheDir: String) {
enum class AcquireStatus { OK, CACHE_CLEARED }
private val cacheRoot = File(cacheDir)
private val guardFile = cacheRoot.resolve("cache.guard")
fun acquire(): AcquireStatus {
val cacheCleared = guardFile.exists()
if (cacheCleared) {
cacheRoot.deleteRecursively()
}
tryAcquire()
return if (cacheCleared) AcquireStatus.CACHE_CLEARED else AcquireStatus.OK
}
fun tryAcquire() {
cacheRoot.mkdirs()
guardFile.createNewFile()
}
fun release() {
guardFile.delete()
}
}
@@ -13,6 +13,7 @@ import kotlin.io.path.appendText
import kotlin.io.path.readText import kotlin.io.path.readText
import kotlin.io.path.writeText import kotlin.io.path.writeText
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -54,11 +55,16 @@ class Kotlin2JsIrBeIncrementalCompilationIT : KGPBaseTest() {
} }
srcFile.writeText(badCode) srcFile.writeText(badCode)
val guardFile = projectPath.resolve("app/build/klib/cache/cache.guard").toFile()
for (i in 0..1) { for (i in 0..1) {
buildAndFail("nodeDevelopmentRun") { buildAndFail("nodeDevelopmentRun") {
assertTasksFailed(":app:compileDevelopmentExecutableKotlinJs") assertTasksFailed(":app:compileDevelopmentExecutableKotlinJs")
val failedBuildCacheFiles = readCacheFiles() val failedBuildCacheFiles = readCacheFiles()
assertEquals(successfulBuildCacheFiles, failedBuildCacheFiles, "The cache files should not be modified") val expectedFiles = successfulBuildCacheFiles.toMutableMap()
expectedFiles[guardFile.absolutePath] = ByteArray(0).contentHashCode()
assertEquals(expectedFiles, failedBuildCacheFiles, "The cache files should not be modified")
guardFile.delete()
} }
} }
@@ -198,4 +204,33 @@ class Kotlin2JsIrBeIncrementalCompilationIT : KGPBaseTest() {
} }
} }
} }
@DisplayName("Test cache invalidation after detecting guard file")
@GradleTest
fun testCacheGuardInvalidation(gradleVersion: GradleVersion) {
project("kotlin2JsIrICProject", gradleVersion) {
build("nodeDevelopmentRun") {
assertTasksExecuted(":compileDevelopmentExecutableKotlinJs")
assertOutputContains("module [main] was built clean")
assertOutputContains(">>> TEST OUT: Hello, Gradle.")
}
val cacheGuard = projectPath.resolve("build/klib/cache/cache.guard").toFile()
assertFalse(cacheGuard.exists(), "Cache guard file should be removed after successful build")
val srcFile = projectPath.resolve("src/main/kotlin/Main.kt").toFile()
srcFile.writeText(srcFile.readText().replace("greeting(\"Gradle\")", "greeting(\"Kotlin\")"))
cacheGuard.createNewFile()
build("nodeDevelopmentRun") {
assertTasksExecuted(":compileDevelopmentExecutableKotlinJs")
assertOutputContains(Regex("Cache guard file detected, cache directory '.+' cleared"))
assertOutputContains("module [main] was built clean")
assertOutputContains(">>> TEST OUT: Hello, Kotlin.")
}
assertFalse(cacheGuard.exists(), "Cache guard file should be removed after successful build")
}
}
} }