Compare commits

...

2 Commits

Author SHA1 Message Date
Janis 65bc6a7599 feat(reflection): add native reflection configuration for tournament classes
Build & Test (NowChessSystems) TeamCity build started
fix(ws): improve WebSocket connection cleanup on close
chore(stream): simplify group name generation in GameCreationStreamClient
2026-06-10 09:00:24 +02:00
Janis a50884a11b fix(tournament): replace scala.util.Random singleton with UUID for native image
Build & Test (NowChessSystems) TeamCity build failed
GraalVM native image fails when scala.util.Random companion object (a static
singleton with cached seed) is reachable from the image heap. UUID.randomUUID()
is always runtime-initialized and safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:48:55 +02:00
5 changed files with 69 additions and 3 deletions
@@ -36,7 +36,7 @@ class GameCreationStreamClient:
private val log = Logger.getLogger(classOf[GameCreationStreamClient])
private val instanceId = UUID.randomUUID().toString
private val groupName = s"account-game-creation-$instanceId"
private val groupName = "account-game-creation"
private val consumerId = instanceId
private val maxStreamLen = 1000L
private val timeout = Duration.ofSeconds(10)
@@ -0,0 +1,27 @@
{
"reflection": [
{ "type": "scala.Tuple1[]" },
{ "type": "scala.Tuple2[]" },
{ "type": "scala.Tuple3[]" },
{ "type": "scala.Tuple4[]" },
{ "type": "scala.Tuple5[]" },
{ "type": "scala.Tuple6[]" },
{ "type": "scala.Tuple7[]" },
{ "type": "scala.Tuple8[]" },
{ "type": "scala.Tuple9[]" },
{ "type": "scala.Tuple10[]" },
{ "type": "scala.Tuple11[]" },
{ "type": "scala.Tuple12[]" },
{ "type": "scala.Tuple13[]" },
{ "type": "scala.Tuple14[]" },
{ "type": "scala.Tuple15[]" },
{ "type": "scala.Tuple16[]" },
{ "type": "scala.Tuple17[]" },
{ "type": "scala.Tuple18[]" },
{ "type": "scala.Tuple19[]" },
{ "type": "scala.Tuple20[]" },
{ "type": "scala.Tuple21[]" },
{ "type": "scala.Tuple22[]" },
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
]
}
@@ -0,0 +1,34 @@
package de.nowchess.tournament.config
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
import de.nowchess.tournament.dto.*
import de.nowchess.tournament.error.TournamentError
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[Tournament],
classOf[TournamentPairing],
classOf[TournamentParticipant],
classOf[TournamentError],
classOf[BotRef],
classOf[Clock],
classOf[Variant],
classOf[CreateTournamentForm],
classOf[ResultDto],
classOf[Standing],
classOf[TournamentDto],
classOf[TournamentListDto],
classOf[PairingDto],
classOf[GameExportDto],
classOf[RoundPairingsDto],
classOf[ErrorDto],
classOf[OkDto],
classOf[CorePlayerInfo],
classOf[CoreTimeControl],
classOf[CoreCreateGameRequest],
classOf[CoreGameResponse],
),
)
class NativeReflectionConfig
@@ -49,7 +49,7 @@ class TournamentService:
@Transactional
def create(createdBy: String, form: CreateTournamentForm): Tournament =
val t = new Tournament()
t.id = scala.util.Random.alphanumeric.take(6).mkString
t.id = java.util.UUID.randomUUID().toString.replace("-", "").take(8)
t.fullName = form.name
t.nbRounds = form.nbRounds
t.clockLimit = form.clockLimit
@@ -63,7 +63,12 @@ class UserWebSocketResource:
@OnClose
def onClose(connection: WebSocketConnection): Unit =
log.infof("User WebSocket closed — connectionId=%s", connection.id())
connections.remove(connection.id())
val userIdOpt = Option(connections.remove(connection.id())).map(_._1)
userIdOpt.foreach { userId =>
Try(redis.stream(classOf[String]).xgroupDestroy(userStreamKey(userId), connection.id())) match
case Failure(ex) => log.warnf(ex, "Failed to destroy consumer group for connectionId=%s", connection.id())
case Success(_) => ()
}
private def createGroupIfAbsent(userId: String, groupName: String): Unit =
Try(