feat(redis): implement Redis integration for game state management and websocket communication

This commit is contained in:
2026-04-26 00:13:35 +02:00
parent ec09a1bdb9
commit 83f84371be
48 changed files with 1475 additions and 427 deletions
+92
View File
@@ -0,0 +1,92 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
@Suppress("UNCHECKED_CAST")
val scoverageExcluded = rootProject.extra["SCOVERAGE_EXCLUDED"] as List<String>
repositories {
mavenCentral()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
excludedFiles.set(scoverageExcluded)
}
tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
compileOnly("org.scala-lang:scala3-compiler_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation("org.scala-lang:scala3-library_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-websockets-next")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("org.redisson:redisson:${versions["REDISSON"]!!}")
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.quarkus:quarkus-junit5-mockito")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
tasks.test {
useJUnitPlatform {
includeEngines("scalatest", "junit-jupiter")
testLogging { events("passed", "skipped", "failed") }
}
finalizedBy(tasks.reportScoverage)
}
tasks.reportScoverage { dependsOn(tasks.test) }
tasks.jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
@@ -0,0 +1,28 @@
quarkus:
http:
port: 8084
application:
name: nowchess-ws
grpc:
server:
use-separate-server: false
nowchess:
redis:
host: localhost
port: 6379
prefix: nowchess
"%dev":
nowchess:
redis:
host: localhost
port: 6379
prefix: nowchess
"%deployed":
nowchess:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT:6379}
prefix: ${REDIS_PREFIX:nowchess}
@@ -0,0 +1,18 @@
package de.nowchess.ws.config
import jakarta.enterprise.context.ApplicationScoped
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@ApplicationScoped
class RedisConfig:
// scalafix:off DisableSyntax.var
@ConfigProperty(name = "nowchess.redis.host", defaultValue = "localhost")
var host: String = uninitialized
@ConfigProperty(name = "nowchess.redis.port", defaultValue = "6379")
var port: Int = uninitialized
@ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess")
var prefix: String = uninitialized
// scalafix:on DisableSyntax.var
@@ -0,0 +1,35 @@
package de.nowchess.ws.config
import jakarta.annotation.PreDestroy
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.inject.Produces
import jakarta.inject.Inject
import org.redisson.Redisson
import org.redisson.api.RedissonClient
import org.redisson.config.Config
import scala.compiletime.uninitialized
@ApplicationScoped
class RedissonProducer:
// scalafix:off DisableSyntax.var
@Inject
var redisConfig: RedisConfig = uninitialized
private var clientOpt: Option[RedissonClient] = None
// scalafix:on DisableSyntax.var
@Produces
@ApplicationScoped
def produceRedissonClient(): RedissonClient =
val config = new Config()
config.useSingleServer().setAddress(s"redis://${redisConfig.host}:${redisConfig.port}")
config.useSingleServer().setConnectionMinimumIdleSize(1)
config.useSingleServer().setConnectTimeout(500)
val client = Redisson.create(config)
clientOpt = Some(client)
client
@PreDestroy
def shutdown(): Unit =
clientOpt.foreach(_.shutdown())
@@ -0,0 +1,52 @@
package de.nowchess.ws.resource
import de.nowchess.ws.config.RedisConfig
import io.quarkus.websockets.next.*
import jakarta.inject.Inject
import org.redisson.api.listener.MessageListener
import org.redisson.api.RedissonClient
import scala.compiletime.uninitialized
import java.util.concurrent.ConcurrentHashMap
@WebSocket(path = "/api/board/game/{gameId}/ws")
class GameWebSocketResource:
// scalafix:off DisableSyntax.var
@Inject
var redisson: RedissonClient = uninitialized
@Inject
var redisConfig: RedisConfig = uninitialized
// scalafix:on DisableSyntax.var
private val listenerIds = new ConcurrentHashMap[String, (String, Int)]()
private def s2cTopic(gameId: String): String =
s"${redisConfig.prefix}:game:$gameId:s2c"
private def c2sTopic(gameId: String): String =
s"${redisConfig.prefix}:game:$gameId:c2s"
@OnOpen
def onOpen(connection: WebSocketConnection): Unit =
val gameId = connection.pathParam("gameId")
val topic = redisson.getTopic(s2cTopic(gameId))
val listenerId = topic.addListener(classOf[String], new MessageListener[String]:
def onMessage(channel: CharSequence, msg: String): Unit =
connection.sendText(msg).subscribe().`with`(_ => (), _ => ())
)
listenerIds.put(connection.id(), (gameId, listenerId))
val connectedMsg = s"""{"type":"CONNECTED","gameId":"$gameId"}"""
redisson.getTopic(c2sTopic(gameId)).publish(connectedMsg)
@OnTextMessage
def onTextMessage(connection: WebSocketConnection, message: String): Unit =
Option(listenerIds.get(connection.id())).foreach { case (gameId, _) =>
redisson.getTopic(c2sTopic(gameId)).publish(message)
}
@OnClose
def onClose(connection: WebSocketConnection): Unit =
Option(listenerIds.remove(connection.id())).foreach { case (gameId, listenerId) =>
redisson.getTopic(s2cTopic(gameId)).removeListener(listenerId)
}
@@ -0,0 +1,11 @@
quarkus:
http:
port: 8084
application:
name: nowchess-ws
nowchess:
redis:
host: localhost
port: 6379
prefix: test