feat(coordinator): add configurable coordinator settings and enhance WebSocket connection handling
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-26 22:33:16 +02:00
parent 64d5afa4d1
commit 6b59e68e04
27 changed files with 873 additions and 617 deletions
+3
View File
@@ -48,9 +48,12 @@ dependencies {
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-websockets-next")
implementation("io.quarkus:quarkus-jackson")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("org.redisson:redisson:${versions["REDISSON"]!!}")
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
@@ -3,6 +3,9 @@ quarkus:
port: 8084
application:
name: nowchess-ws
swagger-ui:
always-include: true
path: /swagger-ui
grpc:
server:
use-separate-server: false
@@ -19,6 +22,12 @@ nowchess:
host: localhost
port: 6379
prefix: nowchess
mp:
jwt:
verify:
publickey:
location: keys/public.pem
issuer: nowchess
"%deployed":
nowchess:
@@ -26,3 +35,9 @@ nowchess:
host: ${REDIS_HOST}
port: ${REDIS_PORT:6379}
prefix: ${REDIS_PREFIX:nowchess}
mp:
jwt:
verify:
publickey:
location: ${JWT_PUBLIC_KEY_PATH:keys/public.pem}
issuer: nowchess
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsnsCAl0vQx7Vu9CLDZ
g0SG05NgUzu9T+3DTEaHGq60T2uriO8BenwyvsF3BnDqTbKf4voohZ1DNfzdbT1J
Fj8B62FrDmxcO+sp1/b5HUCJP6y2uSRCmzOHe5k7Pk1IEi72FgBpKXSRkFibRlVf
634g7mgsPZAQ9PJEsv4Qvm05T9L6+Gmq6N3bMVLKRXs4RhDhaFbYH9GtUg1eI0yH
YjGyRfqzW/nqVMstOLHt8CuPouq4p7eMzeDH3YHkxPm4GG5foCXMOd2DZrW0SCcr
7dhFeNVWzQ2m53eOhBzNQX+v3pgjVStsePhBRt2LyGfwkNzmqDgqWsMzSHRMY+cn
WQIDAQAB
-----END PUBLIC KEY-----
@@ -0,0 +1,18 @@
package de.nowchess.ws.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(new DefaultScalaModule() {
override def version(): Version =
// scalafix:off DisableSyntax.null
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
// scalafix:on DisableSyntax.null
})
@@ -0,0 +1,12 @@
package de.nowchess.ws.config
import de.nowchess.ws.resource.ConnectionMeta
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[ConnectionMeta],
),
)
class NativeReflectionConfig
@@ -0,0 +1,7 @@
package de.nowchess.ws.resource
final case class ConnectionMeta(
gameId: String,
listenerId: Int,
playerId: Option[String],
)
@@ -2,10 +2,13 @@ package de.nowchess.ws.resource
import de.nowchess.ws.config.RedisConfig
import io.quarkus.websockets.next.*
import io.smallrye.jwt.auth.principal.JWTParser
import jakarta.inject.Inject
import org.redisson.api.listener.MessageListener
import org.redisson.api.RedissonClient
import scala.compiletime.uninitialized
import scala.util.Try
import java.util.concurrent.ConcurrentHashMap
@WebSocket(path = "/api/board/game/{gameId}/ws")
@@ -17,9 +20,12 @@ class GameWebSocketResource:
@Inject
var redisConfig: RedisConfig = uninitialized
@Inject
var jwtParser: JWTParser = uninitialized
// scalafix:on DisableSyntax.var
private val listenerIds = new ConcurrentHashMap[String, (String, Int)]()
private val connections = new ConcurrentHashMap[String, ConnectionMeta]()
private def s2cTopic(gameId: String): String =
s"${redisConfig.prefix}:game:$gameId:s2c"
@@ -28,27 +34,41 @@ class GameWebSocketResource:
s"${redisConfig.prefix}:game:$gameId:c2s"
@OnOpen
def onOpen(connection: WebSocketConnection): Unit =
val gameId = connection.pathParam("gameId")
val topic = redisson.getTopic(s2cTopic(gameId))
def onOpen(connection: WebSocketConnection, handshake: HandshakeRequest): Unit =
val gameId = connection.pathParam("gameId")
val playerId = Option(handshake.header("Authorization"))
.filter(_.nonEmpty)
.flatMap(token => Try(jwtParser.parse(token)).toOption)
.map(_.getSubject)
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"}"""
connections.put(connection.id(), ConnectionMeta(gameId, listenerId, playerId))
val connectedMsg = playerId match
case Some(pid) => s"""{"type":"CONNECTED","gameId":"$gameId","playerId":"$pid"}"""
case None => 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)
Option(connections.get(connection.id())).foreach { meta =>
val enriched = meta.playerId match
case Some(pid) => injectPlayerId(message, pid)
case None => message
redisson.getTopic(c2sTopic(meta.gameId)).publish(enriched)
}
@OnClose
def onClose(connection: WebSocketConnection): Unit =
Option(listenerIds.remove(connection.id())).foreach { case (gameId, listenerId) =>
redisson.getTopic(s2cTopic(gameId)).removeListener(listenerId)
Option(connections.remove(connection.id())).foreach { meta =>
redisson.getTopic(s2cTopic(meta.gameId)).removeListener(meta.listenerId)
}
private def injectPlayerId(msg: String, pid: String): String =
val trimmed = msg.trim
if trimmed.endsWith("}") then trimmed.dropRight(1) + s""","playerId":"$pid"}"""
else msg