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,14 +120,16 @@ open class ConfigManager {
}
}
fun resetUserConfig(updateInternalConfig: Boolean = true): ConfigDocument {
private fun createConfigDocumentFromReference(): ConfigDocument {
val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText()
val serverConfigDoc = ConfigDocumentFactory.parseString(serverConfigFileContent)
userConfigFile.writeText(serverConfigDoc.render())
return ConfigDocumentFactory.parseString(serverConfigFileContent)
}
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
}
@@ -135,8 +137,9 @@ open class ConfigManager {
/**
* Makes sure the "UserConfig" is up-to-date.
*
* - adds missing settings
* - removes outdated settings
* - Adds missing settings
* - Migrates deprecated settings
* - Removes outdated settings
*/
fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) {
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
@@ -149,16 +152,17 @@ open class ConfigManager {
}
val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) }
val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 }
val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings
if (!isUserConfigOutdated) {
return
}
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
.entrySet()
.filter {

View File

@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.config4k.getValue
import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JavalinJackson
@@ -144,7 +145,7 @@ fun setupLogLevelUpdating(
)
}
fun migrateConfig(
fun migrateConfigValue(
configDocument: ConfigDocument,
config: Config,
configKey: String,
@@ -168,6 +169,54 @@ fun migrateConfig(
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 =
module {
single { applicationDirs }
@@ -310,37 +359,7 @@ fun applicationSetup() {
}
} else {
// make sure the user config file is up-to-date
GlobalConfigManager.updateUserConfig { config ->
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
}
GlobalConfigManager.updateUserConfig { migrateConfig(this, it) }
}
} catch (e: Exception) {
logger.error(e) { "Exception while creating initial server.conf" }