Fix synchronization when working with IC caches

IC caches could be modified and read from different threads.
In JPS builder these threads are RMI worker (invoked from Compiler
Daemon) and JPS worker thread. Proper synchronization fixes cases
when caches could become broken.
#KT-42265 Fixed
#KT-42194 Fixed
#KT-42265 Fixed
#KT-42937 Fixed
This commit is contained in:
Andrey Uskov
2020-11-12 17:25:18 +03:00
parent af95b8d1fe
commit 275a02ce88
10 changed files with 56 additions and 17 deletions
@@ -176,8 +176,8 @@ abstract class AbstractIncrementalCache<ClassName>(
protected class ClassFqNameToSourceMap( protected class ClassFqNameToSourceMap(
storageFile: File, storageFile: File,
private val pathConverter: FileToPathConverter private val pathConverter: FileToPathConverter
) : ) : BasicStringMap<String>(storageFile, EnumeratorStringDescriptor(), PathStringDescriptor) {
BasicStringMap<String>(storageFile, EnumeratorStringDescriptor(), PathStringDescriptor) {
operator fun set(fqName: FqName, sourceFile: File) { operator fun set(fqName: FqName, sourceFile: File) {
storage[fqName.asString()] = pathConverter.toPath(sourceFile) storage[fqName.asString()] = pathConverter.toPath(sourceFile)
} }
@@ -272,6 +272,7 @@ open class IncrementalJvmCache(
private inner class ProtoMap(storageFile: File) : BasicStringMap<ProtoMapValue>(storageFile, ProtoMapValueExternalizer) { private inner class ProtoMap(storageFile: File) : BasicStringMap<ProtoMapValue>(storageFile, ProtoMapValueExternalizer) {
@Synchronized
fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) {
return put(kotlinClass, changesCollector) return put(kotlinClass, changesCollector)
} }
@@ -283,10 +284,12 @@ open class IncrementalJvmCache(
// from files compiled during last round. // from files compiled during last round.
// However there is no need to compare old and new data in this case // However there is no need to compare old and new data in this case
// (also that would fail with exception). // (also that would fail with exception).
@Synchronized
fun storeModuleMapping(className: JvmClassName, bytes: ByteArray) { fun storeModuleMapping(className: JvmClassName, bytes: ByteArray) {
storage[className.internalName] = ProtoMapValue(isPackageFacade = false, bytes = bytes, strings = emptyArray()) storage[className.internalName] = ProtoMapValue(isPackageFacade = false, bytes = bytes, strings = emptyArray())
} }
@Synchronized
private fun put(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { private fun put(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) {
val header = kotlinClass.classHeader val header = kotlinClass.classHeader
@@ -309,6 +312,7 @@ open class IncrementalJvmCache(
operator fun get(className: JvmClassName): ProtoMapValue? = operator fun get(className: JvmClassName): ProtoMapValue? =
storage[className.internalName] storage[className.internalName]
@Synchronized
fun remove(className: JvmClassName, changesCollector: ChangesCollector) { fun remove(className: JvmClassName, changesCollector: ChangesCollector) {
val key = className.internalName val key = className.internalName
val oldValue = storage[key] ?: return val oldValue = storage[key] ?: return
@@ -325,6 +329,8 @@ open class IncrementalJvmCache(
private inner class JavaSourcesProtoMap(storageFile: File) : private inner class JavaSourcesProtoMap(storageFile: File) :
BasicStringMap<SerializedJavaClass>(storageFile, JavaClassProtoMapValueExternalizer) { BasicStringMap<SerializedJavaClass>(storageFile, JavaClassProtoMapValueExternalizer) {
@Synchronized
fun process(jvmClassName: JvmClassName, newData: SerializedJavaClass, changesCollector: ChangesCollector) { fun process(jvmClassName: JvmClassName, newData: SerializedJavaClass, changesCollector: ChangesCollector) {
val key = jvmClassName.internalName val key = jvmClassName.internalName
val oldData = storage[key] val oldData = storage[key]
@@ -336,6 +342,7 @@ open class IncrementalJvmCache(
) )
} }
@Synchronized
fun remove(className: JvmClassName, changesCollector: ChangesCollector) { fun remove(className: JvmClassName, changesCollector: ChangesCollector) {
val key = className.internalName val key = className.internalName
val oldValue = storage[key] ?: return val oldValue = storage[key] ?: return
@@ -375,6 +382,7 @@ open class IncrementalJvmCache(
operator fun contains(className: JvmClassName): Boolean = operator fun contains(className: JvmClassName): Boolean =
className.internalName in storage className.internalName in storage
@Synchronized
fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) {
val key = kotlinClass.className.internalName val key = kotlinClass.className.internalName
val oldMap = storage[key] ?: emptyMap() val oldMap = storage[key] ?: emptyMap()
@@ -391,6 +399,7 @@ open class IncrementalJvmCache(
} }
} }
@Synchronized
fun remove(className: JvmClassName) { fun remove(className: JvmClassName) {
storage.remove(className.internalName) storage.remove(className.internalName)
} }
@@ -523,6 +532,7 @@ open class IncrementalJvmCache(
return result return result
} }
@Synchronized
fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) {
val key = kotlinClass.className.internalName val key = kotlinClass.className.internalName
val oldMap = storage[key] ?: emptyMap() val oldMap = storage[key] ?: emptyMap()
@@ -548,6 +558,7 @@ open class IncrementalJvmCache(
private fun functionNameBySignature(signature: String): String = private fun functionNameBySignature(signature: String): String =
signature.substringBefore("(") signature.substringBefore("(")
@Synchronized
fun remove(className: JvmClassName) { fun remove(className: JvmClassName) {
storage.remove(className.internalName) storage.remove(className.internalName)
} }
@@ -30,6 +30,7 @@ open class BasicMapsOwner(val cachesDir: File) {
protected val String.storageFile: File protected val String.storageFile: File
get() = File(cachesDir, this + "." + CACHE_EXTENSION) get() = File(cachesDir, this + "." + CACHE_EXTENSION)
@Synchronized
protected fun <K, V, M : BasicMap<K, V>> registerMap(map: M): M { protected fun <K, V, M : BasicMap<K, V>> registerMap(map: M): M {
maps.add(map) maps.add(map)
return map return map
@@ -47,6 +48,7 @@ open class BasicMapsOwner(val cachesDir: File) {
forEachMapSafe("flush") { it.flush(memoryCachesOnly) } forEachMapSafe("flush") { it.flush(memoryCachesOnly) }
} }
@Synchronized
private fun forEachMapSafe(actionName: String, action: (BasicMap<*, *>) -> Unit) { private fun forEachMapSafe(actionName: String, action: (BasicMap<*, *>) -> Unit) {
val actionExceptions = LinkedHashMap<String, Exception>() val actionExceptions = LinkedHashMap<String, Exception>()
maps.forEach { maps.forEach {
@@ -66,5 +68,6 @@ open class BasicMapsOwner(val cachesDir: File) {
} }
@TestOnly @TestOnly
@Synchronized
fun dump(): String = maps.joinToString("\n\n") { it.dump() } fun dump(): String = maps.joinToString("\n\n") { it.dump() }
} }
@@ -17,9 +17,11 @@
package org.jetbrains.kotlin.incremental.storage package org.jetbrains.kotlin.incremental.storage
import com.intellij.util.io.DataExternalizer import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor import com.intellij.util.io.KeyDescriptor
import com.intellij.util.io.PersistentHashMap import com.intellij.util.io.PersistentHashMap
import java.io.File import java.io.File
import java.io.IOException
/** /**
@@ -30,7 +32,6 @@ class CachingLazyStorage<K, V>(
private val keyDescriptor: KeyDescriptor<K>, private val keyDescriptor: KeyDescriptor<K>,
private val valueExternalizer: DataExternalizer<V> private val valueExternalizer: DataExternalizer<V>
) : LazyStorage<K, V> { ) : LazyStorage<K, V> {
@Volatile
private var storage: PersistentHashMap<K, V>? = null private var storage: PersistentHashMap<K, V>? = null
@Synchronized @Synchronized
@@ -80,8 +81,10 @@ class CachingLazyStorage<K, V>(
try { try {
storage?.close() storage?.close()
} finally { } finally {
PersistentHashMap.deleteFilesStartingWith(storageFile)
storage = null storage = null
if (!IOUtil.deleteAllFilesStartingWith(storageFile)) {
throw IOException("Could not delete internal storage: ${storageFile.absolutePath}")
}
} }
} }
@@ -20,18 +20,18 @@ import org.jetbrains.kotlin.incremental.dumpCollection
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import java.io.File import java.io.File
internal open class ClassOneToManyMap( internal open class ClassOneToManyMap(storageFile: File) : BasicStringMap<Collection<String>>(storageFile, StringCollectionExternalizer) {
storageFile: File
) : BasicStringMap<Collection<String>>(storageFile, StringCollectionExternalizer) {
override fun dumpValue(value: Collection<String>): String = value.dumpCollection() override fun dumpValue(value: Collection<String>): String = value.dumpCollection()
@Synchronized
fun add(key: FqName, value: FqName) { fun add(key: FqName, value: FqName) {
storage.append(key.asString(), listOf(value.asString())) storage.append(key.asString(), listOf(value.asString()))
} }
operator fun get(key: FqName): Collection<FqName> = operator fun get(key: FqName): Collection<FqName> =
storage[key.asString()]?.map(::FqName) ?: setOf() storage[key.asString()]?.map(::FqName) ?: setOf()
@Synchronized
operator fun set(key: FqName, values: Collection<FqName>) { operator fun set(key: FqName, values: Collection<FqName>) {
if (values.isEmpty()) { if (values.isEmpty()) {
remove(key) remove(key)
@@ -41,10 +41,14 @@ internal open class ClassOneToManyMap(
storage[key.asString()] = values.map(FqName::asString) storage[key.asString()] = values.map(FqName::asString)
} }
@Synchronized
fun remove(key: FqName) { fun remove(key: FqName) {
storage.remove(key.asString()) storage.remove(key.asString())
} }
// Access to caches could be done from multiple threads (e.g. JPS worker and RMI). The underlying collection is already synchronized,
// thus we need synchronization of this method and all modification methods.
@Synchronized
fun removeValues(key: FqName, removed: Set<FqName>) { fun removeValues(key: FqName, removed: Set<FqName>) {
val notRemoved = this[key].filter { it !in removed } val notRemoved = this[key].filter { it !in removed }
this[key] = notRemoved this[key] = notRemoved
@@ -17,9 +17,11 @@
package org.jetbrains.kotlin.incremental.storage package org.jetbrains.kotlin.incremental.storage
import com.intellij.util.io.DataExternalizer import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor import com.intellij.util.io.KeyDescriptor
import com.intellij.util.io.PersistentHashMap import com.intellij.util.io.PersistentHashMap
import java.io.File import java.io.File
import java.io.IOException
class NonCachingLazyStorage<K, V>( class NonCachingLazyStorage<K, V>(
@@ -27,7 +29,6 @@ class NonCachingLazyStorage<K, V>(
private val keyDescriptor: KeyDescriptor<K>, private val keyDescriptor: KeyDescriptor<K>,
private val valueExternalizer: DataExternalizer<V> private val valueExternalizer: DataExternalizer<V>
) : LazyStorage<K, V> { ) : LazyStorage<K, V> {
@Volatile
private var storage: PersistentHashMap<K, V>? = null private var storage: PersistentHashMap<K, V>? = null
@Synchronized @Synchronized
@@ -76,11 +77,12 @@ class NonCachingLazyStorage<K, V>(
override fun clean() { override fun clean() {
try { try {
storage?.close() storage?.close()
} catch (ignored: Throwable) { } finally {
storage = null
if (!IOUtil.deleteAllFilesStartingWith(storageFile)) {
throw IOException("Could not delete internal storage: ${storageFile.absolutePath}")
}
} }
PersistentHashMap.deleteFilesStartingWith(storageFile)
storage = null
} }
@Synchronized @Synchronized
@@ -30,7 +30,12 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
) { ) {
val pathConverter = IncrementalFileToPathConverter(rootProjectDir) val pathConverter = IncrementalFileToPathConverter(rootProjectDir)
private val caches = arrayListOf<BasicMapsOwner>() private val caches = arrayListOf<BasicMapsOwner>()
var isClosed = false
@Synchronized
protected fun <T : BasicMapsOwner> T.registerCache() { protected fun <T : BasicMapsOwner> T.registerCache() {
assert(!isClosed) { "Attempted to add new cache into closed storage." }
caches.add(this) caches.add(this)
} }
@@ -41,9 +46,12 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
val lookupCache: LookupStorage = LookupStorage(lookupCacheDir, pathConverter).apply { registerCache() } val lookupCache: LookupStorage = LookupStorage(lookupCacheDir, pathConverter).apply { registerCache() }
abstract val platformCache: PlatformCache abstract val platformCache: PlatformCache
@Synchronized
fun close(flush: Boolean = false): Boolean { fun close(flush: Boolean = false): Boolean {
if (isClosed) {
return true
}
var successful = true var successful = true
for (cache in caches) { for (cache in caches) {
if (flush) { if (flush) {
try { try {
@@ -62,6 +70,7 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
} }
} }
isClosed = true
return successful return successful
} }
} }
@@ -37,6 +37,7 @@ import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import java.io.File import java.io.File
import java.io.IOException
import java.util.* import java.util.*
abstract class IncrementalCompilerRunner< abstract class IncrementalCompilerRunner<
@@ -97,7 +98,10 @@ abstract class IncrementalCompilerRunner<
val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) } val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) }
return compileIncrementally(args, caches, allKotlinFiles, CompilationMode.Rebuild(reason), messageCollector) return compileIncrementally(args, caches, allKotlinFiles, CompilationMode.Rebuild(reason), messageCollector)
} }
// attempt IC
// if OK or failed compilation - return
// internal error - clear
// failed to close - clear
return try { return try {
val changedFiles = providedChangedFiles ?: caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles) val changedFiles = providedChangedFiles ?: caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector) val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector)
@@ -143,8 +147,8 @@ abstract class IncrementalCompilerRunner<
} }
} }
assert(!destinationDir.exists()) { "Could not delete destination dir $destinationDir" } if (destinationDir.exists()) throw IOException("Could not delete directory $destinationDir.")
assert(!workingDir.exists()) { "Could not delete caches dir $workingDir" } if (workingDir.exists()) throw IOException("Could not delete internal caches in folder $workingDir")
destinationDir.mkdirs() destinationDir.mkdirs()
workingDir.mkdirs() workingDir.mkdirs()
} }
@@ -26,6 +26,7 @@ class FileSnapshotMap(storageFile: File) : BasicStringMap<FileSnapshot>(storageF
override fun dumpValue(value: FileSnapshot): String = override fun dumpValue(value: FileSnapshot): String =
value.toString() value.toString()
@Synchronized
fun compareAndUpdate(newFiles: Iterable<File>): ChangedFiles.Known { fun compareAndUpdate(newFiles: Iterable<File>): ChangedFiles.Known {
val snapshotProvider = SimpleFileSnapshotProviderImpl() val snapshotProvider = SimpleFileSnapshotProviderImpl()
val newOrModified = ArrayList<File>() val newOrModified = ArrayList<File>()
@@ -12,6 +12,7 @@ class SourceToOutputFilesMap(
storageFile: File storageFile: File
) : BasicStringMap<Collection<String>>(storageFile, PathStringDescriptor, StringCollectionExternalizer) { ) : BasicStringMap<Collection<String>>(storageFile, PathStringDescriptor, StringCollectionExternalizer) {
@Synchronized
operator fun set(sourceFile: File, outputFiles: Collection<File>) { operator fun set(sourceFile: File, outputFiles: Collection<File>) {
storage[sourceFile.absolutePath] = outputFiles.map { it.absolutePath } storage[sourceFile.absolutePath] = outputFiles.map { it.absolutePath }
} }
@@ -22,6 +23,7 @@ class SourceToOutputFilesMap(
override fun dumpValue(value: Collection<String>) = override fun dumpValue(value: Collection<String>) =
value.dumpCollection() value.dumpCollection()
@Synchronized
fun remove(file: File): Collection<File> = fun remove(file: File): Collection<File> =
get(file).also { storage.remove(file.absolutePath) } get(file).also { storage.remove(file.absolutePath) }
} }