[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")
assertContains("Native Distribution Commonization: Cache hit")
assertContains("Native Distribution Commonization: All available targets are commonized already")
assertContains("Native Distribution Commonization: Lock acquired")
assertContains("Native Distribution Commonization: Lock released")
assertNotContains(commonizerOutput)
assertSuccessful()
}
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.commonizer.*
import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.resolveCommonizedDirectory
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import java.io.File
import java.io.FileOutputStream
internal val Project.isNativeDistributionCommonizationCacheEnabled: Boolean
get() = PropertiesProvider(this).enableNativeDistributionCommonizationCache
@@ -21,6 +22,7 @@ internal class NativeDistributionCommonizationCache(
private val commonizer: NativeDistributionCommonizer
) : NativeDistributionCommonizer {
override fun commonizeNativeDistribution(
konanHome: File,
outputDirectory: File,
@@ -31,31 +33,33 @@ internal class NativeDistributionCommonizationCache(
logInfo("Cache disabled")
}
val cachedOutputTargets = outputTargets
.filter { outputTarget -> isCached(resolveCommonizedDirectory(outputDirectory, outputTarget)) }
.onEach { outputTarget -> logInfo("Cache hit: $outputTarget already commonized") }
.toSet()
withLock(outputDirectory) {
val cachedOutputTargets = outputTargets
.filter { outputTarget -> isCached(resolveCommonizedDirectory(outputDirectory, outputTarget)) }
.onEach { outputTarget -> logInfo("Cache hit: $outputTarget already commonized") }
.toSet()
val enqueuedOutputTargets = if (project.isNativeDistributionCommonizationCacheEnabled) outputTargets - cachedOutputTargets
else outputTargets
val enqueuedOutputTargets = if (project.isNativeDistributionCommonizationCacheEnabled) outputTargets - cachedOutputTargets
else outputTargets
if (canReturnFast(konanHome, enqueuedOutputTargets)) {
logInfo("All available targets are commonized already - Nothing to do")
return
if (canReturnFast(konanHome, enqueuedOutputTargets)) {
logInfo("All available targets are commonized already - Nothing to do")
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 {
@@ -76,9 +80,26 @@ internal class NativeDistributionCommonizationCache(
.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 object Logging {
const val prefix = "Native Distribution Commonization"
}
}