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

336 lines
12 KiB
Kotlin
Raw Normal View History

2021-01-24 04:55:11 +00:00
/*
Mangadex@Home
Copyright (c) 2020, MangaDex Network
This file is part of MangaDex@Home.
MangaDex@Home is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MangaDex@Home is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
2021-01-25 02:25:49 +00:00
*/
2021-01-24 04:55:11 +00:00
package mdnet
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import mdnet.cache.ImageStorage
import mdnet.data.Statistics
import mdnet.logging.error
import mdnet.logging.info
import mdnet.logging.warn
import mdnet.metrics.DefaultMicrometerMetrics
import mdnet.server.getServer
import mdnet.settings.ClientSettings
import mdnet.settings.RemoteSettings
2021-03-11 20:09:14 +00:00
import okhttp3.ConnectionPool
2021-03-02 18:22:24 +00:00
import okhttp3.OkHttpClient
import okhttp3.Protocol
import org.http4k.client.OkHttp
2021-01-28 14:17:24 +00:00
import org.http4k.core.BodyMode
import org.http4k.core.then
import org.http4k.filter.ClientFilters
import org.http4k.filter.MicrometerMetrics
2021-01-24 04:55:11 +00:00
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
2021-03-02 18:22:24 +00:00
import java.time.Duration
2021-01-24 04:55:11 +00:00
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
sealed class State
// server is not running
2021-01-25 18:22:07 +00:00
object Uninitialized : State()
2021-01-24 04:55:11 +00:00
// server has shut down
object Shutdown : State()
// server is in the process of stopping
data class GracefulStop(
val lastRunning: Running,
val counts: Int = 0,
2021-02-11 15:11:03 +00:00
val nextState: State,
2021-01-24 04:55:11 +00:00
val action: () -> Unit = {}
) : State()
// server is currently running
2021-01-25 18:22:07 +00:00
data class Running(val server: Http4kServer, val settings: RemoteSettings) : State()
2021-01-24 04:55:11 +00:00
class ServerManager(
2021-01-25 18:22:07 +00:00
private val settings: ClientSettings,
2021-01-24 04:55:11 +00:00
private val storage: ImageStorage
) {
// this must remain single-threaded because of how the state mechanism works
private val executor = Executors.newSingleThreadScheduledExecutor()
private val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
2021-01-26 16:15:50 +00:00
private val statistics = Statistics()
2021-03-02 18:22:24 +00:00
private val okhttp = ClientFilters.MicrometerMetrics.RequestCounter(registry)
.then(ClientFilters.MicrometerMetrics.RequestTimer(registry))
.then(
2021-03-02 18:22:24 +00:00
OkHttp(
bodyMode = BodyMode.Stream,
client = OkHttpClient.Builder()
2021-03-11 20:09:14 +00:00
.connectTimeout(Duration.ofSeconds(2))
.connectionPool(
ConnectionPool(
maxIdleConnections = 100,
keepAliveDuration = 1,
timeUnit = TimeUnit.MINUTES
)
)
.writeTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(10))
.protocols(listOf(Protocol.HTTP_1_1))
2021-01-28 14:17:24 +00:00
.build()
)
)
2021-01-24 04:55:11 +00:00
// state that must only be accessed from the thread on the executor
private var state: State
private var backendApi: BackendApi
// end protected state
init {
2021-01-25 18:22:07 +00:00
state = Uninitialized
2021-03-11 20:09:14 +00:00
backendApi = BackendApi(settings)
2021-01-24 04:55:11 +00:00
}
fun start() {
LOGGER.info { "Image server starting" }
DefaultMicrometerMetrics(registry, storage.cacheDirectory)
loginAndStartServer()
2021-01-26 16:15:50 +00:00
var lastBytesSent = statistics.bytesSent.get()
2021-01-24 04:55:11 +00:00
executor.scheduleAtFixedRate(
{
try {
2021-01-26 16:15:50 +00:00
lastBytesSent = statistics.bytesSent.get()
2021-01-24 04:55:11 +00:00
val state = this.state
if (state is GracefulStop && state.nextState != Shutdown) {
LOGGER.info { "Aborting graceful shutdown started due to hourly bandwidth limit" }
this.state = state.lastRunning
}
if (state is Uninitialized) {
LOGGER.info { "Restarting server stopped due to hourly bandwidth limit" }
loginAndStartServer()
}
} catch (e: Exception) {
LOGGER.warn(e) { "Hourly bandwidth check failed" }
}
},
1, 1, TimeUnit.HOURS
)
var lastRequests = 0L
executor.scheduleAtFixedRate(
{
try {
val state = this.state
if (state is GracefulStop) {
2021-01-25 18:22:07 +00:00
val timesToWait = settings.serverSettings.gracefulShutdownWaitSeconds / 5
2021-01-24 04:55:11 +00:00
val requestCounters = registry.find("http.server.request.latency").timers()
2021-01-28 17:03:41 +00:00
2021-01-24 04:55:11 +00:00
val curRequests = requestCounters.map { it.count() }.sum()
val noRequests = lastRequests >= curRequests
when {
state.counts == 0 -> {
LOGGER.info { "Starting graceful stop" }
logout()
lastRequests = curRequests
this.state = state.copy(counts = state.counts + 1)
}
state.counts == timesToWait || noRequests -> {
if (noRequests) {
LOGGER.info { "No requests received, stopping" }
} else {
LOGGER.info { "Max tries attempted (${state.counts} out of $timesToWait), shutting down" }
}
stopServer(state.nextState)
state.action()
}
else -> {
LOGGER.info {
"Waiting another 5 seconds for graceful stop (${state.counts} out of $timesToWait)"
}
lastRequests = curRequests
this.state = state.copy(counts = state.counts + 1)
}
}
}
} catch (e: Exception) {
LOGGER.error(e) { "Main loop failed" }
}
},
5, 5, TimeUnit.SECONDS
)
executor.scheduleWithFixedDelay(
{
try {
val state = this.state
if (state is Running) {
2021-01-26 16:15:50 +00:00
val currentBytesSent = statistics.bytesSent.get() - lastBytesSent
2021-01-25 18:22:07 +00:00
if (settings.serverSettings.maxMebibytesPerHour != 0L && settings.serverSettings.maxMebibytesPerHour * 1024 * 1024 /* MiB to bytes */ < currentBytesSent) {
2021-01-24 04:55:11 +00:00
LOGGER.info { "Stopping image server as hourly bandwidth limit reached" }
2021-02-11 15:11:03 +00:00
this.state = GracefulStop(lastRunning = state, nextState = Uninitialized)
2021-01-24 04:55:11 +00:00
} else {
pingControl()
}
}
} catch (e: Exception) {
LOGGER.warn(e) { "Bandwidth shutdown checker/ping failed" }
}
},
45, 45, TimeUnit.SECONDS
)
LOGGER.info { "Image server has started" }
}
private fun pingControl() {
// this is currentSettings, other is newSettings
// if tls is null that means same as previous ping
fun RemoteSettings.logicalEqual(other: RemoteSettings): Boolean {
val test = if (other.tls != null) {
other
} else {
other.copy(tls = this.tls)
}
return this == test
}
val state = this.state as Running
val newSettings = backendApi.pingControl(state.settings)
2021-01-28 13:50:13 +00:00
if (newSettings is RemoteSettings) {
2021-01-24 04:55:11 +00:00
LOGGER.info { "Server settings received: $newSettings" }
2021-01-24 18:05:05 +00:00
warnBasedOnSettings(newSettings)
2021-01-24 04:55:11 +00:00
if (!state.settings.logicalEqual(newSettings)) {
LOGGER.info { "Doing internal restart of HTTP server to refresh settings" }
2021-02-11 15:11:03 +00:00
this.state = GracefulStop(lastRunning = state, nextState = Uninitialized) {
2021-01-24 04:55:11 +00:00
loginAndStartServer()
}
}
} else {
2021-01-28 13:50:13 +00:00
LOGGER.info { "Ignoring failed server ping - $newSettings" }
2021-01-24 04:55:11 +00:00
}
}
private fun loginAndStartServer() {
2021-01-25 18:22:07 +00:00
this.state as Uninitialized
2021-01-24 04:55:11 +00:00
val remoteSettings = backendApi.loginToControl()
2021-01-28 13:50:13 +00:00
if (remoteSettings !is RemoteSettings) {
throw RuntimeException(remoteSettings.toString())
}
2021-01-24 04:55:11 +00:00
LOGGER.info { "Server settings received: $remoteSettings" }
2021-01-24 18:05:05 +00:00
warnBasedOnSettings(remoteSettings)
2021-01-24 04:55:11 +00:00
val server = getServer(
storage,
remoteSettings,
2021-01-25 18:22:07 +00:00
settings.serverSettings,
settings.metricsSettings,
2021-01-26 16:15:50 +00:00
statistics,
2021-01-28 14:17:24 +00:00
registry,
2021-03-02 18:22:24 +00:00
okhttp,
2021-01-24 04:55:11 +00:00
).start()
2021-01-25 18:22:07 +00:00
this.state = Running(server, remoteSettings)
2021-01-24 04:55:11 +00:00
LOGGER.info { "Internal HTTP server was successfully started" }
}
private fun logout() {
backendApi.logoutFromControl()
}
private fun stopServer(nextState: State) {
val state = this.state.let {
when (it) {
is Running ->
it
is GracefulStop ->
it.lastRunning
else ->
throw AssertionError()
}
}
LOGGER.info { "Image server stopping" }
state.server.stop()
LOGGER.info { "Image server has stopped" }
this.state = nextState
}
fun shutdown() {
LOGGER.info { "Image server shutting down" }
val latch = CountDownLatch(1)
executor.schedule(
{
val state = this.state
if (state is Running) {
this.state = GracefulStop(state, nextState = Shutdown) {
latch.countDown()
}
} else if (state is GracefulStop) {
this.state = state.copy(nextState = Shutdown) {
latch.countDown()
}
} else if (state is Uninitialized || state is Shutdown) {
this.state = Shutdown
latch.countDown()
}
},
0, TimeUnit.SECONDS
)
latch.await()
executor.shutdown()
LOGGER.info { "Image server has shut down" }
}
2021-01-24 18:05:05 +00:00
private fun warnBasedOnSettings(settings: RemoteSettings) {
2021-01-24 04:55:11 +00:00
if (settings.latestBuild > Constants.CLIENT_BUILD) {
LOGGER.warn {
"Outdated build detected! Latest: ${settings.latestBuild}, Current: ${Constants.CLIENT_BUILD}"
}
}
if (settings.paused) {
LOGGER.warn {
"Your client is paused by the backend and will not serve any images!"
}
}
if (settings.compromised) {
LOGGER.warn {
"Your client secret is compromised and it will not serve any images!"
}
}
}
companion object {
private val LOGGER = LoggerFactory.getLogger(ServerManager::class.java)
}
}