From 5b20cb316b88d74bbbc8f0d86d905c29f96b48af Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:58:02 -0500 Subject: [PATCH] [O] Pre-compute ranking cache --- src/main/java/ext/Ext.kt | 3 + .../aqua/net/games/GameApiController.kt | 77 +++++++++++-------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index ab767bc0..754cdac4 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -22,6 +22,7 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import java.util.concurrent.locks.Lock import kotlin.reflect.KCallable import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 @@ -189,6 +190,8 @@ fun Str.md5() = MD5.digest(toByteArray(Charsets.UTF_8)).toHexString() // Coroutine suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } +fun thread(block: () -> T) = Thread { block() }.apply { start() } +fun Lock.maybeLock(block: () -> T) = if (tryLock()) try { block() } finally { unlock() } else null // Paths fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts) diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt index 159bb87c..9680f59b 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -4,7 +4,11 @@ import ext.* import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.sega.general.model.Card +import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock import kotlin.jvm.optionals.getOrNull import kotlin.reflect.KClass @@ -32,18 +36,41 @@ abstract class GameApiController(val name: String, userDataClass: } // Pair>> - private var rankingCache: Pair>> = 0L to emptyList() - private val rankingCacheDuration = 240_000 + private var rankingCache: List> = emptyList() + private var rankingCacheLock = ReentrantLock() @API("ranking") fun ranking(@RP token: String?): List { val time = millis() - val tableName = when (name) { "mai2" -> "maimai2"; "chu3" -> "chusan"; else -> name } - // Check if ranking cache needs to be updated + // Check cache validity + if (rankingCache.isEmpty()) (500 - "Ranking is computing... please wait") + + val reqUser = token?.let { us.jwt.auth(it) }?.let { u -> + // Optimization: If the user is not banned, we don't need to process user information + if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null + else u + } + + // Read from cache if we just computed it less than duration ago + // Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card // TODO: pagination - if (time - rankingCache.first > rankingCacheDuration) { - rankingCache = time to us.em.createNativeQuery( - """ + return rankingCache.filter { !it.l || it.r.username == reqUser?.username } + .mapIndexed { i, it -> it.r.apply { rank = i + 1 } } + .also { logger.info("Ranking returned in ${millis() - time}ms") } + } + + @PostConstruct + fun rakingCacheInit() = thread { rankingCacheRun() } + + // Every 20 minutes + @Scheduled(fixedRate = 20, timeUnit = TimeUnit.MINUTES) + fun rankingCacheRun() = rankingCacheLock.maybeLock { rankingCacheCompute() } + + fun rankingCacheCompute() { + val time = millis() + val tableName = when (name) { "mai2" -> "maimai2"; "chu3" -> "chusan"; else -> name } + rankingCache = us.em.createNativeQuery( + """ SELECT c.id, u.user_name, @@ -61,31 +88,19 @@ abstract class GameApiController(val name: String, userDataClass: GROUP BY p.user_id, u.player_rating ORDER BY u.player_rating DESC; """ - ).exec.mapIndexed { i, it -> - it[7].truthy to GenericRankingPlayer( - rank = i + 1, - name = it[1].toString(), - rating = it[2]!!.int, - lastSeen = it[3].toString(), - accuracy = it[4]!!.double, - fullCombo = it[5]!!.int, - allPerfect = it[6]!!.int, - username = it[8]?.toString() ?: "user${it[0]}" - ) - } + ).exec.mapIndexed { i, it -> + it[7].truthy to GenericRankingPlayer( + rank = i + 1, + name = it[1].toString(), + rating = it[2]!!.int, + lastSeen = it[3].toString(), + accuracy = it[4]!!.double, + fullCombo = it[5]!!.int, + allPerfect = it[6]!!.int, + username = it[8]?.toString() ?: "user${it[0]}" + ) } - - val reqUser = token?.let { us.jwt.auth(it) }?.let { u -> - // Optimization: If the user is not banned, we don't need to process user information - if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null - else u - } - - // Read from cache if we just computed it less than duration ago - // Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card - return rankingCache.r.filter { !it.l || it.r.username == reqUser?.username } - .mapIndexed { i, it -> it.r.apply { rank = i + 1 } } - .also { logger.info("Ranking computed in ${millis() - time}ms") } + logger.info("Ranking computed in ${millis() - time}ms") } @API("playlog")