optimize code
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
+29
-2
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -49,6 +49,6 @@ class FuzzyMovieSearchFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val THRESHOLD = 55
|
const val THRESHOLD = 70
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-45
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -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!")
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-16
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user