1
0
Fork 1
mirror of https://gitlab.com/mangadex-pub/mangadex_at_home.git synced 2024-01-19 02:48:37 +00:00

ClientSettings will be immediately copied over for live use.

If the Server does not need to be restarted, but the WebUI does,
the WebUI will be stopped, the ClientSettings will be copied over,
then the WebUI will be started back again (if the new settings
still require a WebUI).

If the Server and WebUI need to be restarted, the WebUI will be
stopped and a Graceful Shutdown will be started follow by a
transition to the 'ReloadClientSettings' state. Once in that state
the ClientSettings will be copied over then both the Server and
WebUI (if the new settings still require a WebUI) will be started
back again.

If the WebUI does not need to be restarted but the Server does,
a Graceful Shutdown will be started followed by a transition to
the 'ReloadClientSettings' state. Once in that state, the
ClientSettings will be copied over then the Server will be started
back
This commit is contained in:
radonbark 2020-07-13 17:42:30 -04:00
parent e2897ec595
commit 0e47dfc7f5
2 changed files with 119 additions and 25 deletions

View file

@ -35,6 +35,8 @@ import mdnet.BuildInfo
import mdnet.base.settings.ClientSettings
import org.slf4j.LoggerFactory
class ClientSettingsException(message: String): Exception(message)
object Main {
private val LOGGER = LoggerFactory.getLogger(Main::class.java)
private val JACKSON: ObjectMapper = jacksonObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).configure(JsonParser.Feature.ALLOW_COMMENTS, true)
@ -67,9 +69,8 @@ object Main {
} else if (args.isNotEmpty()) {
dieWithError("Expected one argument: path to config file, or nothing")
}
val settings = try {
JACKSON.readValue<ClientSettings>(FileReader(file))
JACKSON.readValue<ClientSettings>(FileReader(file)).apply(::validateSettings)
} catch (e: UnrecognizedPropertyException) {
dieWithError("'${e.propertyName}' is not a valid setting")
} catch (e: JsonProcessingException) {
@ -83,10 +84,12 @@ object Main {
dieWithError(e)
}
}
}.apply(::validateSettings)
} catch(e: ClientSettingsException) {
dieWithError(e)
}
LOGGER.info { "Client settings loaded: $settings" }
val client = MangaDexClient(settings)
val client = MangaDexClient(settings, file)
Runtime.getRuntime().addShutdownHook(Thread { client.shutdown() })
client.runLoop()
}
@ -104,29 +107,29 @@ object Main {
exitProcess(1)
}
private fun validateSettings(settings: ClientSettings) {
if (!isSecretValid(settings.clientSecret)) dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
fun validateSettings(settings: ClientSettings) {
if (!isSecretValid(settings.clientSecret)) throw ClientSettingsException("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
if (settings.clientPort == 0) {
dieWithError("Config Error: Invalid port number")
throw ClientSettingsException("Config Error: Invalid port number")
}
if (settings.maxCacheSizeInMebibytes < 1024) {
dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)")
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)")
}
if (settings.threads < 4) {
dieWithError("Config Error: Invalid number of threads, must be >= 4")
throw ClientSettingsException("Config Error: Invalid number of threads, must be >= 4")
}
if (settings.maxMebibytesPerHour < 0) {
dieWithError("Config Error: Max bandwidth must be >= 0")
}
if (settings.maxKilobitsPerSecond < 0) {
dieWithError("Config Error: Max burst rate must be >= 0")
throw ClientSettingsException("Config Error: Max burst rate must be >= 0")
}
if (settings.gracefulShutdownWaitSeconds < 15) {
dieWithError("Config Error: Graceful shutdown wait be >= 15")
throw ClientSettingsException("Config Error: Graceful shutdown wait be >= 15")
}
if (settings.webSettings != null) {
if (settings.webSettings.uiPort == 0) {
dieWithError("Config Error: Invalid UI port number")
throw ClientSettingsException("Config Error: Invalid UI port number")
}
}
}

View file

