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

Fix graceful shutdown

This commit is contained in:
carbotaniuman 2021-02-11 15:54:21 -06:00
parent d1d7fcca7f
commit 1c64f05ac6
7 changed files with 85 additions and 61 deletions

View file

@ -17,6 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security ### Security
## [2.0.0-rc10] - 2021-02-11
### Added
- [2021-02-11] Add ability to disable io_uring and epoll [@carbotaniuman].
### Changed
- [2021-02-11] Changed `databaseFile` to `databaseFolder` in run args [@carbotaniuman].
- [2021-02-11] Added more error recovery and logging [@carbotaniuman].
- [2021-02-11] Enforce stricter bounds on disk size [@carbotaniuman].
### Fixed
- [2021-02-11] Fixed issues with Netty not shutting down properly [@carbotaniuman].
## [2.0.0-rc9] - 2021-02-06 ## [2.0.0-rc9] - 2021-02-06
### Added ### Added
- [2021-02-06] Add Micrometer and Resilience4J dashboards to defaults [@_tde9]. - [2021-02-06] Add Micrometer and Resilience4J dashboards to defaults [@_tde9].
@ -341,7 +353,8 @@ This release contains many breaking changes! Of note are the changes to the cach
### Fixed ### Fixed
- [2020-06-11] Tweaked logging configuration to reduce log file sizes by [@carbotaniuman]. - [2020-06-11] Tweaked logging configuration to reduce log file sizes by [@carbotaniuman].
[Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc9...HEAD [Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc10...HEAD
[2.0.0-rc10]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc9...2.0.0-rc10
[2.0.0-rc9]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc8...2.0.0-rc9 [2.0.0-rc9]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc8...2.0.0-rc9
[2.0.0-rc8]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc7...2.0.0-rc8 [2.0.0-rc8]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc7...2.0.0-rc8
[2.0.0-rc7]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc6...2.0.0-rc7 [2.0.0-rc7]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc6...2.0.0-rc7

View file

@ -11,7 +11,7 @@ plugins {
group = "com.mangadex" group = "com.mangadex"
version = "git describe --tags --dirty".execute().text.trim() version = "git describe --tags --dirty".execute().text.trim()
mainClassName = "mdnet.Main" mainClassName = "mdnet.MainKt"
repositories { repositories {
mavenCentral() mavenCentral()

View file

@ -20,8 +20,8 @@ services:
-Dfile-level=off \ -Dfile-level=off \
-Dstdout-level=info \ -Dstdout-level=info \
-jar mangadex_at_home.jar \ -jar mangadex_at_home.jar \
--cache /mangahome/data/images \ --cache /mangahome/data/images/ \
--database /mangahome/data/metadata" --database /mangahome/data/"
] ]
logging: logging:
driver: "json-file" driver: "json-file"

View file

@ -21,7 +21,7 @@ package mdnet
import java.time.Duration import java.time.Duration
object Constants { object Constants {
const val CLIENT_BUILD = 26 const val CLIENT_BUILD = 27
@JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14) @JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14)

View file

@ -23,49 +23,28 @@ import mdnet.logging.error
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import picocli.CommandLine import picocli.CommandLine
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import kotlin.io.path.isRegularFile
import kotlin.system.exitProcess
object Main { fun main(args: Array<String>) {
private val LOGGER = LoggerFactory.getLogger(Main::class.java) CommandLine(Main()).execute(*args)
@JvmStatic
fun main(args: Array<String>) {
try {
CommandLine(ClientArgs()).execute(*args)
} catch (e: Throwable) {
LOGGER.error(e) { "Critical Error " }
}
}
fun dieWithError(e: Throwable): Nothing {
LOGGER.error(e) { "Critical Error" }
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
exitProcess(1)
}
fun dieWithError(error: String): Nothing {
LOGGER.error { "Critical Error: $error" }
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
exitProcess(1)
}
} }
@CommandLine.Command(name = "java -jar <jar>", usageHelpWidth = 120, version = ["Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD})"]) @CommandLine.Command(name = "java -jar <jar>", usageHelpWidth = 120, version = ["Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD})"])
class ClientArgs( class Main : Runnable {
@field:CommandLine.Option(names = ["-s", "--settings"], defaultValue = "settings.yaml", paramLabel = "<settings>", description = ["the settings file (default: \${DEFAULT-VALUE})"]) @field:CommandLine.Option(names = ["-s", "--settings"], defaultValue = ".\\settings.yaml", paramLabel = "<settings>", description = ["the settings file (default: \${DEFAULT-VALUE})"])
var settingsFile: File = File("settings.yaml"), lateinit var settingsFile: File
@field:CommandLine.Option(names = ["-d", "--database"], defaultValue = ".\${sys:file.separator}metadata", paramLabel = "<settings>", description = ["the database file (default: \${DEFAULT-VALUE})"]) @field:CommandLine.Option(names = ["-d", "--database"], defaultValue = ".\\", paramLabel = "<settings>", description = ["the database folder (default: \${DEFAULT-VALUE})"])
var databaseFile: File = File(".${File.separator}metadata"), lateinit var databaseFolder: Path
@field:CommandLine.Option(names = ["-c", "--cache"], defaultValue = "images", paramLabel = "<settings>", description = ["the cache folder (default: \${DEFAULT-VALUE})"]) @field:CommandLine.Option(names = ["-c", "--cache"], defaultValue = ".\\images", paramLabel = "<settings>", description = ["the cache folder (default: \${DEFAULT-VALUE})"])
var cacheFolder: Path = Paths.get("images"), lateinit var cacheFolder: Path
@field:CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["show this help message and exit"]) @field:CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["show this help message and exit"])
var helpRequested: Boolean = false, var helpRequested: Boolean = false
@field:CommandLine.Option(names = ["-v", "--version"], versionHelp = true, description = ["show the version message and exit"]) @field:CommandLine.Option(names = ["-v", "--version"], versionHelp = true, description = ["show the version message and exit"])
var versionRequested: Boolean = false var versionRequested: Boolean = false
) : Runnable {
override fun run() { override fun run() {
println( println(
"Mangadex@Home Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD}) initializing" "Mangadex@Home Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD}) initializing"
@ -89,7 +68,33 @@ class ClientArgs(
""".trimIndent() """.trimIndent()
) )
val client = MangaDexClient(settingsFile, databaseFile, cacheFolder) if (!Files.isDirectory(databaseFolder) || Files.isRegularFile(databaseFolder.resolveSibling(databaseFolder.fileName.toString() + ".mv.db"))) {
println()
println()
println(
"""the --database option now takes in the folder with the database file!
|(it previously took in the path to the file without any extensions)
|if you are using docker update your docker mount settings!
|if you are not, manually move update your --database args!
|note: the database file itself should be named metadata.{extension}
|where {extension} can be `.db` or `.mv.db`
""".trimMargin()
)
println()
println()
throw IllegalArgumentException()
}
if (!Files.isDirectory(databaseFolder)) {
throw IllegalArgumentException("Database folder $databaseFolder must be a directory")
}
if (!Files.isDirectory(cacheFolder)) {
throw IllegalArgumentException("Database folder $cacheFolder must be a directory")
}
val client = MangaDexClient(settingsFile, databaseFolder, cacheFolder)
val hook = Thread { val hook = Thread {
client.shutdown() client.shutdown()
(LoggerFactory.getILoggerFactory() as LoggerContext).stop() (LoggerFactory.getILoggerFactory() as LoggerContext).stop()
@ -101,6 +106,7 @@ class ClientArgs(
try { try {
client.runLoop() client.runLoop()
} catch (e: Throwable) { } catch (e: Throwable) {
LOGGER.error(e) { "Failure when starting main loop" }
Runtime.getRuntime().removeShutdownHook( Runtime.getRuntime().removeShutdownHook(
hook hook
) )
@ -108,4 +114,8 @@ class ClientArgs(
throw e throw e
} }
} }
companion object {
private val LOGGER = LoggerFactory.getLogger(Main::class.java)
}
} }

View file

@ -26,7 +26,6 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import mdnet.Main.dieWithError
import mdnet.cache.ImageStorage import mdnet.cache.ImageStorage
import mdnet.logging.info import mdnet.logging.info
import mdnet.logging.warn import mdnet.logging.warn
@ -46,7 +45,7 @@ import java.util.regex.Pattern
// Exception class to handle when Client Settings have invalid values // Exception class to handle when Client Settings have invalid values
class ClientSettingsException(message: String) : Exception(message) class ClientSettingsException(message: String) : Exception(message)
class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFolder: Path) { class MangaDexClient(private val settingsFile: File, databaseFolder: Path, cacheFolder: Path) {
// this must remain single-threaded because of how the state mechanism works // this must remain single-threaded because of how the state mechanism works
private val executor = Executors.newSingleThreadScheduledExecutor() private val executor = Executors.newSingleThreadScheduledExecutor()
private var scheduledFuture: ScheduledFuture<*>? = null private var scheduledFuture: ScheduledFuture<*>? = null
@ -62,22 +61,13 @@ class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFo
// end protected state // end protected state
init { init {
settings = try { settings = readClientSettings()
readClientSettings()
} catch (e: UnrecognizedPropertyException) {
dieWithError("'${e.propertyName}' is not a valid setting")
} catch (e: JsonProcessingException) {
dieWithError(e)
} catch (e: ClientSettingsException) {
dieWithError(e)
} catch (e: IOException) {
dieWithError(e)
}
LOGGER.info { "Client settings loaded: $settings" } LOGGER.info { "Client settings loaded: $settings" }
val config = HikariConfig() val config = HikariConfig()
config.jdbcUrl = "jdbc:h2:$databaseFile" val db = databaseFolder.resolve("metadata")
config.jdbcUrl = "jdbc:h2:$db"
config.addDataSourceProperty("cachePrepStmts", "true") config.addDataSourceProperty("cachePrepStmts", "true")
config.addDataSourceProperty("prepStmtCacheSize", "100") config.addDataSourceProperty("prepStmtCacheSize", "100")
config.addDataSourceProperty("prepStmtCacheSqlLimit", "1000") config.addDataSourceProperty("prepStmtCacheSqlLimit", "1000")
@ -99,7 +89,6 @@ class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFo
scheduledFuture = executor.scheduleWithFixedDelay( scheduledFuture = executor.scheduleWithFixedDelay(
{ {
try { try {
// this blocks the executor, so no worries about concurrency
reloadClientSettings() reloadClientSettings()
} catch (e: Exception) { } catch (e: Exception) {
LOGGER.warn(e) { "Reload of ClientSettings failed" } LOGGER.warn(e) { "Reload of ClientSettings failed" }
@ -107,13 +96,25 @@ class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFo
}, },
1, 1, TimeUnit.MINUTES 1, 1, TimeUnit.MINUTES
) )
scheduledFuture = executor.scheduleWithFixedDelay(
{
try {
if (imageServer == null) {
LOGGER.info { "Restarting image server that failed to start" }
startImageServer()
}
} catch (e: Exception) {
LOGGER.warn(e) { "Image server restart failed" }
}
},
1, 1, TimeUnit.MINUTES
)
} }
// Precondition: settings must be filled with up-to-date settings // Precondition: settings must be filled with up-to-date settings
private fun startImageServer() { private fun startImageServer() {
if (imageServer != null) { require(imageServer == null) { "imageServer was not null" }
throw AssertionError()
}
LOGGER.info { "Server manager starting" } LOGGER.info { "Server manager starting" }
imageServer = ServerManager( imageServer = ServerManager(
settings, settings,

View file

@ -86,7 +86,7 @@ class ImageServer(
Response(Status.NOT_MODIFIED) Response(Status.NOT_MODIFIED)
.header("Last-Modified", lastModified) .header("Last-Modified", lastModified)
} else { } else {
LOGGER.info { "Request for $sanitizedUri is being served" } LOGGER.info { "Request for $sanitizedUri is being served from cache" }
respondWithImage( respondWithImage(
BufferedInputStream(image.stream), BufferedInputStream(image.stream),
@ -147,7 +147,7 @@ class ImageServer(
} }
respondWithImage(tee, contentLength, contentType, lastModified, false) respondWithImage(tee, contentLength, contentType, lastModified, false)
} else { } else {
LOGGER.info { "Request for $sanitizedUri is being served" } LOGGER.info { "Request for $sanitizedUri is being served due to write errors" }
respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false) respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false)
} }
} }