[Gradle] NativeDistributionCommonizationCache: Acquire FileLock

A `.lock` file will be acquired before running the native
distribution commonizer. This is done to protect data corruption
when multiple process try to commonize at the same time.

This could happen when e.g. two new projects are opened that
both trigger the commonizer during syncing.

^KT-46343 Verification Pending
This commit is contained in:
Sebastian Sellmair
2021-08-18 09:22:46 +00:00
committed by Space
parent fb1eac0985
commit 21f7e16ee6
2 changed files with 45 additions and 22 deletions
@@ -43,6 +43,8 @@ class CommonizerIT : BaseGradleIT() {
assertTasksExecuted(":commonizeNativeDistribution") assertTasksExecuted(":commonizeNativeDistribution")
assertContains("Native Distribution Commonization: Cache hit") assertContains("Native Distribution Commonization: Cache hit")
assertContains("Native Distribution Commonization: All available targets are commonized already") assertContains("Native Distribution Commonization: All available targets are commonized already")
assertContains("Native Distribution Commonization: Lock acquired")
assertContains("Native Distribution Commonization: Lock released")
assertNotContains(commonizerOutput) assertNotContains(commonizerOutput)
assertSuccessful() assertSuccessful()
} }
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.commonizer.*
import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.resolveCommonizedDirectory import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.resolveCommonizedDirectory
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import java.io.File import java.io.File
import java.io.FileOutputStream
internal val Project.isNativeDistributionCommonizationCacheEnabled: Boolean internal val Project.isNativeDistributionCommonizationCacheEnabled: Boolean
get() = PropertiesProvider(this).enableNativeDistributionCommonizationCache get() = PropertiesProvider(this).enableNativeDistributionCommonizationCache
@@ -21,6 +22,7 @@ internal class NativeDistributionCommonizationCache(
private val commonizer: NativeDistributionCommonizer private val commonizer: NativeDistributionCommonizer
) : NativeDistributionCommonizer { ) : NativeDistributionCommonizer {
override fun commonizeNativeDistribution( override fun commonizeNativeDistribution(
konanHome: File, konanHome: File,
outputDirectory: File, outputDirectory: File,
@@ -31,31 +33,33 @@ internal class NativeDistributionCommonizationCache(
logInfo("Cache disabled") logInfo("Cache disabled")
} }
val cachedOutputTargets = outputTargets withLock(outputDirectory) {
.filter { outputTarget -> isCached(resolveCommonizedDirectory(outputDirectory, outputTarget)) } val cachedOutputTargets = outputTargets
.onEach { outputTarget -> logInfo("Cache hit: $outputTarget already commonized") } .filter { outputTarget -> isCached(resolveCommonizedDirectory(outputDirectory, outputTarget)) }
.toSet() .onEach { outputTarget -> logInfo("Cache hit: $outputTarget already commonized") }
.toSet()
val enqueuedOutputTargets = if (project.isNativeDistributionCommonizationCacheEnabled) outputTargets - cachedOutputTargets val enqueuedOutputTargets = if (project.isNativeDistributionCommonizationCacheEnabled) outputTargets - cachedOutputTargets
else outputTargets else outputTargets
if (canReturnFast(konanHome, enqueuedOutputTargets)) { if (canReturnFast(konanHome, enqueuedOutputTargets)) {
logInfo("All available targets are commonized already - Nothing to do") logInfo("All available targets are commonized already - Nothing to do")
return return
}
enqueuedOutputTargets
.map { outputTarget -> resolveCommonizedDirectory(outputDirectory, outputTarget) }
.forEach { commonizedDirectory -> if (commonizedDirectory.exists()) commonizedDirectory.deleteRecursively() }
commonizer.commonizeNativeDistribution(
konanHome, outputDirectory, enqueuedOutputTargets, logLevel
)
enqueuedOutputTargets
.map { outputTarget -> resolveCommonizedDirectory(outputDirectory, outputTarget) }
.filter { commonizedDirectory -> commonizedDirectory.isDirectory }
.forEach { commonizedDirectory -> commonizedDirectory.resolve(".success").createNewFile() }
} }
enqueuedOutputTargets
.map { outputTarget -> resolveCommonizedDirectory(outputDirectory, outputTarget) }
.forEach { commonizedDirectory -> if (commonizedDirectory.exists()) commonizedDirectory.deleteRecursively() }
commonizer.commonizeNativeDistribution(
konanHome, outputDirectory, enqueuedOutputTargets, logLevel
)
enqueuedOutputTargets
.map { outputTarget -> resolveCommonizedDirectory(outputDirectory, outputTarget) }
.filter { commonizedDirectory -> commonizedDirectory.isDirectory }
.forEach { commonizedDirectory -> commonizedDirectory.resolve(".success").createNewFile() }
} }
private fun isCached(directory: File): Boolean { private fun isCached(directory: File): Boolean {
@@ -76,9 +80,26 @@ internal class NativeDistributionCommonizationCache(
.none { platformLibsDir -> platformLibsDir.exists() } .none { platformLibsDir -> platformLibsDir.exists() }
} }
private inline fun <T> withLock(outputDirectory: File, action: () -> T): T {
val lockfile = outputDirectory.resolve(".lock")
logInfo("Acquire lock: ${lockfile.path} ...")
FileOutputStream(outputDirectory.resolve(".lock")).use { stream ->
val lock = stream.channel.lock()
assert(lock.isValid)
return try {
logInfo("Lock acquired: ${lockfile.path}")
action()
} finally {
lock.release()
logInfo("Lock released: ${lockfile.path}")
}
}
}
private fun logInfo(message: String) = project.logger.info("${Logging.prefix}: $message") private fun logInfo(message: String) = project.logger.info("${Logging.prefix}: $message")
private object Logging { private object Logging {
const val prefix = "Native Distribution Commonization" const val prefix = "Native Distribution Commonization"
} }
} }