[IC] Fix fallback logic in IncrementalCompilerRunner

The current logic works as follows:
- Try either incremental compilation or non-incremental compilation
- If the above (or any of its surrounding work) fails, fall back to
  non-incremental compilation

This means we may perform non-incremental compilation twice.

This commit will fix that logic so that we fall back to non-incremental
compilation only if *incremental compilation* fails.

A nice consequence of this change is that it also resolves the critical
bugs described at KT-52669 (which occur because the current logic is
flawed).

#KT-52669 Fixed
diff --git a/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildAttribute.kt b/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildAttribute.kt
index 9115e6d..bdfe01a 100644
--- a/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildAttribute.kt
+++ b/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildAttribute.kt
@@ -18,8 +18,9 @@
 enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable {
     NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file not found"),
     NO_ABI_SNAPSHOT(BuildAttributeKind.REBUILD_REASON, "ABI snapshot not found"),
+    INTERNAL_ERROR(BuildAttributeKind.REBUILD_REASON, "Internal error during preparation of IC round"),
     CLASSPATH_SNAPSHOT_NOT_FOUND(BuildAttributeKind.REBUILD_REASON, "Classpath snapshot not found"),
-    CACHE_CORRUPTION(BuildAttributeKind.REBUILD_REASON, "Cache corrupted"),
+    INCREMENTAL_COMPILATION_FAILED(BuildAttributeKind.REBUILD_REASON, "Incremental compilation failed"),
     UNKNOWN_CHANGES_IN_GRADLE_INPUTS(BuildAttributeKind.REBUILD_REASON, "Unknown Gradle changes"),
     JAVA_CHANGE_UNTRACKED_FILE_IS_REMOVED(BuildAttributeKind.REBUILD_REASON, "Untracked Java file is removed"),
     JAVA_CHANGE_UNEXPECTED_PSI(BuildAttributeKind.REBUILD_REASON, "Java PSI file is expected"),
diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCachesManager.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCachesManager.kt
index b70c1a1..a6acd7d 100644
--- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCachesManager.kt
+++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCachesManager.kt
@@ -34,6 +34,7 @@
     private val caches = arrayListOf<BasicMapsOwner>()
 
     var isClosed = false
+    var isSuccessfulyClosed = false
 
     @Synchronized
     protected fun <T : BasicMapsOwner> T.registerCache() {
@@ -52,15 +53,15 @@
     @Synchronized
     fun close(flush: Boolean = false): Boolean {
         if (isClosed) {
-            return true
+            return isSuccessfulyClosed
         }
-        var successful = true
+        isSuccessfulyClosed = true
         for (cache in caches) {
             if (flush) {
                 try {
                     cache.flush(false)
                 } catch (e: Throwable) {
-                    successful = false
+                    isSuccessfulyClosed = false
                     reporter.report { "Exception when flushing cache ${cache.javaClass}: $e" }
                 }
             }
@@ -68,13 +69,13 @@
             try {
                 cache.close()
             } catch (e: Throwable) {
-                successful = false
+                isSuccessfulyClosed = false
                 reporter.report { "Exception when closing cache ${cache.javaClass}: $e" }
             }
         }
 
         isClosed = true
-        return successful
+        return isSuccessfulyClosed
     }
 }
 
diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt
index eadf86c..dd9a0e8 100644
--- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt
+++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt
@@ -71,7 +71,53 @@
         providedChangedFiles: ChangedFiles?,
         projectDir: File? = null
     ): ExitCode = reporter.measure(BuildTime.INCREMENTAL_COMPILATION_DAEMON) {
-        compileImpl(allSourceFiles, args, messageCollector, providedChangedFiles, projectDir)
+        try {
+            compileImpl(allSourceFiles, args, messageCollector, providedChangedFiles, projectDir)
+        } finally {
+            reporter.measure(BuildTime.CALCULATE_OUTPUT_SIZE) {
+                reporter.addMetric(
+                    BuildPerformanceMetric.SNAPSHOT_SIZE,
+                    buildHistoryFile.length() + lastBuildInfoFile.length() + abiSnapshotFile.length()
+                )
+                if (cacheDirectory.exists() && cacheDirectory.isDirectory()) {
+                    cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let {
+                        reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, it)
+                    }
+                }
+            }
+        }
+    }
+
+    fun rebuild(
+        reason: BuildAttribute,
+        allSourceFiles: List<File>,
+        args: Args,
+        messageCollector: MessageCollector,
+        providedChangedFiles: ChangedFiles?,
+        projectDir: File? = null,
+        classpathAbiSnapshot: Map<String, AbiSnapshot>
+    ): ExitCode {
+        reporter.report { "Non-incremental compilation will be performed: $reason" }
+        reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
+            cleanOutputsAndLocalStateOnRebuild(args)
+        }
+        val caches = createCacheManager(args, projectDir)
+        try {
+            if (providedChangedFiles == null) {
+                caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
+            }
+            val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) }
+            return compileIncrementally(
+                args, caches, allKotlinFiles, CompilationMode.Rebuild(reason), messageCollector, withAbiSnapshot,
+                classpathAbiSnapshot = classpathAbiSnapshot
+            ).also {
+                if (it == ExitCode.OK) {
+                    performWorkAfterSuccessfulCompilation(caches)
+                }
+            }
+        } finally {
+            caches.close(true)
+        }
     }
 
     private fun compileImpl(
@@ -82,13 +128,11 @@
         projectDir: File? = null
     ): ExitCode {
         var caches = createCacheManager(args, projectDir)
+        var rebuildReason = BuildAttribute.INTERNAL_ERROR
 
-        if (withAbiSnapshot) {
-            reporter.report { "Incremental compilation with ABI snapshot enabled" }
-        }
-        //TODO if abi-snapshot is corrupted unable to rebuild. Should roll back to withSnapshot = false?
         val classpathAbiSnapshot =
             if (withAbiSnapshot) {
+                reporter.report { "Incremental compilation with ABI snapshot enabled" }
                 reporter.measure(BuildTime.SET_UP_ABI_SNAPSHOTS) {
                     setupJarDependencies(args, withAbiSnapshot, reporter)
                 }
@@ -96,27 +140,7 @@
                 emptyMap()
             }
 
-        fun rebuild(reason: BuildAttribute): ExitCode {
-            reporter.report { "Non-incremental compilation will be performed: $reason" }
-            caches.close(false)
-            // todo: we can recompile all files incrementally (not cleaning caches), so rebuild won't propagate
-            reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
-                cleanOutputsAndLocalStateOnRebuild(args)
-            }
-            caches = createCacheManager(args, projectDir)
-            if (providedChangedFiles == null) {
-                caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
-            }
-            val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) }
-            return compileIncrementally(
-                args, caches, allKotlinFiles, CompilationMode.Rebuild(reason), messageCollector, withAbiSnapshot,
-                classpathAbiSnapshot = classpathAbiSnapshot
-            )
-        }
-
-        // If compilation has crashed or we failed to close caches we have to clear them
-        var cachesMayBeCorrupted = true
-        return try {
+        try {
             val changedFiles = when (providedChangedFiles) {
                 is ChangedFiles.Dependencies -> {
                     val changedSources = caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
@@ -129,75 +153,51 @@
                 else -> providedChangedFiles
             }
 
-            @Suppress("MoveVariableDeclarationIntoWhen")
-            val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
+            var compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
+            val abiSnapshot = if (compilationMode is CompilationMode.Incremental && withAbiSnapshot) {
+                AbiSnapshotImpl.read(abiSnapshotFile, reporter)
+            } else {
+                if (withAbiSnapshot) {
+                    compilationMode = CompilationMode.Rebuild(BuildAttribute.NO_ABI_SNAPSHOT)
+                }
+                null
+            }
 
-            val exitCode = when (compilationMode) {
+            when (compilationMode) {
                 is CompilationMode.Incremental -> {
-                    if (withAbiSnapshot) {
-                        val abiSnapshot = AbiSnapshotImpl.read(abiSnapshotFile, reporter)
-                        if (abiSnapshot != null) {
+                    try {
+                        val exitCode = if (withAbiSnapshot) {
                             compileIncrementally(
-                                args,
-                                caches,
-                                allSourceFiles,
-                                compilationMode,
-                                messageCollector,
-                                withAbiSnapshot,
-                                abiSnapshot,
-                                classpathAbiSnapshot
+                                args, caches, allSourceFiles, compilationMode, messageCollector,
+                                withAbiSnapshot, abiSnapshot!!, classpathAbiSnapshot
                             )
                         } else {
-                            rebuild(BuildAttribute.NO_ABI_SNAPSHOT)
+                            compileIncrementally(args, caches, allSourceFiles, compilationMode, messageCollector, withAbiSnapshot)
                         }
-                    } else {
-                        compileIncrementally(
-                            args,
-                            caches,
-                            allSourceFiles,
-                            compilationMode,
-                            messageCollector,
-                            withAbiSnapshot
-                        )
+                        if (exitCode == ExitCode.OK) {
+                            performWorkAfterSuccessfulCompilation(caches)
+                        }
+                        return exitCode
+                    } catch (e: Throwable) {
+                        reporter.report {
+                            "Incremental compilation failed: ${e.stackTraceToString()}.\nFalling back to non-incremental compilation."
+                        }
+                        rebuildReason = BuildAttribute.INCREMENTAL_COMPILATION_FAILED
                     }
                 }
-                is CompilationMode.Rebuild -> {
-                    rebuild(compilationMode.reason)
-                }
+                is CompilationMode.Rebuild -> rebuildReason = compilationMode.reason
             }
-
-            if (exitCode == ExitCode.OK) {
-                performWorkAfterSuccessfulCompilation(caches)
-            }
-
-            if (!caches.close(flush = true)) throw RuntimeException("Could not flush caches")
-            // Here we should analyze exit code of compiler. E.g. compiler failure should lead to caches rebuild,
-            // but now JsKlib compiler reports invalid exit code.
-            cachesMayBeCorrupted = false
-
-            reporter.measure(BuildTime.CALCULATE_OUTPUT_SIZE) {
-                reporter.addMetric(
-                    BuildPerformanceMetric.SNAPSHOT_SIZE,
-                    buildHistoryFile.length() + lastBuildInfoFile.length() + abiSnapshotFile.length()
-                )
-                if (cacheDirectory.exists() && cacheDirectory.isDirectory()) {
-                    cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let {
-                        reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, it)
-                    }
-                }
-            }
-            return exitCode
-        } catch (e: Exception) { // todo: catch only cache corruption
-            // todo: warn?
-            reporter.report { "Possible caches corruption: $e" }
-            rebuild(BuildAttribute.CACHE_CORRUPTION).also {
-                cachesMayBeCorrupted = false
+        } catch (e: Exception) {
+            reporter.report {
+                "Incremental compilation analysis failed: ${e.stackTraceToString()}.\nFalling back to non-incremental compilation."
             }
         } finally {
-            if (cachesMayBeCorrupted) {
+            if (!caches.close()) {
+                reporter.report { "Unable to close IC caches. Cleaning internal state" }
                 cleanOutputsAndLocalStateOnRebuild(args)
             }
         }
+        return rebuild(rebuildReason, allSourceFiles, args, messageCollector, providedChangedFiles, projectDir, classpathAbiSnapshot)
     }
 
     /**
@@ -411,7 +411,10 @@
                 break
             }
 
-            val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getDirtyData(listOf(caches.platformCache), reporter)
+            val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getDirtyData(
+                listOf(caches.platformCache),
+                reporter
+            )
             val compiledInThisIterationSet = sourcesToCompile.toHashSet()
 
             val forceToRecompileFiles = mapClassesFqNamesToFiles(listOf(caches.platformCache), forceRecompile, reporter)
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt
index 06ff14b..3adbdbe 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt
@@ -700,7 +700,7 @@
             }
 
             build("assemble") {
-                assertOutputContains("Non-incremental compilation will be performed: CACHE_CORRUPTION")
+                assertOutputContains("Non-incremental compilation will be performed: INCREMENTAL_COMPILATION_FAILED")
             }
 
             val lookupFile = projectPath.resolve("lib/build/kotlin/${compileKotlinTaskName}/cacheable/${compileCacheFolderName}/lookups/file-to-id.tab")