flush on disk committed module components state in the same write action as for project components

SaveResult introduced because coroutines can executes on different threads — so, lock free approach is used (coroutine returns own result and it's appended to total result (synchronously, for simplicity, because it is enough and no need to complicate))
This commit is contained in:
Vladimir Krivosheev
2019-01-12 20:41:33 +01:00
parent 12efe3a3e7
commit f73755001c
12 changed files with 185 additions and 100 deletions
@@ -7,7 +7,6 @@ import com.intellij.openapi.application.PathManager
import com.intellij.openapi.application.appSystemDir
import com.intellij.openapi.components.*
import com.intellij.openapi.components.impl.stores.FileStorageCoreUtil
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.openapi.diagnostic.runAndLogException
import com.intellij.openapi.util.NamedJDOMExternalizable
import com.intellij.util.io.systemIndependentPath
@@ -35,24 +34,19 @@ class ApplicationStoreImpl(private val application: Application, pathMacroManage
storageManager.addMacro(StoragePathMacros.CACHE_FILE, appSystemDir.resolve("workspace").resolve("app.xml").systemIndependentPath)
}
override suspend fun doSave(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean) {
val saveSessionManager = saveSettingsSavingComponentsAndCommitComponents(errors, isForceSavingAllSettings)
override suspend fun doSave(result: SaveResult, isForceSavingAllSettings: Boolean) {
val saveSessionManager = saveSettingsSavingComponentsAndCommitComponents(result, isForceSavingAllSettings)
// todo can we store default project in parallel to regular saving? for now only flush on disk is async, but not component committing
coroutineScope {
launch {
saveSessionManager.save(readonlyFiles, errors)
saveSessionManager.save().appendTo(result)
}
launch {
// here, because no Project (and so, ProjectStoreImpl) on Welcome Screen
serviceIfCreated<DefaultProjectExportableAndSaveTrigger>()?.save(errors, readonlyFiles, isForceSavingAllSettings)
}
}
}
override fun createSaveSessionProducerManager(): SaveSessionProducerManager {
return object : SaveSessionProducerManager() {
override suspend fun save(readonlyFiles: MutableList<SaveSessionAndFile>, errors: MutableList<Throwable>): Boolean {
return super.save(readonlyFiles, errors)
val r = serviceIfCreated<DefaultProjectExportableAndSaveTrigger>()?.save(isForceSavingAllSettings) ?: return@launch
// ignore
r.isChanged = false
r.appendTo(result)
}
}
}
@@ -13,7 +13,6 @@ import com.intellij.openapi.components.*
import com.intellij.openapi.components.StateStorageChooserEx.Resolution
import com.intellij.openapi.components.impl.ComponentManagerImpl
import com.intellij.openapi.components.impl.stores.IComponentStore
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.openapi.components.impl.stores.UnknownMacroNotification
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
@@ -32,7 +31,6 @@ import com.intellij.util.SmartList
import com.intellij.util.SystemProperties
import com.intellij.util.containers.SmartHashSet
import com.intellij.util.containers.isNullOrEmpty
import com.intellij.util.lang.CompoundRuntimeException
import com.intellij.util.messages.MessageBus
import com.intellij.util.xmlb.XmlSerializerUtil
import gnu.trove.THashMap
@@ -131,16 +129,24 @@ abstract class ComponentStoreImpl : IComponentStore {
}
final override suspend fun save(isForceSavingAllSettings: Boolean) {
val errors = SmartList<Throwable>()
val readonlyFiles = SmartList<SaveSessionAndFile>()
doSave(errors, readonlyFiles, isForceSavingAllSettings)
CompoundRuntimeException.throwIfNotEmpty(errors)
val result = SaveResult()
doSave(result, isForceSavingAllSettings)
result.throwIfErrored()
}
internal abstract suspend fun doSave(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean)
internal abstract suspend fun doSave(result: SaveResult, isForceSavingAllSettings: Boolean)
internal suspend fun createSaveSessionManagerAndSaveComponents(saveResult: SaveResult, isForceSavingAllSettings: Boolean): SaveSessionProducerManager {
return withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
val errors = SmartList<Throwable>()
val manager = doCreateSaveSessionManagerAndSaveComponents(isForceSavingAllSettings, errors)
saveResult.addErrors(errors)
manager
}
}
@CalledInAwt
internal fun createSaveSessionManagerAndSaveComponents(isForce: Boolean, errors: MutableList<Throwable>): SaveSessionProducerManager {
internal fun doCreateSaveSessionManagerAndSaveComponents(isForce: Boolean, errors: MutableList<Throwable>): SaveSessionProducerManager {
val saveSessionProducerManager = createSaveSessionProducerManager()
saveComponents(isForce, saveSessionProducerManager, errors)
return saveSessionProducerManager
@@ -218,12 +224,12 @@ abstract class ComponentStoreImpl : IComponentStore {
commitComponent(externalizationSession, ComponentInfoImpl(component, stateSpec), null)
val absolutePath = Paths.get(storageManager.expandMacros(findNonDeprecated(stateSpec.storages).path)).toAbsolutePath().toString()
runUndoTransparentWriteAction {
val errors: MutableList<Throwable> = SmartList<Throwable>()
val saveResult = SaveResult()
val newDisposable = Disposer.newDisposable()
try {
VfsRootAccess.allowRootAccess(newDisposable, absolutePath)
runBlocking {
val isSomethingChanged = externalizationSession.save(errors = errors)
val isSomethingChanged = externalizationSession.save().isChanged
if (!isSomethingChanged) {
LOG.info("saveApplicationComponent is called for ${stateSpec.name} but nothing to save")
}
@@ -232,7 +238,7 @@ abstract class ComponentStoreImpl : IComponentStore {
finally {
Disposer.dispose(newDisposable)
}
CompoundRuntimeException.throwIfNotEmpty(errors)
saveResult.throwIfErrored()
}
}
@@ -583,10 +589,9 @@ private fun notifyUnknownMacros(store: IComponentStore, project: Project, compon
// to make sure that ApplicationStore or ProjectStore will not call incomplete doSave implementation
// (because these stores combine several calls for better control/async instead of simple sequential delegation)
abstract class ChildlessComponentStore : ComponentStoreImpl() {
override suspend fun doSave(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean) {
withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
createSaveSessionManagerAndSaveComponents(isForceSavingAllSettings, errors)
}
.save(readonlyFiles, errors)
override suspend fun doSave(result: SaveResult, isForceSavingAllSettings: Boolean) {
createSaveSessionManagerAndSaveComponents(result, isForceSavingAllSettings)
.save()
.appendTo(result)
}
}
@@ -5,10 +5,10 @@ import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.async.coroutineDispatchingContext
import com.intellij.openapi.components.SettingsSavingComponent
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.util.SmartList
import com.intellij.util.containers.ContainerUtil
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class ComponentStoreWithExtraComponents : ComponentStoreImpl() {
@Suppress("DEPRECATION")
@@ -27,31 +27,34 @@ abstract class ComponentStoreWithExtraComponents : ComponentStoreImpl() {
super.initComponent(component, isService)
}
internal suspend fun saveSettingsSavingComponentsAndCommitComponents(errors: MutableList<Throwable>, isForceSavingAllSettings: Boolean): SaveSessionProducerManager {
internal suspend fun saveSettingsSavingComponentsAndCommitComponents(result: SaveResult, isForceSavingAllSettings: Boolean): SaveSessionProducerManager {
coroutineScope {
// expects EDT
launch(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
@Suppress("Duplicates")
val errors = SmartList<Throwable>()
for (settingsSavingComponent in settingsSavingComponents) {
runAndCollectException(errors) {
settingsSavingComponent.save()
}
}
result.addErrors(errors)
}
launch {
val errors = SmartList<Throwable>()
for (settingsSavingComponent in asyncSettingsSavingComponents) {
runAndCollectException(errors) {
settingsSavingComponent.save()
}
}
result.addErrors(errors)
}
}
// SchemeManager (old settingsSavingComponent) must be saved before saving components (component state uses scheme manager in an ipr project, so, we must save it before)
// so, call sequentially it, not inside coroutineScope
return withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
createSaveSessionManagerAndSaveComponents(isForceSavingAllSettings, errors)
}
return createSaveSessionManagerAndSaveComponents(result, isForceSavingAllSettings)
}
}
@@ -3,7 +3,6 @@ package com.intellij.configurationStore
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.openapi.project.Project
import org.jdom.Element
import java.io.Writer
@@ -104,8 +103,10 @@ internal class DefaultProjectExportableAndSaveTrigger {
@Volatile
var project: Project? = null
suspend fun save(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean) {
val project = project ?: return
(project.stateStore as ComponentStoreImpl).doSave(errors, readonlyFiles, isForceSavingAllSettings)
suspend fun save(isForceSavingAllSettings: Boolean): SaveResult {
val project = project ?: return SaveResult.EMPTY
val result = SaveResult()
(project.stateStore as ComponentStoreImpl).doSave(result, isForceSavingAllSettings)
return result
}
}
@@ -319,7 +319,8 @@ private fun deleteFile(file: Path, requestor: Any, virtualFile: VirtualFile?) {
else {
throw ReadOnlyModificationException(virtualFile, object : SaveSession {
override fun save() {
deleteFile(requestor, virtualFile)
// caller must wraps into undo transparent and write action
virtualFile.delete(requestor)
}
})
}
@@ -3,52 +3,74 @@ package com.intellij.configurationStore
import com.intellij.notification.Notifications
import com.intellij.notification.NotificationsManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.impl.stores.IComponentStore
import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.async.coroutineDispatchingContext
import com.intellij.openapi.application.async.inUndoTransparentAction
import com.intellij.openapi.application.async.inWriteAction
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.impl.ProjectManagerImpl.UnableToSaveProjectNotification
import com.intellij.openapi.vfs.ReadonlyStatusHandler
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.SmartList
import com.intellij.util.containers.mapSmart
import com.intellij.util.lang.CompoundRuntimeException
import kotlinx.coroutines.withContext
internal class ProjectSaveSessionProducerManager(private val project: Project) : SaveSessionProducerManager() {
override suspend fun save(readonlyFiles: MutableList<SaveSessionAndFile>, errors: MutableList<Throwable>): Boolean {
val isChanged = super.save(readonlyFiles, errors)
suspend fun saveWithAdditionalSaveSessions(extraSessions: List<SaveSession>): SaveResult {
val saveSessions = SmartList<SaveSession>()
collectSaveSessions(saveSessions)
if (saveSessions.isEmpty() && extraSessions.isEmpty()) {
return SaveResult.EMPTY
}
val saveResult = withContext(AppUIExecutor.onUiThread().inUndoTransparentAction().inWriteAction().coroutineDispatchingContext()) {
val r = SaveResult()
saveSessions(extraSessions, r)
saveSessions(saveSessions, r)
r
}
validate(saveResult)
return saveResult
}
private suspend fun validate(saveResult: SaveResult) {
val notifications = getUnableToSaveNotifications()
val readonlyFiles = saveResult.readonlyFiles
if (readonlyFiles.isEmpty()) {
notifications.forEach { it.expire() }
return isChanged
return
}
if (!notifications.isEmpty()) {
throw IComponentStore.SaveCancelledException()
throw UnresolvedReadOnlyFilesException(readonlyFiles.mapSmart { it.file })
}
val status = runReadAction {
val status = withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(getFilesList(readonlyFiles))
}
if (status.hasReadonlyFiles()) {
dropUnableToSaveProjectNotification(project, status.readonlyFiles.toList())
throw IComponentStore.SaveCancelledException()
val unresolvedReadOnlyFiles = status.readonlyFiles.toList()
dropUnableToSaveProjectNotification(project, unresolvedReadOnlyFiles)
saveResult.addError(UnresolvedReadOnlyFilesException(unresolvedReadOnlyFiles))
return
}
val oldList = readonlyFiles.toTypedArray()
readonlyFiles.clear()
for (entry in oldList) {
executeSave(entry.session, readonlyFiles, errors)
}
CompoundRuntimeException.throwIfNotEmpty(errors)
withContext(AppUIExecutor.onUiThread().inUndoTransparentAction().inWriteAction().coroutineDispatchingContext()) {
val r = SaveResult()
for (entry in oldList) {
executeSave(entry.session, r)
}
r
}.appendTo(saveResult)
if (!readonlyFiles.isEmpty()) {
dropUnableToSaveProjectNotification(project, getFilesList(readonlyFiles))
throw IComponentStore.SaveCancelledException()
saveResult.addError(UnresolvedReadOnlyFilesException(readonlyFiles.mapSmart { it.file }))
}
return isChanged
}
private fun dropUnableToSaveProjectNotification(project: Project, readOnlyFiles: List<VirtualFile>) {
@@ -6,13 +6,10 @@ import com.intellij.ide.highlighter.WorkspaceFileType
import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.async.coroutineDispatchingContext
import com.intellij.openapi.application.async.inUndoTransparentAction
import com.intellij.openapi.application.async.inWriteAction
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.*
import com.intellij.openapi.components.impl.stores.IComponentStore
import com.intellij.openapi.components.impl.stores.IProjectStore
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.openapi.diagnostic.runAndLogException
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
@@ -329,7 +326,7 @@ private open class ProjectStoreImpl(project: Project, private val pathMacroManag
}
}
override suspend fun doSave(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean) = coroutineScope<Unit> {
final override suspend fun doSave(result: SaveResult, isForceSavingAllSettings: Boolean) {
coroutineScope {
launch {
try {
@@ -340,43 +337,42 @@ private open class ProjectStoreImpl(project: Project, private val pathMacroManag
}
}
launch {
saveSettingsSavingComponentsAndCommitComponents(errors, isForceSavingAllSettings).save(readonlyFiles, errors)
// save modules before project
val errors = SmartList<Throwable>()
val moduleSaveSessions = saveModules(errors, isForceSavingAllSettings)
result.addErrors(errors)
(saveSettingsSavingComponentsAndCommitComponents(result, isForceSavingAllSettings) as ProjectSaveSessionProducerManager)
.saveWithAdditionalSaveSessions(moduleSaveSessions)
.appendTo(result)
}
}
}
protected open suspend fun saveModules(errors: MutableList<Throwable>, isForceSavingAllSettings: Boolean): List<SaveSession> {
return emptyList()
}
final override fun createSaveSessionProducerManager() = ProjectSaveSessionProducerManager(project)
}
private class ProjectWithModulesStoreImpl(project: Project, pathMacroManager: PathMacroManager) : ProjectStoreImpl(project, pathMacroManager) {
override suspend fun doSave(errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForceSavingAllSettings: Boolean) {
// save modules before project
saveModules(ModuleManager.getInstance(project)?.modules ?: Module.EMPTY_ARRAY, errors, readonlyFiles, isForceSavingAllSettings)
super.doSave(errors, readonlyFiles, isForceSavingAllSettings)
}
private suspend fun saveModules(modules: Array<Module>, errors: MutableList<Throwable>, readonlyFiles: MutableList<SaveSessionAndFile>, isForce: Boolean) {
override suspend fun saveModules(errors: MutableList<Throwable>, isForceSavingAllSettings: Boolean): List<SaveSession> {
val modules = ModuleManager.getInstance(project)?.modules ?: Module.EMPTY_ARRAY
if (modules.isEmpty()) {
return
return emptyList()
}
withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
return withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
// do no create with capacity because very rarely a lot of modules will be modified
val saveSessions: MutableList<SaveSession> = SmartList<SaveSession>()
// commit components
for (module in modules) {
val moduleStore = ModuleServiceManager.getService(module, IComponentStore::class.java) as ComponentStoreImpl
// collectSaveSessions is very cheap, so, do it in EDT
moduleStore.createSaveSessionManagerAndSaveComponents(isForce, errors).collectSaveSessions(saveSessions)
}
if (!saveSessions.isEmpty()) {
withContext(AppUIExecutor.onUiThread().inUndoTransparentAction().inWriteAction().coroutineDispatchingContext()) {
// flush on disk
saveSessions(saveSessions, readonlyFiles, errors)
}
moduleStore.doCreateSaveSessionManagerAndSaveComponents(isForceSavingAllSettings, errors).collectSaveSessions(saveSessions)
}
saveSessions
}
}
}
@@ -12,11 +12,11 @@ import com.intellij.util.SmartList
import kotlinx.coroutines.withContext
import java.util.*
interface SaveExecutor {
internal interface SaveExecutor {
/**
* @return was something really saved
*/
suspend fun save(readonlyFiles: MutableList<SaveSessionAndFile> = SmartList(), errors: MutableList<Throwable>): Boolean
suspend fun save(): SaveResult
}
internal open class SaveSessionProducerManager : SaveExecutor {
@@ -56,44 +56,48 @@ internal open class SaveSessionProducerManager : SaveExecutor {
}
}
override suspend fun save(readonlyFiles: MutableList<SaveSessionAndFile>, errors: MutableList<Throwable>): Boolean {
override suspend fun save(): SaveResult {
val saveSessions = SmartList<SaveSession>()
collectSaveSessions(saveSessions)
if (saveSessions.isEmpty()) {
return false
return SaveResult.EMPTY
}
val task = {
val result = SaveResult()
saveSessions(saveSessions, result)
result
}
val task = { saveSessions(saveSessions, readonlyFiles, errors) }
if (isVfsRequired) {
withContext(AppUIExecutor.onUiThread().inUndoTransparentAction().inWriteAction().coroutineDispatchingContext()) {
return withContext(AppUIExecutor.onUiThread().inUndoTransparentAction().inWriteAction().coroutineDispatchingContext()) {
task()
}
}
else {
task()
return task()
}
return true
}
}
internal fun saveSessions(saveSessions: MutableList<SaveSession>, readonlyFiles: MutableList<SaveSessionAndFile>, errors: MutableList<Throwable>) {
internal fun saveSessions(saveSessions: List<SaveSession>, result: SaveResult) {
for (saveSession in saveSessions) {
executeSave(saveSession, readonlyFiles, errors)
executeSave(saveSession, result)
}
}
internal fun executeSave(session: SaveSession, readonlyFiles: MutableList<SaveSessionAndFile>, errors: MutableList<Throwable>) {
internal fun executeSave(session: SaveSession, result: SaveResult) {
try {
session.save()
}
catch (e: ReadOnlyModificationException) {
LOG.warn(e)
readonlyFiles.add(SaveSessionAndFile(e.session ?: session, e.file))
result.addReadOnlyFile(SaveSessionAndFile(e.session ?: session, e.file))
}
catch (e: ProcessCanceledException) {
throw e
}
catch (e: Exception) {
errors.add(e)
result.addError(e)
}
}
@@ -0,0 +1,59 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.configurationStore
import com.intellij.openapi.components.impl.stores.SaveSessionAndFile
import com.intellij.util.SmartList
import com.intellij.util.lang.CompoundRuntimeException
internal class SaveResult {
companion object {
val EMPTY = SaveResult()
}
private val errors: MutableList<Throwable> = SmartList()
val readonlyFiles: MutableList<SaveSessionAndFile> = SmartList()
@Suppress("MemberVisibilityCanBePrivate")
var isChanged = false
@Synchronized
fun addError(error: Throwable) {
errors.add(error)
}
@Synchronized
fun addReadOnlyFile(info: SaveSessionAndFile) {
readonlyFiles.add(info)
}
fun addErrors(list: List<Throwable>) {
if (list.isEmpty()) {
return
}
synchronized(this) {
errors.addAll(list)
}
}
@Synchronized
fun appendTo(saveResult: SaveResult) {
if (this === EMPTY) {
return
}
synchronized(saveResult) {
saveResult.errors.addAll(errors)
saveResult.readonlyFiles.addAll(readonlyFiles)
if (isChanged) {
saveResult.isChanged = isChanged
}
}
}
@Synchronized
fun throwIfErrored() {
CompoundRuntimeException.throwIfNotEmpty(errors)
}
}
@@ -1,10 +1,11 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.configurationStore
import com.intellij.openapi.components.StateStorage
import com.intellij.openapi.components.impl.stores.BatchUpdateListener
import com.intellij.openapi.diagnostic.debugOrInfoIfTestMode
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.messages.MessageBus
import org.jdom.Element
import java.util.concurrent.atomic.AtomicReference
@@ -83,4 +84,6 @@ inline fun <T> runBatchUpdate(messageBus: MessageBus, runnable: () -> T): T {
finally {
publisher.onBatchUpdateFinished()
}
}
}
class UnresolvedReadOnlyFilesException(val files: List<VirtualFile>) : RuntimeException()
@@ -13,7 +13,6 @@ import com.intellij.openapi.application.ex.ApplicationManagerEx
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.impl.stores.IComponentStore
import com.intellij.openapi.components.stateStore
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileDocumentManager
@@ -28,7 +27,7 @@ import kotlinx.coroutines.runBlocking
private val LOG = Logger.getInstance("#com.intellij.openapi.components.impl.stores.StoreUtil")
/**
* Consider to not use this method in tests, for tests direct saving of state store is ok.
* Do not use this method in tests, instead directly save using state store.
*/
@JvmOverloads
fun saveSettings(componentManager: ComponentManager, isForceSavingAllSettings: Boolean = false) {
@@ -39,7 +38,7 @@ fun saveSettings(componentManager: ComponentManager, isForceSavingAllSettings: B
componentManager.stateStore.save(isForceSavingAllSettings = isForceSavingAllSettings)
}
}
catch (e: IComponentStore.SaveCancelledException) {
catch (e: UnresolvedReadOnlyFilesException) {
LOG.info(e)
}
catch (e: Throwable) {
@@ -24,8 +24,6 @@ interface IComponentStore {
fun isReloadPossible(componentNames: Set<String>): Boolean
class SaveCancelledException : RuntimeException()
suspend fun save(isForceSavingAllSettings: Boolean = false)
@TestOnly