Fix/server startup config update failure handling (#1646)

* Catch config value migration exception

In case the value did not exist in the config a "ConfigException.Missing" exception was thrown which caused the whole migration to fail.

Fixes #1645

* Improve config migration logging

* Update user config file after config update

The user config file gets reset before the update.
This could cause the user settings to get lost on the next server start in case something went wrong during the update and the updated config never got saved to the actual file.
This commit is contained in:
schroda
2025-09-14 16:32:23 +02:00
committed by GitHub
parent 2818fbe575
commit bbd7e30298
2 changed files with 65 additions and 42 deletions

View File

@@ -120,23 +120,26 @@ open class ConfigManager {
} }
} }
fun resetUserConfig(updateInternalConfig: Boolean = true): ConfigDocument { private fun createConfigDocumentFromReference(): ConfigDocument {
val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText() val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText()
val serverConfigDoc = ConfigDocumentFactory.parseString(serverConfigFileContent) return ConfigDocumentFactory.parseString(serverConfigFileContent)
userConfigFile.writeText(serverConfigDoc.render())
if (updateInternalConfig) {
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
} }
fun resetUserConfig(): ConfigDocument {
val serverConfigDoc = createConfigDocumentFromReference()
userConfigFile.writeText(serverConfigDoc.render())
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
return serverConfigDoc return serverConfigDoc
} }
/** /**
* Makes sure the "UserConfig" is up-to-date. * Makes sure the "UserConfig" is up-to-date.
* *
* - adds missing settings * - Adds missing settings
* - removes outdated settings * - Migrates deprecated settings
* - Removes outdated settings
*/ */
fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) { fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) {
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
@@ -149,16 +152,17 @@ open class ConfigManager {
} }
val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) } val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) }
val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 } val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 }
val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings
if (!isUserConfigOutdated) { if (!isUserConfigOutdated) {
return return
} }
logger.debug { logger.debug {
"user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings" "user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings)"
} }
var newUserConfigDoc: ConfigDocument = resetUserConfig(false) var newUserConfigDoc: ConfigDocument = createConfigDocumentFromReference()
userConfig userConfig
.entrySet() .entrySet()
.filter { .filter {

View File

@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.createAppModule import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.config4k.getValue
import io.github.config4k.toConfig import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson
@@ -144,7 +145,7 @@ fun setupLogLevelUpdating(
) )
} }
fun migrateConfig( fun migrateConfigValue(
configDocument: ConfigDocument, configDocument: ConfigDocument,
config: Config, config: Config,
configKey: String, configKey: String,
@@ -168,6 +169,54 @@ fun migrateConfig(
return configDocument return configDocument
} }
fun migrateConfig(
configDocument: ConfigDocument,
config: Config,
): ConfigDocument {
var updatedConfig = configDocument
val settingsRequiringMigration = SettingsRegistry.getAll().filterValues { it.deprecated?.replaceWith != null }
settingsRequiringMigration.forEach { (name, data) ->
val configKey = "server.$name"
val toConfigKey = "server.${data.deprecated!!.replaceWith}"
try {
config.getValue(configKey)
} catch (_: ConfigException) {
// Ignore, no migration required
return@forEach
}
logger.debug { "Migrating config value: $configKey -> $toConfigKey" }
try {
if (data.deprecated!!.migrateConfig != null) {
updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig)
return@forEach
}
if (data.deprecated!!.migrateConfigValue != null) {
updatedConfig =
migrateConfigValue(
updatedConfig,
config,
configKey,
toConfigKey,
data.deprecated!!.migrateConfigValue!!,
)
return@forEach
}
} catch (e: Exception) {
logger.warn(e) { "Failed to migrate config value: $configKey -> $toConfigKey" }
return@forEach
}
shutdownApp(ExitCode.ConfigMigrationMisconfiguredFailure)
}
return updatedConfig
}
fun serverModule(applicationDirs: ApplicationDirs): Module = fun serverModule(applicationDirs: ApplicationDirs): Module =
module { module {
single { applicationDirs } single { applicationDirs }
@@ -310,37 +359,7 @@ fun applicationSetup() {
} }
} else { } else {
// make sure the user config file is up-to-date // make sure the user config file is up-to-date
GlobalConfigManager.updateUserConfig { config -> GlobalConfigManager.updateUserConfig { migrateConfig(this, it) }
var updatedConfig = this
val settingsRequiringMigration = SettingsRegistry.getAll().filterValues { it.deprecated?.replaceWith != null }
settingsRequiringMigration.forEach { (name, data) ->
val configKey = "server.$name"
val toConfigKey = "server.${data.deprecated!!.replaceWith}"
if (data.deprecated!!.migrateConfig != null) {
logger.debug { "Migrating config value: $configKey -> $toConfigKey" }
updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig)
return@forEach
}
if (data.deprecated!!.migrateConfigValue != null) {
updatedConfig =
migrateConfig(
updatedConfig,
config,
configKey,
toConfigKey,
data.deprecated!!.migrateConfigValue!!,
)
return@forEach
}
shutdownApp(ExitCode.ConfigMigrationMisconfiguredFailure)
}
updatedConfig
}
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Exception while creating initial server.conf" } logger.error(e) { "Exception while creating initial server.conf" }