Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60f4e87579 | |||
| 145f467648 | |||
| db9d153391 | |||
| 55f102cbaa | |||
| d66b6fa471 | |||
| 676e4110c0 | |||
| 0ad2e10999 |
@@ -56,6 +56,7 @@ jobs:
|
||||
- official-bots
|
||||
- rule
|
||||
- store
|
||||
- tournament
|
||||
- ws
|
||||
arch:
|
||||
- name: default
|
||||
|
||||
@@ -54,3 +54,7 @@ modules/account/src/main/resources/keys/dev-private.pem
|
||||
modules/account/src/main/resources/keys/dev-public.pem
|
||||
modules/core/src/main/resources/keys/dev-public.pem
|
||||
*.hprof
|
||||
|
||||
### Embedded repos (not submodules) ###
|
||||
GitOps/
|
||||
frontend/
|
||||
|
||||
@@ -490,3 +490,42 @@
|
||||
|
||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
|
||||
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
|
||||
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
|
||||
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
|
||||
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
|
||||
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package de.nowchess.account.client
|
||||
|
||||
import de.nowchess.security.{InternalClientHeadersFactory, InternalSecretClientFilter}
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.MediaType
|
||||
import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, RegisterProvider}
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||
|
||||
case class CorePlayerInfo(id: String, displayName: String)
|
||||
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
|
||||
case class CoreCreateGameRequest(
|
||||
white: Option[CorePlayerInfo],
|
||||
black: Option[CorePlayerInfo],
|
||||
timeControl: Option[CoreTimeControl],
|
||||
mode: Option[String],
|
||||
)
|
||||
case class CoreGameResponse(gameId: String)
|
||||
|
||||
@Path("/api/board/game")
|
||||
@RegisterRestClient(configKey = "core-service")
|
||||
@RegisterProvider(classOf[InternalSecretClientFilter])
|
||||
@RegisterClientHeaders(classOf[InternalClientHeadersFactory])
|
||||
trait CoreGameClient:
|
||||
|
||||
@POST
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def createGame(req: CoreCreateGameRequest): CoreGameResponse
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.nowchess.account.client
|
||||
|
||||
case class CorePlayerInfo(id: String, displayName: String)
|
||||
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
|
||||
case class CoreCreateGameRequest(
|
||||
white: Option[CorePlayerInfo],
|
||||
black: Option[CorePlayerInfo],
|
||||
timeControl: Option[CoreTimeControl],
|
||||
mode: Option[String],
|
||||
)
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.account.config
|
||||
|
||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||
import de.nowchess.account.client.{CoreCreateGameRequest, CorePlayerInfo, CoreTimeControl}
|
||||
import de.nowchess.account.domain.{
|
||||
BotAccount,
|
||||
Challenge,
|
||||
@@ -53,7 +53,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[CorePlayerInfo],
|
||||
classOf[CoreTimeControl],
|
||||
classOf[CoreCreateGameRequest],
|
||||
classOf[CoreGameResponse],
|
||||
classOf[OfficialChallengeResponse],
|
||||
classOf[GameCreationRequestDto],
|
||||
classOf[GameCreationResponseDto],
|
||||
|
||||
@@ -36,26 +36,32 @@ class EventPublisher:
|
||||
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
|
||||
Map("data" -> json).asJava,
|
||||
)
|
||||
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", json)
|
||||
()
|
||||
|
||||
def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit =
|
||||
val payload = objectMapper.createObjectNode()
|
||||
payload.put("challengeId", challengeId)
|
||||
payload.put("challengerName", challengerName)
|
||||
publish(s"${redisConfig.prefix}:user:$destUserId:events", EventType.ChallengeCreated, payload)
|
||||
publishToUserStream(destUserId, EventType.ChallengeCreated, payload)
|
||||
|
||||
def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit =
|
||||
val payload = objectMapper.createObjectNode()
|
||||
payload.put("challengeId", challengeId)
|
||||
payload.put("gameId", gameId)
|
||||
publish(s"${redisConfig.prefix}:user:$challengerId:events", EventType.ChallengeAccepted, payload)
|
||||
publishToUserStream(challengerId, EventType.ChallengeAccepted, payload)
|
||||
|
||||
private def publish(
|
||||
channel: String,
|
||||
private def publishToUserStream(
|
||||
userId: String,
|
||||
eventType: EventType,
|
||||
payload: com.fasterxml.jackson.databind.node.ObjectNode,
|
||||
): Unit =
|
||||
val envelope = EventEnvelope.of(eventType, payload)
|
||||
redis.pubsub(classOf[String]).publish(channel, objectMapper.writeValueAsString(envelope))
|
||||
val json = objectMapper.writeValueAsString(envelope)
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xadd(
|
||||
s"${redisConfig.prefix}:user:$userId:events:stream",
|
||||
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
|
||||
Map("data" -> json).asJava,
|
||||
)
|
||||
()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.nowchess.account.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import de.nowchess.account.config.RedisConfig
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.stream.{StreamCommands, XAddArgs}
|
||||
import org.junit.jupiter.api.{BeforeEach, Test}
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import org.mockito.Mockito.*
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
class EventPublisherTest:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
private var redis: RedisDataSource = uninitialized
|
||||
private var streamCmds: StreamCommands[String, String, Nothing] = uninitialized
|
||||
private var redisConfig: RedisConfig = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
|
||||
|
||||
@BeforeEach
|
||||
def setup(): Unit =
|
||||
redis = mock(classOf[RedisDataSource])
|
||||
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
|
||||
redisConfig = mock(classOf[RedisConfig])
|
||||
when(redis.stream(classOf[String])).thenReturn(streamCmds)
|
||||
when(redisConfig.prefix).thenReturn("nowchess")
|
||||
|
||||
private def publisher: EventPublisher =
|
||||
val p = new EventPublisher
|
||||
p.redis = redis
|
||||
p.redisConfig = redisConfig
|
||||
p.objectMapper = objectMapper
|
||||
p
|
||||
|
||||
@Test
|
||||
def publishChallengeCreatedWritesToUserStream(): Unit =
|
||||
publisher.publishChallengeCreated("user1", "ch1", "Alice")
|
||||
verify(streamCmds).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:user:user1:events:stream"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
verify(redis, never()).pubsub(any(classOf[Class[?]]))
|
||||
|
||||
@Test
|
||||
def publishChallengeAcceptedWritesToUserStream(): Unit =
|
||||
publisher.publishChallengeAccepted("user2", "ch1", "game42")
|
||||
verify(streamCmds).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:user:user2:events:stream"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
verify(redis, never()).pubsub(any(classOf[Class[?]]))
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=22
|
||||
MINOR=23
|
||||
PATCH=0
|
||||
|
||||
@@ -184,3 +184,24 @@
|
||||
|
||||
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
|
||||
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
|
||||
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
|
||||
* **dto:** update GameWritebackEventDto for JSON deserialization and remove unused mixin ([576e3fe](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/576e3fea9bf1082549ea53efd3288474c42be93d))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
|
||||
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
|
||||
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
|
||||
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.nowchess.api.event
|
||||
|
||||
enum EventType:
|
||||
case GameStart, GameCreationRequest, GameCreationResponse, BotGameStart, ChallengeCreated, ChallengeAccepted
|
||||
case GameStart, GameCreationRequest, GameCreationResponse, BotGameStart, ChallengeCreated, ChallengeAccepted, GameOver
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.api.event
|
||||
|
||||
final case class GameOverPayload(
|
||||
gameId: String,
|
||||
result: String,
|
||||
terminationReason: String,
|
||||
whiteId: String,
|
||||
blackId: String,
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=16
|
||||
MINOR=17
|
||||
PATCH=0
|
||||
|
||||
@@ -129,3 +129,22 @@
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
@@ -73,6 +73,7 @@ dependencies {
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("io.quarkus:quarkus-junit")
|
||||
testImplementation("io.quarkus:quarkus-junit5-mockito")
|
||||
testImplementation("io.rest-assured:rest-assured")
|
||||
testImplementation("io.quarkus:quarkus-test-security")
|
||||
|
||||
|
||||
+61
-20
@@ -2,14 +2,18 @@ package de.nowchess.botplatform.registry
|
||||
|
||||
import de.nowchess.botplatform.config.RedisConfig
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.pubsub.PubSubCommands
|
||||
import io.quarkus.redis.datasource.stream.{XGroupCreateArgs, XReadGroupArgs}
|
||||
import io.smallrye.mutiny.subscription.MultiEmitter
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import org.eclipse.microprofile.context.ManagedExecutor
|
||||
import org.jboss.logging.Logger
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.Consumer
|
||||
|
||||
@ApplicationScoped
|
||||
class BotRegistry:
|
||||
@@ -17,31 +21,68 @@ class BotRegistry:
|
||||
private val log = Logger.getLogger(classOf[BotRegistry])
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var executor: ManagedExecutor = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val connections = ConcurrentHashMap[String, (MultiEmitter[? >: String], PubSubCommands.RedisSubscriber)]()
|
||||
private val groupName = "bot-platform-consumer"
|
||||
private val consumerId = UUID.randomUUID().toString
|
||||
|
||||
private val emitters = ConcurrentHashMap[String, MultiEmitter[? >: String]]()
|
||||
|
||||
def register(botId: String, emitter: MultiEmitter[? >: String]): Unit =
|
||||
val channel = s"${redisConfig.prefix}:bot:$botId:events"
|
||||
val handler: Consumer[String] = msg => emitter.emit(msg)
|
||||
val subscriber = redis.pubsub(classOf[String]).subscribe(channel, handler)
|
||||
connections.put(botId, (emitter, subscriber))
|
||||
log.infof("Bot %s registered", botId)
|
||||
createGroupIfAbsent(botId)
|
||||
emitters.put(botId, emitter)
|
||||
executor.submit(
|
||||
new Runnable:
|
||||
def run(): Unit = pollLoop(botId, emitter),
|
||||
)
|
||||
log.infof("Bot %s registered on stream consumer group", botId)
|
||||
()
|
||||
|
||||
def unregister(botId: String): Unit =
|
||||
Option(connections.remove(botId)).foreach { (_, subscriber) =>
|
||||
subscriber.unsubscribe(s"${redisConfig.prefix}:bot:$botId:events")
|
||||
}
|
||||
emitters.remove(botId)
|
||||
log.infof("Bot %s unregistered", botId)
|
||||
|
||||
def dispatch(botId: String, event: String): Unit =
|
||||
log.debugf("Dispatching event to bot %s", botId)
|
||||
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event)
|
||||
()
|
||||
|
||||
def registeredBots: List[String] =
|
||||
import scala.jdk.CollectionConverters.*
|
||||
connections.keys().asScala.toList
|
||||
emitters.keys().asScala.toList
|
||||
|
||||
private def streamKey(botId: String): String =
|
||||
s"${redisConfig.prefix}:bot:$botId:events:stream"
|
||||
|
||||
private def createGroupIfAbsent(botId: String): Unit =
|
||||
Try(
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xgroupCreate(streamKey(botId), groupName, "$", new XGroupCreateArgs().mkstream()),
|
||||
) match
|
||||
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
|
||||
case Failure(ex) => log.warnf(ex, "Failed to create consumer group for bot %s", botId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def pollLoop(botId: String, myEmitter: MultiEmitter[? >: String]): Unit =
|
||||
while emitters.get(botId) eq myEmitter do
|
||||
Try {
|
||||
val messages = redis
|
||||
.stream(classOf[String])
|
||||
.xreadgroup(
|
||||
groupName,
|
||||
consumerId,
|
||||
streamKey(botId),
|
||||
">",
|
||||
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
|
||||
)
|
||||
Option(messages).foreach(_.forEach { msg =>
|
||||
if emitters.get(botId) eq myEmitter then
|
||||
myEmitter.emit(msg.payload().get("data"))
|
||||
ack(botId, msg.id())
|
||||
})
|
||||
} match
|
||||
case Failure(ex) => log.warnf(ex, "Error in poll loop for bot %s", botId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def ack(botId: String, id: String): Unit =
|
||||
Try(redis.stream(classOf[String]).xack(streamKey(botId), groupName, id)) match
|
||||
case Failure(ex) => log.warnf(ex, "Failed to ack message %s for bot %s", id, botId)
|
||||
case Success(_) => ()
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package de.nowchess.botplatform.registry
|
||||
|
||||
import de.nowchess.botplatform.config.RedisConfig
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.stream.{StreamCommands, XGroupCreateArgs}
|
||||
import io.smallrye.mutiny.subscription.MultiEmitter
|
||||
import org.eclipse.microprofile.context.ManagedExecutor
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.function.Executable
|
||||
import org.junit.jupiter.api.{BeforeEach, Test}
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import org.mockito.Mockito.*
|
||||
|
||||
class BotRegistryTest:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
private var registry: BotRegistry = scala.compiletime.uninitialized
|
||||
private var redis: RedisDataSource = scala.compiletime.uninitialized
|
||||
private var streamCmds: StreamCommands[String, String, Nothing] =
|
||||
scala.compiletime.uninitialized
|
||||
private var redisConfig: RedisConfig = scala.compiletime.uninitialized
|
||||
private var executor: ManagedExecutor = scala.compiletime.uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
@BeforeEach
|
||||
def setup(): Unit =
|
||||
redis = mock(classOf[RedisDataSource])
|
||||
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
|
||||
redisConfig = mock(classOf[RedisConfig])
|
||||
executor = mock(classOf[ManagedExecutor])
|
||||
|
||||
when(redis.stream(classOf[String])).thenReturn(streamCmds)
|
||||
when(redisConfig.prefix).thenReturn("nowchess")
|
||||
|
||||
registry = new BotRegistry
|
||||
registry.redis = redis
|
||||
registry.redisConfig = redisConfig
|
||||
registry.executor = executor
|
||||
|
||||
@Test
|
||||
def registerStartsPollThread(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
registry.register("bot1", emitter)
|
||||
verify(executor).submit(any(classOf[Runnable]))
|
||||
|
||||
@Test
|
||||
def registerCreatesConsumerGroupWithMkstream(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
registry.register("bot1", emitter)
|
||||
verify(streamCmds)
|
||||
.xgroupCreate(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:bot:bot1:events:stream"),
|
||||
org.mockito.ArgumentMatchers.eq("bot-platform-consumer"),
|
||||
org.mockito.ArgumentMatchers.eq("$"),
|
||||
any(classOf[XGroupCreateArgs]),
|
||||
)
|
||||
|
||||
@Test
|
||||
def registerTracksBot(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
registry.register("bot42", emitter)
|
||||
assertTrue(registry.registeredBots.contains("bot42"))
|
||||
|
||||
@Test
|
||||
def unregisterRemovesBot(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
registry.register("botX", emitter)
|
||||
registry.unregister("botX")
|
||||
assertFalse(registry.registeredBots.contains("botX"))
|
||||
|
||||
@Test
|
||||
def busyGroupExceptionIsIgnoredOnRegister(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
when(streamCmds.xgroupCreate(any(), any(), any(), any()))
|
||||
.thenThrow(new RuntimeException("BUSYGROUP Consumer Group name already exists"))
|
||||
val exec: Executable = () => registry.register("botBusy", emitter)
|
||||
assertDoesNotThrow(exec)
|
||||
|
||||
@Test
|
||||
def registerDoesNotInteractWithPubSub(): Unit =
|
||||
val emitter = mock(classOf[MultiEmitter[String]])
|
||||
registry.register("botNoPubSub", emitter)
|
||||
verify(redis, never()).pubsub(any(classOf[Class[?]]))
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=11
|
||||
MINOR=12
|
||||
PATCH=0
|
||||
|
||||
@@ -1966,3 +1966,74 @@
|
||||
|
||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
|
||||
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
|
||||
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
|
||||
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
|
||||
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
|
||||
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
|
||||
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
|
||||
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
|
||||
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
|
||||
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
|
||||
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
|
||||
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
|
||||
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
|
||||
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
|
||||
* NCS-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
|
||||
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
|
||||
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
|
||||
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
|
||||
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
|
||||
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
|
||||
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
|
||||
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
|
||||
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
||||
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
|
||||
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
|
||||
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
|
||||
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
|
||||
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
|
||||
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
|
||||
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
|
||||
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
|
||||
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
|
||||
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
|
||||
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
|
||||
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
|
||||
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
|
||||
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
|
||||
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.nowchess.chess.config
|
||||
|
||||
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.dto.*
|
||||
import de.nowchess.api.event.{EventEnvelope, EventType}
|
||||
import de.nowchess.api.event.{EventEnvelope, EventType, GameOverPayload}
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameMode, GameResult}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.chess.registry.GameCacheDto
|
||||
@@ -18,6 +18,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[GameCreationResponseDto],
|
||||
classOf[EventEnvelope],
|
||||
classOf[EventType],
|
||||
classOf[GameOverPayload],
|
||||
classOf[ErrorEventDto],
|
||||
classOf[GameWritebackEventDto],
|
||||
classOf[GameFullDto],
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package de.nowchess.chess.redis
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import de.nowchess.api.dto.{GameStateDto, GameStateEventDto, GameWritebackEventDto}
|
||||
import de.nowchess.api.game.{CorrespondenceClockState, DrawReason, GameResult, LiveClockState, TimeControl, WinReason}
|
||||
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
||||
import de.nowchess.api.board.Color
|
||||
import de.nowchess.api.dto.{GameStateDto, GameStateEventDto, GameWritebackEventDto}
|
||||
import de.nowchess.api.event.{EventEnvelope, EventType, GameOverPayload}
|
||||
import de.nowchess.api.game.{CorrespondenceClockState, DrawReason, GameResult, LiveClockState, TimeControl, WinReason}
|
||||
import de.nowchess.chess.grpc.IoGrpcClientWrapper
|
||||
import de.nowchess.chess.observer.{GameEvent, Observer}
|
||||
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
||||
import de.nowchess.chess.resource.GameDtoMapper
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.stream.XAddArgs
|
||||
import org.jboss.logging.Logger
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
object GameRedisPublisher:
|
||||
private val log = Logger.getLogger(classOf[GameRedisPublisher])
|
||||
@@ -23,8 +26,11 @@ class GameRedisPublisher(
|
||||
writebackEmit: String => Unit,
|
||||
ioClient: IoGrpcClientWrapper,
|
||||
onGameOver: String => Unit,
|
||||
redisPrefix: String,
|
||||
) extends Observer:
|
||||
|
||||
private val maxStreamLen = 1000L
|
||||
|
||||
def emitInitialWriteback(): Unit =
|
||||
try
|
||||
registry.get(gameId).foreach { entry =>
|
||||
@@ -40,10 +46,39 @@ class GameRedisPublisher(
|
||||
val dto = GameDtoMapper.toGameStateDto(entry, ioClient)
|
||||
redis.pubsub(classOf[String]).publish(s2cTopicName, objectMapper.writeValueAsString(GameStateEventDto(dto)))
|
||||
writebackEmit(objectMapper.writeValueAsString(buildWriteback(entry, dto)))
|
||||
if entry.engine.context.result.isDefined then onGameOver(gameId)
|
||||
entry.engine.context.result.foreach { result =>
|
||||
publishGameOver(entry, result)
|
||||
onGameOver(gameId)
|
||||
}
|
||||
}
|
||||
catch case ex: Exception => GameRedisPublisher.log.warnf(ex, "Failed to publish game event for game %s", gameId)
|
||||
|
||||
private def publishGameOver(entry: GameEntry, result: GameResult): Unit =
|
||||
val resultStr = result match
|
||||
case GameResult.Win(Color.White, _) => "white"
|
||||
case GameResult.Win(Color.Black, _) => "black"
|
||||
case GameResult.Draw(_) => "draw"
|
||||
val terminationReason = result match
|
||||
case GameResult.Win(_, WinReason.Checkmate) => "checkmate"
|
||||
case GameResult.Win(_, WinReason.Resignation) => "resignation"
|
||||
case GameResult.Win(_, WinReason.TimeControl) => "timeout"
|
||||
case GameResult.Draw(DrawReason.Stalemate) => "stalemate"
|
||||
case GameResult.Draw(DrawReason.InsufficientMaterial) => "insufficient_material"
|
||||
case GameResult.Draw(DrawReason.FiftyMoveRule) => "fifty_move"
|
||||
case GameResult.Draw(DrawReason.ThreefoldRepetition) => "repetition"
|
||||
case GameResult.Draw(DrawReason.Agreement) => "agreement"
|
||||
val payload = objectMapper.valueToTree[JsonNode](
|
||||
GameOverPayload(gameId, resultStr, terminationReason, entry.white.id.value, entry.black.id.value),
|
||||
)
|
||||
val envelope = EventEnvelope.of(EventType.GameOver, payload)
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xadd(
|
||||
s"$redisPrefix:game-over",
|
||||
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
|
||||
Map("data" -> objectMapper.writeValueAsString(envelope)).asJava,
|
||||
)
|
||||
|
||||
private def buildWriteback(entry: GameEntry, dto: GameStateDto): GameWritebackEventDto =
|
||||
val clock = entry.engine.currentClockState
|
||||
GameWritebackEventDto(
|
||||
|
||||
@@ -93,6 +93,7 @@ class GameRedisSubscriberManager:
|
||||
writebackFn,
|
||||
ioClient,
|
||||
unsubscribeGame,
|
||||
redisConfig.prefix,
|
||||
)
|
||||
s2cObservers.put(gameId, obs)
|
||||
registry.get(gameId).foreach(_.engine.subscribe(obs))
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.nowchess.chess.redis
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import de.nowchess.api.board.Color
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.chess.client.CombinedExportResponse
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
import de.nowchess.chess.grpc.IoGrpcClientWrapper
|
||||
import de.nowchess.chess.observer.GameEvent
|
||||
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.pubsub.PubSubCommands
|
||||
import io.quarkus.redis.datasource.stream.{StreamCommands, XAddArgs}
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.{BeforeEach, Test}
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import org.mockito.Mockito.*
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
class GameRedisPublisherTest:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
private var redis: RedisDataSource = uninitialized
|
||||
private var streamCmds: StreamCommands[String, String, Nothing] = uninitialized
|
||||
private var pubsubCmds: PubSubCommands[String] = uninitialized
|
||||
private var registry: GameRegistry = uninitialized
|
||||
private var ioClient: IoGrpcClientWrapper = uninitialized
|
||||
private var onGameOverCalled: Boolean = false
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
|
||||
private val gameId = "game1"
|
||||
private val whitePlayer = PlayerInfo(PlayerId("white1"), "Alice")
|
||||
private val blackPlayer = PlayerInfo(PlayerId("black1"), "Bob")
|
||||
|
||||
@BeforeEach
|
||||
def setup(): Unit =
|
||||
redis = mock(classOf[RedisDataSource])
|
||||
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
|
||||
pubsubCmds = mock(classOf[PubSubCommands[String]])
|
||||
registry = mock(classOf[GameRegistry])
|
||||
ioClient = mock(classOf[IoGrpcClientWrapper])
|
||||
when(redis.stream(classOf[String])).thenReturn(streamCmds)
|
||||
when(redis.pubsub(classOf[String])).thenReturn(pubsubCmds)
|
||||
when(ioClient.exportCombined(any()))
|
||||
.thenReturn(CombinedExportResponse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", ""))
|
||||
onGameOverCalled = false
|
||||
|
||||
private def publisherWithResult(result: GameResult): GameRedisPublisher =
|
||||
val ctx = GameContext.initial.copy(result = Some(result))
|
||||
val engine = new GameEngine(initialContext = ctx, ruleSet = DefaultRules)
|
||||
val entry = GameEntry(gameId, engine, whitePlayer, blackPlayer)
|
||||
when(registry.get(gameId)).thenReturn(Some(entry))
|
||||
new GameRedisPublisher(
|
||||
gameId,
|
||||
registry,
|
||||
redis,
|
||||
objectMapper,
|
||||
s"nowchess:game:$gameId:s2c",
|
||||
_ => (),
|
||||
ioClient,
|
||||
_ => onGameOverCalled = true,
|
||||
"nowchess",
|
||||
)
|
||||
|
||||
@Test
|
||||
def publishesGameOverOnCheckmate(): Unit =
|
||||
val publisher = publisherWithResult(GameResult.Win(Color.White, WinReason.Checkmate))
|
||||
publisher.onGameEvent(mock(classOf[GameEvent]))
|
||||
verify(streamCmds).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
assertTrue(onGameOverCalled)
|
||||
|
||||
@Test
|
||||
def publishesGameOverOnResignation(): Unit =
|
||||
val publisher = publisherWithResult(GameResult.Win(Color.Black, WinReason.Resignation))
|
||||
publisher.onGameEvent(mock(classOf[GameEvent]))
|
||||
verify(streamCmds).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
|
||||
@Test
|
||||
def publishesGameOverOnDraw(): Unit =
|
||||
val publisher = publisherWithResult(GameResult.Draw(DrawReason.Agreement))
|
||||
publisher.onGameEvent(mock(classOf[GameEvent]))
|
||||
verify(streamCmds).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
|
||||
@Test
|
||||
def doesNotPublishGameOverWhenNoResult(): Unit =
|
||||
val ctx = GameContext.initial
|
||||
val engine = new GameEngine(initialContext = ctx, ruleSet = DefaultRules)
|
||||
val entry = GameEntry(gameId, engine, whitePlayer, blackPlayer)
|
||||
when(registry.get(gameId)).thenReturn(Some(entry))
|
||||
val publisher = new GameRedisPublisher(
|
||||
gameId,
|
||||
registry,
|
||||
redis,
|
||||
objectMapper,
|
||||
s"nowchess:game:$gameId:s2c",
|
||||
_ => (),
|
||||
ioClient,
|
||||
_ => onGameOverCalled = true,
|
||||
"nowchess",
|
||||
)
|
||||
publisher.onGameEvent(mock(classOf[GameEvent]))
|
||||
verify(streamCmds, never()).xadd(
|
||||
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
|
||||
any(classOf[XAddArgs]),
|
||||
any(),
|
||||
)
|
||||
assertFalse(onGameOverCalled)
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=49
|
||||
MINOR=50
|
||||
PATCH=0
|
||||
|
||||
@@ -228,3 +228,29 @@
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
+83
-6
@@ -24,8 +24,9 @@ import scala.jdk.CollectionConverters.*
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
import io.quarkus.redis.datasource.pubsub.PubSubCommands
|
||||
import java.util.concurrent.{ConcurrentHashMap, TimeUnit}
|
||||
import java.util.function.Consumer
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ApplicationScoped
|
||||
class OfficialBotService:
|
||||
@@ -48,14 +49,18 @@ class OfficialBotService:
|
||||
private val terminalStatuses =
|
||||
Set("checkmate", "resign", "timeout", "stalemate", "insufficientMaterial", "draw")
|
||||
|
||||
private val groupName = "official-bot"
|
||||
private val consumerId = UUID.randomUUID().toString
|
||||
private val maxRetries = 3
|
||||
private val maxStreamLen = 1000L
|
||||
private val groupName = "official-bot"
|
||||
private val gameOverGroup = "official-bots-game-over"
|
||||
private val consumerId = UUID.randomUUID().toString
|
||||
private val maxRetries = 3
|
||||
private val maxStreamLen = 1000L
|
||||
|
||||
private def eventStream(botName: String): String = s"${redisConfig.prefix}:bot:$botName:events:stream"
|
||||
private def gameOverStream: String = s"${redisConfig.prefix}:game-over"
|
||||
private def dlqStream: String = s"${redisConfig.prefix}:dlq"
|
||||
|
||||
private val gameWatches = new ConcurrentHashMap[String, (String, PubSubCommands.RedisSubscriber)]()
|
||||
|
||||
@PostConstruct
|
||||
def initializeMetrics(): Unit =
|
||||
BotController.listBots.foreach { bot =>
|
||||
@@ -68,6 +73,7 @@ class OfficialBotService:
|
||||
try accountServiceClient.syncBots(SyncOfficialBotsRequest(bots))
|
||||
catch case ex: Exception => log.errorf(ex, "Failed to auto-register official bots with account service")
|
||||
bots.foreach(subscribeToEventChannel)
|
||||
subscribeToGameOverStream()
|
||||
|
||||
private def subscribeToEventChannel(botName: String): Unit =
|
||||
createGroupIfAbsent(botName)
|
||||
@@ -165,9 +171,80 @@ class OfficialBotService:
|
||||
botAccountId: String,
|
||||
): Unit =
|
||||
val handler: Consumer[String] = msg => handleGameEvent(botName, gameId, playingAs, difficulty, botAccountId, msg)
|
||||
redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:game:$gameId:s2c", handler)
|
||||
val subscriber = redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:game:$gameId:s2c", handler)
|
||||
gameWatches.put(gameId, (botName, subscriber))
|
||||
()
|
||||
|
||||
private def subscribeToGameOverStream(): Unit =
|
||||
Try(
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xgroupCreate(gameOverStream, gameOverGroup, "$", new XGroupCreateArgs().mkstream()),
|
||||
) match
|
||||
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
|
||||
case Failure(ex) => log.warnf(ex, "Failed to create game-over consumer group")
|
||||
case Success(_) => ()
|
||||
executor.submit(
|
||||
new Runnable:
|
||||
def run(): Unit = gameOverPollLoop(),
|
||||
)
|
||||
log.infof("Listening to game-over stream (consumer=%s)", consumerId)
|
||||
|
||||
private def gameOverPollLoop(): Unit =
|
||||
while true do
|
||||
Try {
|
||||
val messages = redis
|
||||
.stream(classOf[String])
|
||||
.xreadgroup(
|
||||
gameOverGroup,
|
||||
consumerId,
|
||||
gameOverStream,
|
||||
">",
|
||||
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
|
||||
)
|
||||
Option(messages).foreach(_.forEach(msg => handleGameOverMessage(msg)))
|
||||
} match
|
||||
case Failure(ex) => log.warnf(ex, "Error in game-over poll loop")
|
||||
case Success(_) => ()
|
||||
|
||||
private def handleGameOverMessage(msg: StreamMessage[String, String, String]): Unit =
|
||||
val json = msg.payload().get("data")
|
||||
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
|
||||
Try {
|
||||
val node = objectMapper.readTree(json)
|
||||
val gameId = node.path("payload").path("gameId").asText()
|
||||
if gameId.nonEmpty then
|
||||
Option(gameWatches.remove(gameId)).foreach { (botName, subscriber) =>
|
||||
val topic = s"${redisConfig.prefix}:game:$gameId:s2c"
|
||||
Try(subscriber.unsubscribe(topic)) match
|
||||
case Failure(ex) => log.warnf(ex, "Failed to unsubscribe from game %s", gameId)
|
||||
case Success(_) => log.infof("Bot %s cleaned up game %s after GameOver", botName, gameId)
|
||||
}
|
||||
} match
|
||||
case Success(_) =>
|
||||
ackGameOver(msg.id())
|
||||
case Failure(ex) if attempt + 1 < maxRetries =>
|
||||
log.warnf(ex, "GameOver handling failed (attempt %d), retrying", attempt)
|
||||
xadd(gameOverStream, Map("data" -> json, "attempt" -> (attempt + 1).toString))
|
||||
ackGameOver(msg.id())
|
||||
case Failure(ex) =>
|
||||
log.errorf(ex, "GameOver handling failed after %d attempts, sending to DLQ", maxRetries)
|
||||
xadd(
|
||||
dlqStream,
|
||||
Map(
|
||||
"data" -> json,
|
||||
"eventType" -> "GameOver",
|
||||
"error" -> Option(ex.getMessage).getOrElse(ex.getClass.getName),
|
||||
"attempt" -> attempt.toString,
|
||||
),
|
||||
)
|
||||
ackGameOver(msg.id())
|
||||
|
||||
private def ackGameOver(id: String): Unit =
|
||||
Try(redis.stream(classOf[String]).xack(gameOverStream, gameOverGroup, id)) match
|
||||
case Failure(ex) => log.warnf(ex, "Failed to ack game-over message %s", id)
|
||||
case Success(_) => ()
|
||||
|
||||
private def handleGameEvent(
|
||||
botName: String,
|
||||
gameId: String,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=16
|
||||
MINOR=17
|
||||
PATCH=0
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./gradlew build
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/tournament-jvm .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tournament-jvm
|
||||
#
|
||||
# This image uses the `run-java.sh` script to run the application.
|
||||
# You can find more information about the UBI base runtime images and their configuration here:
|
||||
# https://rh-openjdk.github.io/redhat-openjdk-containers/
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
# We make four distinct layers so if there are application changes the library layers can be re-used
|
||||
COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
|
||||
COPY --chown=185 build/quarkus-app/*.jar /deployments/
|
||||
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
|
||||
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
|
||||
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
|
||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||
@@ -0,0 +1,33 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./gradlew build -Dquarkus.package.jar.type=legacy-jar
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/tournament-legacy-jar .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tournament-legacy-jar
|
||||
#
|
||||
# This image uses the `run-java.sh` script to run the application.
|
||||
# You can find more information about the UBI base runtime images and their configuration here:
|
||||
# https://rh-openjdk.github.io/redhat-openjdk-containers/
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
COPY build/lib/* /deployments/lib/
|
||||
COPY build/*-runner.jar /deployments/quarkus-run.jar
|
||||
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
|
||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||
@@ -0,0 +1,29 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./gradlew :modules:tournament:build -Dquarkus.native.enabled=true
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f modules/tournament/src/main/docker/Dockerfile.native -t quarkus/tournament .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tournament
|
||||
#
|
||||
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
|
||||
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
|
||||
WORKDIR /work/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
&& chown 1001:root /work
|
||||
COPY --chown=1001:root --chmod=0755 modules/tournament/build/*-runner /work/application
|
||||
|
||||
EXPOSE 8080
|
||||
USER 1001
|
||||
|
||||
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
@@ -0,0 +1,32 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
|
||||
# It uses a micro base image, tuned for Quarkus native executables.
|
||||
# It reduces the size of the resulting container image.
|
||||
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./gradlew build -Dquarkus.native.enabled=true
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/tournament .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tournament
|
||||
#
|
||||
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
|
||||
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
|
||||
###
|
||||
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
|
||||
WORKDIR /work/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
&& chown 1001:root /work
|
||||
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
|
||||
|
||||
EXPOSE 8080
|
||||
USER 1001
|
||||
|
||||
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
@@ -0,0 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=1
|
||||
PATCH=0
|
||||
@@ -204,3 +204,30 @@
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-09)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([e5fe7d0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5fe7d07a58e018151bb24f4ee37c06e72608ded))
|
||||
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
@@ -2,15 +2,18 @@ package de.nowchess.ws.resource
|
||||
|
||||
import de.nowchess.ws.config.RedisConfig
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.redis.datasource.pubsub.PubSubCommands
|
||||
import io.quarkus.redis.datasource.stream.{XAddArgs, XGroupCreateArgs, XReadGroupArgs}
|
||||
import io.quarkus.websockets.next.*
|
||||
import io.smallrye.jwt.auth.principal.JWTParser
|
||||
import jakarta.inject.Inject
|
||||
import org.eclipse.microprofile.context.ManagedExecutor
|
||||
import org.jboss.logging.Logger
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.util.Try
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.Consumer
|
||||
|
||||
@WebSocket(path = "/api/user/ws")
|
||||
class UserWebSocketResource:
|
||||
@@ -18,20 +21,22 @@ class UserWebSocketResource:
|
||||
private val log = Logger.getLogger(classOf[UserWebSocketResource])
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject
|
||||
var redis: RedisDataSource = uninitialized
|
||||
|
||||
@Inject
|
||||
var redisConfig: RedisConfig = uninitialized
|
||||
|
||||
@Inject
|
||||
var jwtParser: JWTParser = uninitialized
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var jwtParser: JWTParser = uninitialized
|
||||
@Inject var executor: ManagedExecutor = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val connections = new ConcurrentHashMap[String, (String, PubSubCommands.RedisSubscriber)]()
|
||||
private val consumerId = UUID.randomUUID().toString
|
||||
private val maxRetries = 3
|
||||
private val maxStreamLen = 1000L
|
||||
|
||||
private def userTopic(userId: String): String =
|
||||
s"${redisConfig.prefix}:user:$userId:events"
|
||||
private val connections = new ConcurrentHashMap[String, (String, WebSocketConnection)]()
|
||||
|
||||
private def userStreamKey(userId: String): String =
|
||||
s"${redisConfig.prefix}:user:$userId:events:stream"
|
||||
|
||||
private def dlqKey: String = s"${redisConfig.prefix}:dlq"
|
||||
|
||||
@OnOpen
|
||||
def onOpen(connection: WebSocketConnection, handshake: HandshakeRequest): Unit =
|
||||
@@ -45,16 +50,76 @@ class UserWebSocketResource:
|
||||
log.warn("WebSocket opened with no valid JWT — closing connection")
|
||||
connection.close().subscribe().`with`(_ => (), _ => ())
|
||||
case Some(userId) =>
|
||||
log.infof("User WebSocket opened — userId=%s", userId)
|
||||
val handler: Consumer[String] = msg => connection.sendText(msg).subscribe().`with`(_ => (), _ => ())
|
||||
val subscriber = redis.pubsub(classOf[String]).subscribe(userTopic(userId), handler)
|
||||
connections.put(connection.id(), (userId, subscriber))
|
||||
log.infof("User WebSocket opened — userId=%s connId=%s", userId, connection.id())
|
||||
createGroupIfAbsent(userId, connection.id())
|
||||
connections.put(connection.id(), (userId, connection))
|
||||
executor.submit(
|
||||
new Runnable:
|
||||
def run(): Unit = pollLoop(connection.id(), userId, connection),
|
||||
)
|
||||
val connectedMsg = s"""{"type":"CONNECTED","userId":"$userId"}"""
|
||||
connection.sendText(connectedMsg).subscribe().`with`(_ => (), _ => ())
|
||||
|
||||
@OnClose
|
||||
def onClose(connection: WebSocketConnection): Unit =
|
||||
log.infof("User WebSocket closed — connectionId=%s", connection.id())
|
||||
Option(connections.remove(connection.id())).foreach { (userId, subscriber) =>
|
||||
subscriber.unsubscribe(userTopic(userId))
|
||||
}
|
||||
connections.remove(connection.id())
|
||||
|
||||
private def createGroupIfAbsent(userId: String, groupName: String): Unit =
|
||||
Try(
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xgroupCreate(userStreamKey(userId), groupName, "$", new XGroupCreateArgs().mkstream()),
|
||||
) match
|
||||
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
|
||||
case Failure(ex) => log.warnf(ex, "Failed to create consumer group for userId=%s", userId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def pollLoop(connectionId: String, userId: String, myConnection: WebSocketConnection): Unit =
|
||||
while Option(connections.get(connectionId)).exists(_._2 eq myConnection) do
|
||||
Try {
|
||||
val messages = redis
|
||||
.stream(classOf[String])
|
||||
.xreadgroup(
|
||||
connectionId,
|
||||
consumerId,
|
||||
userStreamKey(userId),
|
||||
">",
|
||||
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
|
||||
)
|
||||
Option(messages).foreach(_.forEach { msg =>
|
||||
if Option(connections.get(connectionId)).exists(_._2 eq myConnection) then
|
||||
val json = msg.payload().get("data")
|
||||
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
|
||||
Try(myConnection.sendText(json).await().atMost(Duration.ofSeconds(5))) match
|
||||
case Success(_) =>
|
||||
ack(connectionId, userId, msg.id())
|
||||
case Failure(_) if attempt + 1 < maxRetries =>
|
||||
xadd(userStreamKey(userId), json, attempt + 1)
|
||||
ack(connectionId, userId, msg.id())
|
||||
case Failure(ex) =>
|
||||
log.warnf(ex, "Delivery failed for userId=%s after %d attempts, sending to DLQ", userId, maxRetries)
|
||||
xadd(dlqKey, json, attempt)
|
||||
ack(connectionId, userId, msg.id())
|
||||
})
|
||||
} match
|
||||
case Failure(ex) => log.warnf(ex, "Error in poll loop for userId=%s", userId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def ack(groupName: String, userId: String, id: String): Unit =
|
||||
Try(redis.stream(classOf[String]).xack(userStreamKey(userId), groupName, id)) match
|
||||
case Failure(ex) => log.warnf(ex, "Failed to ack message %s for userId=%s", id, userId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def xadd(key: String, json: String, attempt: Int): Unit =
|
||||
Try(
|
||||
redis
|
||||
.stream(classOf[String])
|
||||
.xadd(
|
||||
key,
|
||||
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
|
||||
Map("data" -> json, "attempt" -> attempt.toString).asJava,
|
||||
),
|
||||
) match
|
||||
case Failure(ex) => log.warnf(ex, "Failed to publish to stream %s", key)
|
||||
case Success(_) => ()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=14
|
||||
MINOR=15
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user