feat: implement clock expiry scanning and handling for game records (#53)
Build & Test (NowChessSystems) TeamCity build finished

Reviewed-on: #53
This commit was merged in pull request #53.
This commit is contained in:
2026-05-16 13:24:48 +02:00
parent 5d5fffa812
commit 8f9eb12f66
7 changed files with 251 additions and 142 deletions
+1
View File
@@ -60,6 +60,7 @@ dependencies {
implementation("io.quarkus:quarkus-opentelemetry")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("io.quarkus:quarkus-redis-client")
implementation("io.quarkus:quarkus-scheduler")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -34,6 +34,19 @@ class GameRecordRepository:
.asScala
.toList
def findExpiredLiveClockGames(nowMs: Long): List[GameRecord] =
em.createQuery(
"SELECT g FROM GameRecord g WHERE g.result IS NULL AND g.clockLastTickAt IS NOT NULL AND g.whiteRemainingMs IS NOT NULL",
classOf[GameRecord],
).getResultList
.asScala
.toList
.filter { g =>
val remaining =
if g.clockActiveColor == "white" then g.whiteRemainingMs.longValue else g.blackRemainingMs.longValue
g.clockLastTickAt.longValue + remaining < nowMs
}
def findByPlayerIdRunning(playerId: String, offset: Int, limit: Int): List[GameRecord] =
em.createQuery(
"SELECT g FROM GameRecord g WHERE g.whiteId = :id OR g.blackId = :id AND g.result = null ORDER BY g.updatedAt DESC",
@@ -0,0 +1,36 @@
package de.nowchess.store.service
import de.nowchess.store.config.RedisConfig
import de.nowchess.store.repository.GameRecordRepository
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.scheduler.Scheduled
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
@ApplicationScoped
class ClockExpiryScanner:
@Inject
// scalafix:off DisableSyntax.var
var repository: GameRecordRepository = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on
private val log = Logger.getLogger(classOf[ClockExpiryScanner])
private def clockExpireChannel: String = s"${redisConfig.prefix}:game:clock:expire"
@Scheduled(every = "30s")
def scan(): Unit =
try
val nowMs = System.currentTimeMillis()
val expired = repository.findExpiredLiveClockGames(nowMs)
if expired.nonEmpty then
log.infof("Found %d games with expired clocks", expired.size)
expired.foreach { record =>
log.infof("Publishing clock expiry for game %s", record.gameId)
redis.pubsub(classOf[String]).publish(clockExpireChannel, record.gameId)
}
catch case ex: Exception => log.warnf(ex, "Clock expiry scan failed")