optimize code

This commit is contained in:
2026-08-22 16:52:49 +03:00
parent 9da161c095
commit 7775579b70
8 changed files with 72 additions and 78 deletions
@@ -27,7 +27,7 @@ fun Application.module() {
runBlocking { koin.getOrNull<CacheStorage>()?.close() } runBlocking { koin.getOrNull<CacheStorage>()?.close() }
} }
val koin = object : KoinComponent {} val koin = object : KoinComponent {}
configureSerialization() configureSerialization(koin.get())
configureJwt(koin.get()) configureJwt(koin.get())
configureWebSocket(koin.get()) configureWebSocket(koin.get())
configureRouting(koin.get(), koin.get()) configureRouting(koin.get(), koin.get())
@@ -1,6 +1,9 @@
package ru.shadowsparky.vbox.backend.data package ru.shadowsparky.vbox.backend.data
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.util.cio.readChannel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
@@ -28,8 +31,18 @@ class BackendUpdateFetcher(
private val dispatcherProvider: DispatcherProvider, private val dispatcherProvider: DispatcherProvider,
private val userId: Long private val userId: Long
) : UpdateFetcher { ) : UpdateFetcher {
private val fetchMutex = Mutex()
private val updateFileMutex = Mutex()
override suspend fun fetch(versionCode: Long): UpdateInfo = withContext(dispatcherProvider.io) { override suspend fun fetch(versionCode: Long): UpdateInfo {
updateInfo?.let { return it }
fetchMutex.withLock {
updateInfo?.let { return it }
return fetchInternal().apply { updateInfo = this }
}
}
private suspend fun fetchInternal(): UpdateInfo = withContext(dispatcherProvider.io) {
val configPath = envFetcher.get("UPDATE_CONFIG") val configPath = envFetcher.get("UPDATE_CONFIG")
if (configPath.isBlank()) { if (configPath.isBlank()) {
UpdateInfo() UpdateInfo()
@@ -38,11 +51,25 @@ class BackendUpdateFetcher(
} }
} }
suspend fun updateFile(): File = withContext(dispatcherProvider.io) { suspend fun updateFile(): File {
updateFile?.let { return it }
updateFileMutex.withLock {
updateFile?.let { return it }
return updateFileInternal().apply { updateFile = this }
}
}
private suspend fun updateFileInternal(): File = withContext(dispatcherProvider.io) {
val updateFilePath = envFetcher.get("UPDATE_FILE") val updateFilePath = envFetcher.get("UPDATE_FILE")
if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found") if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found")
val file = File(updateFilePath) val file = File(updateFilePath)
file.readChannel()
if (!file.exists()) throw HttpException(HttpStatusCode.NotFound.value, "Update not exists") if (!file.exists()) throw HttpException(HttpStatusCode.NotFound.value, "Update not exists")
file file
} }
private companion object {
var updateInfo: UpdateInfo? = null
var updateFile: File? = null
}
} }
@@ -49,6 +49,6 @@ class FuzzyMovieSearchFilter {
} }
private companion object { private companion object {
const val THRESHOLD = 55 const val THRESHOLD = 70
} }
} }
@@ -3,7 +3,7 @@ package ru.shadowsparky.vbox.backend.data
import io.lettuce.core.ExperimentalLettuceCoroutinesApi import io.lettuce.core.ExperimentalLettuceCoroutinesApi
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.SetArgs import io.lettuce.core.SetArgs
import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.StatefulConnection
import io.lettuce.core.api.coroutines import io.lettuce.core.api.coroutines
import io.lettuce.core.api.coroutines.RedisCoroutinesCommands import io.lettuce.core.api.coroutines.RedisCoroutinesCommands
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -18,6 +18,7 @@ import ru.shadowsparky.domain.DispatcherProvider
import ru.shadowsparky.vbox.backend.domain.CacheStorage import ru.shadowsparky.vbox.backend.domain.CacheStorage
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.toJavaDuration
@OptIn(ExperimentalLettuceCoroutinesApi::class) @OptIn(ExperimentalLettuceCoroutinesApi::class)
@Single @Single
@@ -27,25 +28,23 @@ class RedisCacheStorage(
private val json: Json private val json: Json
) : CacheStorage { ) : CacheStorage {
private val logger = LoggerFactory.getLogger("RedisCacheStorage") private val logger = LoggerFactory.getLogger("RedisCacheStorage")
private var session: Session? = null
private var client: RedisClient? = null
private var connection: StatefulRedisConnection<String, String>? = null
private var commands: RedisCoroutinesCommands<String, String>? = null
private val connectionMutex = Mutex() private val connectionMutex = Mutex()
private val locks = ConcurrentHashMap<String, Mutex>() private val locks = ConcurrentHashMap<String, Mutex>()
override suspend fun <T> getOrCreate( override suspend fun <T> getOrCreate(
key: String, key: String,
serializer: KSerializer<T>, serializer: KSerializer<T>,
ttl: Duration?, ttl: Duration,
createNew: suspend () -> T, factory: suspend () -> T,
): T { ): T {
read(key, serializer)?.let { return it } read(key, serializer)?.let { return it }
val mutex = locks.computeIfAbsent(key) { Mutex() } val mutex = locks.computeIfAbsent(key) { Mutex() }
return mutex.withLock { return mutex.withLock {
try { try {
read(key, serializer)?.let { return@withLock it } read(key, serializer)?.let { return@withLock it }
createNew().also { factory().also {
write(key, it, serializer, ttl) write(key, it, serializer, ttl)
} }
} finally { } finally {
@@ -58,16 +57,11 @@ class RedisCacheStorage(
key: String, key: String,
value: T, value: T,
serializer: KSerializer<T>, serializer: KSerializer<T>,
duration: Duration? = null duration: Duration
) { ) {
val ttlInSeconds = duration?.inWholeSeconds
val rawValue = json.encodeToString(serializer, value) val rawValue = json.encodeToString(serializer, value)
if (ttlInSeconds != null && ttlInSeconds > 0) { val args = SetArgs().ex(duration.toJavaDuration())
val args = SetArgs().ex(ttlInSeconds) getCommands().set(key, rawValue, args)
getCommands().set(key, rawValue, args)
} else {
getCommands().set(key, rawValue)
}
} }
private suspend fun <T> read(key: String, serializer: KSerializer<T>): T? { private suspend fun <T> read(key: String, serializer: KSerializer<T>): T? {
@@ -76,50 +70,40 @@ class RedisCacheStorage(
} }
override suspend fun delete(key: String) { override suspend fun delete(key: String) {
(getCommands().del(key) ?: 0L) > 0L getCommands().del(key)
} }
override suspend fun close() { override suspend fun close() {
connectionMutex.withLock { connectionMutex.withLock {
if (connection != null || client != null) { session?.let {
logger.info("closing redis...") logger.info("closing redis...")
try { runCatching {
connection?.close() it.connection.close()
client?.shutdown() it.client.shutdown()
} catch (e: Exception) {
logger.error("unable to close redis: ${e.message}", e)
} finally {
connection = null
client = null
commands = null
} }
session = null
} }
} }
} }
private suspend fun getCommands(): RedisCoroutinesCommands<String, String> { private suspend fun getCommands(): RedisCoroutinesCommands<String, String> {
commands?.let { return it } session?.commands?.let { return it }
return connectionMutex.withLock { return connectionMutex.withLock {
commands?.let { return@withLock it } session?.commands?.let { return@withLock it }
withContext(dispatcherProvider.io) { withContext(dispatcherProvider.io) {
try { val redisUri = envFetcher.get("REDIS_URI", "redis://redis:6379")
val redisUri = envFetcher.get("REDIS_URI", "redis://redis:6379") val redisClient = RedisClient.create(redisUri)
logger.info("Динамическое подключение к Redis: $redisUri") val conn = redisClient.connect()
val redisClient = RedisClient.create(redisUri) conn.coroutines().apply {
val conn = redisClient.connect() session = Session(redisClient, conn, this)
val cmds = conn.coroutines()
client = redisClient
connection = conn
commands = cmds
logger.info("Успешно подключено к Redis")
cmds
} catch (e: Exception) {
logger.error("Ошибка динамического подключения к Redis: ${e.message}", e)
throw e
} }
} }
} }
} }
private class Session(
val client: RedisClient,
val connection: StatefulConnection<String, String>,
val commands: RedisCoroutinesCommands<String, String>
)
} }
@@ -8,8 +8,8 @@ interface CacheStorage {
suspend fun <T> getOrCreate( suspend fun <T> getOrCreate(
key: String, key: String,
serializer: KSerializer<T>, serializer: KSerializer<T>,
ttl: Duration? = null, ttl: Duration,
createNew: suspend () -> T, factory: suspend () -> T
): T ): T
suspend fun delete(key: String) suspend fun delete(key: String)
suspend fun close() suspend fun close()
@@ -18,12 +18,12 @@ interface CacheStorage {
suspend inline fun <reified T> CacheStorage.getOrCreate( suspend inline fun <reified T> CacheStorage.getOrCreate(
key: String, key: String,
ttl: Duration, ttl: Duration,
noinline createNew: suspend () -> T, noinline factory: suspend () -> T,
): T { ): T {
return getOrCreate( return getOrCreate(
key = key, key = key,
serializer = serializer<T>(), serializer = serializer<T>(),
ttl = ttl, ttl = ttl,
createNew = createNew factory = factory
) )
} }
@@ -18,18 +18,16 @@ import ru.shadowsparky.vbox.shared.domain.model.ServerExceptionInfo
const val AUTH_JWT_NAME = "auth-jwt" const val AUTH_JWT_NAME = "auth-jwt"
fun Application.configureJwt(authEntryPoint: AuthEntryPoint) { fun Application.configureJwt(authEntryPoint: AuthEntryPoint) = with(authEntryPoint) {
install(Authentication) { install(Authentication) {
jwt(AUTH_JWT_NAME) { jwt(AUTH_JWT_NAME) {
val jwtInfo = authEntryPoint.jwtInfo
realm = jwtInfo.realm realm = jwtInfo.realm
verifier(authEntryPoint.tokenVerifier.verifier) verifier(tokenVerifier.verifier)
validate { credential -> validate { credential ->
val tokenRepo = authEntryPoint.loginVerifier
val login = credential.payload.getClaim(LOGIN_NAME).asString() val login = credential.payload.getClaim(LOGIN_NAME).asString()
try { try {
if (login != null) { if (login != null) {
tokenRepo.verify(login) loginVerifier.verify(login)
if (credential.payload.expiresAt == null) { if (credential.payload.expiresAt == null) {
throw VerifyTokenException("Static tokens not supported!") throw VerifyTokenException("Static tokens not supported!")
} }
@@ -4,9 +4,8 @@ import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import kotlinx.serialization.json.Json
fun Application.configureSerialization() { fun Application.configureSerialization(json: Json) {
install(ContentNegotiation) { install(ContentNegotiation) { json(json) }
json()
}
} }
@@ -13,24 +13,19 @@ import io.ktor.server.websocket.webSocket
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.shadowsparky.backend.data.TokenVerifier import ru.shadowsparky.backend.data.TokenVerifier
import ru.shadowsparky.http.domain.BadRequestException
import ru.shadowsparky.http.domain.HttpException import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.vbox.backend.data.SessionRegistry import ru.shadowsparky.vbox.backend.data.SessionRegistry
import ru.shadowsparky.vbox.backend.data.eventLogger import ru.shadowsparky.vbox.backend.data.eventLogger
import ru.shadowsparky.vbox.backend.di.WebSocketEntryPoint import ru.shadowsparky.vbox.backend.di.WebSocketEntryPoint
import ru.shadowsparky.vbox.shared.domain.AuthRequest import ru.shadowsparky.vbox.shared.domain.AuthRequest
import ru.shadowsparky.vbox.shared.domain.AuthResponse import ru.shadowsparky.vbox.shared.domain.AuthResponse
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRepository
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
import ru.shadowsparky.vbox.shared.domain.SearchRepository
import ru.shadowsparky.vbox.shared.domain.USER_ID_ARG import ru.shadowsparky.vbox.shared.domain.USER_ID_ARG
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with(socketEntryPoint) { fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with(socketEntryPoint) {
@@ -41,15 +36,6 @@ fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with
masking = false masking = false
} }
routing { routing {
listOf(
"${SearchRepository.PREFIX}/onChange",
"${RecentlyWatchedRepository.PREFIX}/onChange",
"${SavedMovieRepository.PREFIX}/onChange",
"${UserTagRepository.PREFIX}/onChange",
"${MovieTagRepository.PREFIX}/onChange"
).forEach {
webSocket(it) { awaitCancellation() }
}
webSocket(RemoteEventHandler.ON_EVENT) { webSocket(RemoteEventHandler.ON_EVENT) {
val userId = incoming.authFlow(json, tokenVerifier, outgoing) val userId = incoming.authFlow(json, tokenVerifier, outgoing)
val session = SessionRegistry.Writer { text -> outgoing.trySend(Frame.Text(text)) } val session = SessionRegistry.Writer { text -> outgoing.trySend(Frame.Text(text)) }
@@ -75,7 +61,7 @@ private suspend fun ReceiveChannel<Frame>.authFlow(
tokenVerifier: TokenVerifier, tokenVerifier: TokenVerifier,
sendChannel: SendChannel<Frame> sendChannel: SendChannel<Frame>
): Long { ): Long {
val rawRequest = receiveTextOrNull() ?: throw HttpException(HttpStatusCode.BadRequest) val rawRequest = receiveTextOrNull() ?: throw BadRequestException("Authentication request required")
val request = runCatching { json.decodeFromString<AuthRequest>(rawRequest) }.getOrNull() val request = runCatching { json.decodeFromString<AuthRequest>(rawRequest) }.getOrNull()
val decodedJwt = if (request == null) { val decodedJwt = if (request == null) {
tokenVerifier.verify(rawRequest) tokenVerifier.verify(rawRequest)