@ -20,9 +20,12 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
package mdnet.base
import ch.qos.logback.classic.LoggerContext
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import mdnet.base.Main.dieWithError
import java.io.File
import java.io.IOException
import java.time.Instant
@ -32,7 +35,6 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import mdnet.base.Main.dieWithError
import mdnet.base.data.Statistics
import mdnet.base.server.getServer
import mdnet.base.server.getUiServer
@ -42,6 +44,8 @@ import mdnet.cache.DiskLruCache
import mdnet.cache.HeaderMismatchException
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
import java.io.FileReader
import java.io.FileWriter
sealed class State
// server is not running
@ -52,8 +56,10 @@ object Shutdown : State()
data class GracefulShutdown(val lastRunning: Running, val counts: Int = 0, val nextState: State = Uninitialized, val action: () -> Unit = {}) : State()
// server is currently running
data class Running(val server: Http4kServer, val settings: ServerSettings, val minutesToSettingsReload: Int) : State()
class MangaDexClient(private val clientSettings: ClientSettings) {
// server is stopped to reload client settings
data class ReloadClientSettings(val newSettings: ClientSettings, val restartWebUI: Boolean = false): State()
// clientSettings must only be accessed from the thread on the executorService
class MangaDexClient(private var clientSettings: ClientSettings, private val clientSettingsFile: String) {
// this must remain singlethreaded because of how the state mechanism works
private val executorService = Executors.newSingleThreadScheduledExecutor()
// state must only be accessed from the thread on the executorService
@ -73,11 +79,12 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
private var webUi: Http4kServer? = null
private val cache: DiskLruCache
init {
try {
cache = DiskLruCache.open(
File("cache"), 1, 1,
clientSettings.maxCacheSizeInMebibytes * 1024 * 1024 /* MiB to bytes */
File("cache"), 1, 1,
clientSettings.maxCacheSizeInMebibytes * 1024 * 1024 /* MiB to bytes */
)
cache.get("statistics")?.use {
statistics.set(JACKSON.readValue<Statistics>(it.getInputStream(0)))
@ -96,14 +103,14 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
statsMap[Instant.now()] = statistics.get()
if (clientSettings.webSettings != null) {
webUi = getUiServer(clientSettings.webSettings, statistics, statsMap)
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
webUi!!.start()
}
LOGGER.info { "Mangadex@Home Client initialized. Starting normal operation." }
executorService.scheduleAtFixedRate({
try {
if (state is Running || state is GracefulShutdown || state is Uninitialized) {
if (state is Running || state is GracefulShutdown || state is Uninitialized || state is ReloadClientSettings) {
statistics.updateAndGet {
it.copy(bytesOnDisk = cache.size())
}
@ -201,18 +208,34 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
try {
val state = this.state
if (state is Running) {
val minutesToNextReload = state.minutesToSettingsReload
val minutesToNextReload = state.minutesToSettingsReload - 1
if (minutesToNextReload <= 0) {
reloadClientSettings()
this.state = state.copy(minutesToSettingsReload = clientSettings.settingsReloadDelayInMinutes)
}
else {
this.state = state.copy(minutesToSettingsReload = minutesToNextReload - 1)
this.state = state.copy(minutesToSettingsReload = minutesToNextReload)
}
}
else if (state is ReloadClientSettings) {
clientSettings = state.newSettings
LOGGER.info("Reloaded ClientSettings: {}", clientSettings)
LOGGER.info("Starting Server after reloading ClientSettings")
this.state = Uninitialized
loginAndStartServer()
// Start the WebUI if we had to stop it
// and still want it
if(state.restartWebUI && clientSettings.webSettings != null) {
LOGGER.info("Starting WebUI after reloading ClientSettings")
if (webUi == null) {
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
}
webUi!!.start()
}
LOGGER.info { "Time to next client settings reload: " + minutesToNextReload }
}
} catch (e: Exception) {
LOGGER.warn(e) { "Client settings reloader failed" }
LOGGER.warn(e) { "Reload of ClientSettings failed" }
}
}, 1, 1, TimeUnit.MINUTES)
}
@ -321,8 +344,76 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
* Web UI and/or the server if needed
*/
private fun reloadClientSettings() {
val state = this.state
val state = this.state as Running
LOGGER.info { "Reloading client settings" }
try {
val newSettings = JACKSON.readValue<ClientSettings>(FileReader(clientSettingsFile)).apply(Main::validateSettings)
if(newSettings == clientSettings) {
LOGGER.info { "Client Settings have not changed" }
return
}
// Setting loaded without issue. Figure out
// if there are changes that require a restart
val restartServer = newSettings.clientSecret != clientSettings.clientSecret ||
newSettings.clientHostname != clientSettings.clientHostname ||
newSettings.clientPort != clientSettings.clientPort ||
newSettings.clientExternalPort != clientSettings.clientExternalPort ||
newSettings.devSettings?.isDev != clientSettings.devSettings?.isDev
var restartWebUI = newSettings.webSettings != clientSettings.webSettings ||
newSettings.webSettings?.uiHostname != clientSettings.webSettings?.uiHostname ||
newSettings.webSettings?.uiPort != clientSettings.webSettings?.uiPort
// Stop the the WebUI if needed
if(restartWebUI && newSettings.webSettings != null) {
LOGGER.info("Stopping WebUI to reload ClientSettings")
webUi?.stop()
} else if(restartWebUI && newSettings.webSettings == null) {
LOGGER.info("Stopping WebUI to because it is no longer in ClientSettings")
webUi?.close()
webUi = null
// We don't want it so don't restart it
restartWebUI = false
}
if(restartServer) {
// If we are restarting the server
// We must do it gracefully and set
// the new settings later
LOGGER.info("Stopping Server to reload ClientSettings")
this.state = GracefulShutdown(state, nextState = ReloadClientSettings(newSettings, restartWebUI))
} else {
// If we aren't restarting the server
// We can update the settings now
clientSettings = newSettings
LOGGER.info("Reloaded ClientSettings: {}", clientSettings)
}
// Start the WebUI if we had to stop it
// and still want it
if(restartWebUI && clientSettings.webSettings != null) {
LOGGER.info("Starting WebUI after reloading ClientSettings")
if (webUi == null) {
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
}
webUi!!.start()
}
} catch (e: UnrecognizedPropertyException) {
dieWithError("'${e.propertyName}' is not a valid setting")
} catch (e: JsonProcessingException) {
dieWithError(e)
} catch (ignored: IOException) {
ClientSettings().also {
LOGGER.warn("Settings file {} not found, generating file", clientSettingsFile)
try {
FileWriter(clientSettingsFile).use { writer -> JACKSON.writeValue(writer, it) }
} catch (e: IOException) {
dieWithError(e)
}
}
} catch(e: ClientSettingsException) {
LOGGER.warn("Can't reload client settings: " + e.message)
}
}
companion object {