Compare commits

..

4 Commits

Author SHA1 Message Date
TeamCity fcc251f777 ci: bump version with Build-75 2026-05-10 21:19:52 +00:00
Janis 7efdd69948 refactor: update meterRegistry usage to Option type and improve formatting
Build & Test (NowChessSystems) TeamCity build finished
2026-05-10 22:56:57 +02:00
Janis 9459203e0d refactor: update timer record calls to use Runnable type
Build & Test (NowChessSystems) TeamCity build failed
2026-05-10 22:24:55 +02:00
Janis d57c488661 feat: configure logging and add OpenTelemetry support (#49)
Build & Test (NowChessSystems) TeamCity build failed
Reviewed-on: #49
2026-05-10 20:31:48 +02:00
27 changed files with 502 additions and 109 deletions
+8 -5
View File
@@ -2,7 +2,7 @@
> **Stack:** raw-http | none | unknown | scala
> 0 routes + 40 rpc | 0 models | 0 components | 163 lib files | 1 env vars | 1 middleware
> 0 routes + 40 rpc | 0 models | 0 components | 164 lib files | 1 env vars | 1 middleware
> **Token savings:** this file is ~0 tokens. Without it, AI exploration would cost ~0 tokens. **Saves ~0 tokens per conversation.**
---
@@ -235,7 +235,7 @@
- function rebalanceInterval
- function rebalanceMinInterval
- function heartbeatTtl
- _...11 more_
- _...12 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoordinatorGrpcServer.scala` — class CoordinatorGrpcServer
@@ -255,6 +255,7 @@
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/AutoScaler.scala`
- class AutoScaler
- function initMetrics
- function checkAndScale
- function scaleUp
- function scaleDown
@@ -273,12 +274,12 @@
- function watchK8sPods
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala`
- class InstanceRegistry
- function initMetrics
- function setRedisPrefix
- function getInstance
- function getAllInstances
- function updateInstanceFromRedis
- function markInstanceDead
- _...1 more_
- _...3 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala`
- class LoadBalancer
- function setRedisPrefix
@@ -319,7 +320,7 @@
- function context
- function pendingDrawOfferBy
- function currentClockState
- _...21 more_
- _...22 more_
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiException.scala`
- class ApiException
- class GameNotFoundException
@@ -371,6 +372,7 @@
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala`
- class RedisGameRegistry
- function initMetrics
- function generateId
- function store
- function get
@@ -588,6 +590,7 @@
- _...6 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — class DefaultRules, function positionOf
- `modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala` — class InternalAuthFilter
- `modules/security/src/main/scala/de/nowchess/security/InternalClientHeadersFactory.scala` — class InternalClientHeadersFactory
- `modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala` — class InternalGrpcAuthInterceptor
- `modules/security/src/main/scala/de/nowchess/security/InternalGrpcSecretClientInterceptor.scala` — class InternalGrpcSecretClientInterceptor
- `modules/security/src/main/scala/de/nowchess/security/InternalSecretClientFilter.scala` — class InternalSecretClientFilter
+7 -4
View File
@@ -179,7 +179,7 @@
- function rebalanceInterval
- function rebalanceMinInterval
- function heartbeatTtl
- _...11 more_
- _...12 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoordinatorGrpcServer.scala` — class CoordinatorGrpcServer
@@ -199,6 +199,7 @@
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/AutoScaler.scala`
- class AutoScaler
- function initMetrics
- function checkAndScale
- function scaleUp
- function scaleDown
@@ -217,12 +218,12 @@
- function watchK8sPods
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala`
- class InstanceRegistry
- function initMetrics
- function setRedisPrefix
- function getInstance
- function getAllInstances
- function updateInstanceFromRedis
- function markInstanceDead
- _...1 more_
- _...3 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala`
- class LoadBalancer
- function setRedisPrefix
@@ -263,7 +264,7 @@
- function context
- function pendingDrawOfferBy
- function currentClockState
- _...21 more_
- _...22 more_
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiException.scala`
- class ApiException
- class GameNotFoundException
@@ -315,6 +316,7 @@
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala`
- class RedisGameRegistry
- function initMetrics
- function generateId
- function store
- function get
@@ -532,6 +534,7 @@
- _...6 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — class DefaultRules, function positionOf
- `modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala` — class InternalAuthFilter
- `modules/security/src/main/scala/de/nowchess/security/InternalClientHeadersFactory.scala` — class InternalClientHeadersFactory
- `modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala` — class InternalGrpcAuthInterceptor
- `modules/security/src/main/scala/de/nowchess/security/InternalGrpcSecretClientInterceptor.scala` — class InternalGrpcSecretClientInterceptor
- `modules/security/src/main/scala/de/nowchess/security/InternalSecretClientFilter.scala` — class InternalSecretClientFilter
+19
View File
@@ -114,3 +114,22 @@
### Bug Fixes
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
## (2026-05-10)
### Features
* **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))
* **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))
### Bug Fixes
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
@@ -4,6 +4,7 @@ import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
import de.nowchess.account.dto.{LoginRequest, RegisterRequest}
import de.nowchess.account.error.AccountError
import de.nowchess.account.repository.{BotAccountRepository, OfficialBotAccountRepository, UserAccountRepository}
import io.micrometer.core.instrument.MeterRegistry
import io.quarkus.elytron.security.common.BcryptUtil
import io.smallrye.jwt.build.Jwt
import jakarta.enterprise.context.ApplicationScoped
@@ -29,11 +30,21 @@ class AccountService:
@Inject
var officialBotAccountRepository: OfficialBotAccountRepository = uninitialized
@Inject
var meterRegistry: MeterRegistry = uninitialized
// scalafix:on
@Transactional
def register(req: RegisterRequest): Either[AccountError, UserAccount] =
log.infof("Registering user %s", req.username)
val result = registerAccount(req)
result match
case Right(_) => meterRegistry.counter("nowchess.users.registrations", "result", "success").increment()
case Left(_) => meterRegistry.counter("nowchess.users.registrations", "result", "failure").increment()
result
private def registerAccount(req: RegisterRequest): Either[AccountError, UserAccount] =
if userAccountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
else if userAccountRepository.findByEmail(req.email).isDefined then
Left(AccountError.EmailAlreadyRegistered(req.email))
@@ -48,6 +59,15 @@ class AccountService:
Right(account)
def login(req: LoginRequest): Either[AccountError, String] =
val result = authenticateUser(req)
result match
case Right(_) => meterRegistry.counter("nowchess.auth.logins", "result", "success").increment()
case Left(error) =>
meterRegistry.counter("nowchess.auth.logins", "result", "failure").increment()
meterRegistry.counter("nowchess.auth.login.failures", "reason", loginFailureReason(error)).increment()
result
private def authenticateUser(req: LoginRequest): Either[AccountError, String] =
userAccountRepository.findByUsername(req.username) match
case None =>
log.warnf("Login failed for unknown user %s", req.username)
@@ -69,6 +89,17 @@ class AccountService:
.sign(),
)
private def loginFailureReason(error: AccountError): String = error match
case AccountError.InvalidCredentials => "invalid_credentials"
case AccountError.UserBanned => "user_banned"
case AccountError.UsernameTaken(_) => "username_taken"
case AccountError.EmailAlreadyRegistered(_) => "email_registered"
case AccountError.UserNotFound => "user_not_found"
case AccountError.BotNotFound => "bot_not_found"
case AccountError.BotLimitExceeded => "bot_limit_exceeded"
case AccountError.NotAuthorized => "not_authorized"
case AccountError.BotBanned => "bot_banned"
def findByUsername(username: String): Option[UserAccount] =
userAccountRepository.findByUsername(username)
@@ -18,6 +18,7 @@ import de.nowchess.account.dto.{
}
import de.nowchess.account.error.ChallengeError
import de.nowchess.account.repository.{ChallengeRepository, UserAccountRepository}
import io.micrometer.core.instrument.MeterRegistry
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.transaction.Transactional
@@ -48,10 +49,22 @@ class ChallengeService:
@Inject
var eventPublisher: EventPublisher = uninitialized
@Inject
var meterRegistry: MeterRegistry = uninitialized
// scalafix:on
@Transactional
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
val result = createChallenge(challengerId, destUsername, req)
result.foreach(_ => meterRegistry.counter("nowchess.challenges.created").increment())
result
private def createChallenge(
challengerId: UUID,
destUsername: String,
req: ChallengeRequest,
): Either[ChallengeError, Challenge] =
for
destUser <- userAccountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
challenger <- userAccountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
@@ -80,6 +93,11 @@ class ChallengeService:
@Transactional
def accept(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
val result = acceptChallenge(challengeId, userId)
result.foreach(_ => meterRegistry.counter("nowchess.challenges.accepted").increment())
result
private def acceptChallenge(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
@@ -96,6 +114,15 @@ class ChallengeService:
@Transactional
def decline(challengeId: UUID, userId: UUID, req: DeclineRequest): Either[ChallengeError, Challenge] =
val result = declineChallenge(challengeId, userId, req)
result.foreach(_ => meterRegistry.counter("nowchess.challenges.declined").increment())
result
private def declineChallenge(
challengeId: UUID,
userId: UUID,
req: DeclineRequest,
): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
@@ -109,6 +136,11 @@ class ChallengeService:
@Transactional
def cancel(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
val result = cancelChallenge(challengeId, userId)
result.foreach(_ => meterRegistry.counter("nowchess.challenges.cancelled").increment())
result
private def cancelChallenge(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=9
MINOR=10
PATCH=0
+26
View File
@@ -253,3 +253,29 @@
* **middleware:** update paths for bot generation and stockfish configuration ([2dd0501](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2dd0501687db08dcd242359f6837125baf8a2fdc))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* update HealthMonitor to evict instances without associated pods ([0f41f13](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0f41f13ce68b76846684bab67241a122250dfaf9))
## (2026-05-10)
### Features
* **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))
* **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))
### Bug Fixes
* add instance-dead-timeout configuration and update HealthMonitor to use it for stale instance eviction ([be0b710](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/be0b710543b542da5c301efef7d2d587d0ba758a))
* **coordinator:** refine type casting in rolloutSpec method ([#45](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/45)) ([d522f7f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d522f7f6edf9c985f03dd16816439d4184f1a589))
* **coordinator:** use genericKubernetesResources API for Argo Rollout scaling ([#43](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/43)) ([fa3c6b2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fa3c6b2886dc59c14c5dad834acc9b41e42023bb))
* **coordinator:** use genericKubernetesResources API for Argo Rollout scaling ([#44](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/44)) ([82d0b75](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/82d0b754be1075084944b466858672d944f9f7d8))
* **dependencies:** replace Fabric8 Kubernetes client with Quarkus Kubernetes client ([5f44570](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5f44570b357277d09f33b7296860c421e2e70ce0))
* enhance AutoScaler and InstanceRegistry for replica management and stale instance eviction ([b4920d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4920d3817e58bda94d7764e608b856ce9a909f7))
* **middleware:** update paths for bot generation and stockfish configuration ([2dd0501](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2dd0501687db08dcd242359f6837125baf8a2fdc))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* update HealthMonitor to evict instances without associated pods ([0f41f13](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0f41f13ce68b76846684bab67241a122250dfaf9))
@@ -1,13 +1,16 @@
package de.nowchess.coordinator.service
import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.inject.Instance
import jakarta.inject.Inject
import de.nowchess.coordinator.config.CoordinatorConfig
import io.fabric8.kubernetes.api.model.GenericKubernetesResource
import io.fabric8.kubernetes.client.KubernetesClient
import io.micrometer.core.instrument.{Gauge, MeterRegistry}
import org.jboss.logging.Logger
import java.util.concurrent.atomic.AtomicReference
import scala.compiletime.uninitialized
@ApplicationScoped
@@ -21,10 +24,14 @@ class AutoScaler:
@Inject
private var instanceRegistry: InstanceRegistry = uninitialized
@Inject
private var meterRegistry: MeterRegistry = uninitialized
// scalafix:on DisableSyntax.var
private val log = Logger.getLogger(classOf[AutoScaler])
private val lastScaleTime = new java.util.concurrent.atomic.AtomicLong(0L)
private val avgLoadRef = new AtomicReference[Double](0.0)
private def kubeClientOpt: Option[KubernetesClient] =
if kubeClientInstance.isUnsatisfied then None
@@ -33,6 +40,13 @@ class AutoScaler:
private val argoApiVersion = "argoproj.io/v1alpha1"
private val argoKind = "Rollout"
@PostConstruct
def initMetrics(): Unit =
Gauge
.builder("nowchess.coordinator.load.average", avgLoadRef, _.get())
.register(meterRegistry)
()
// scalafix:off DisableSyntax.asInstanceOf
private def rolloutSpec(rollout: GenericKubernetesResource): Option[java.util.Map[String, AnyRef]] =
Option(rollout.get[AnyRef]("spec")).collect { case m: java.util.Map[?, ?] =>
@@ -48,6 +62,7 @@ class AutoScaler:
val instances = instanceRegistry.getAllInstances.filter(_.state == "HEALTHY")
if instances.nonEmpty then
val avgLoad = instances.map(_.subscriptionCount).sum.toDouble / instances.size
avgLoadRef.set(avgLoad)
if avgLoad > config.scaleUpThreshold * config.maxGamesPerCore then scaleUp()
else if avgLoad < config.scaleDownThreshold * config.maxGamesPerCore then scaleDown()
@@ -79,6 +94,7 @@ class AutoScaler:
.inNamespace(config.k8sNamespace)
.resource(rollout)
.update()
meterRegistry.counter("nowchess.coordinator.scale.events", "direction", "up").increment()
log.infof(
"Scaled up %s from %d to %d replicas",
config.k8sRolloutName,
@@ -91,6 +107,7 @@ class AutoScaler:
}
catch
case ex: Exception =>
meterRegistry.counter("nowchess.coordinator.scale.failures", "direction", "up").increment()
log.warnf(ex, "Failed to scale up %s", config.k8sRolloutName)
def scaleDown(): Unit =
@@ -120,6 +137,7 @@ class AutoScaler:
.inNamespace(config.k8sNamespace)
.resource(rollout)
.update()
meterRegistry.counter("nowchess.coordinator.scale.events", "direction", "down").increment()
log.infof(
"Scaled down %s from %d to %d replicas",
config.k8sRolloutName,
@@ -132,4 +150,5 @@ class AutoScaler:
}
catch
case ex: Exception =>
meterRegistry.counter("nowchess.coordinator.scale.failures", "direction", "down").increment()
log.warnf(ex, "Failed to scale down %s", config.k8sRolloutName)
@@ -5,6 +5,7 @@ import jakarta.inject.Inject
import io.quarkus.redis.datasource.RedisDataSource
import de.nowchess.coordinator.config.CoordinatorConfig
import com.fasterxml.jackson.databind.ObjectMapper
import io.micrometer.core.instrument.MeterRegistry
import scala.jdk.CollectionConverters.*
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
@@ -30,6 +31,9 @@ class CacheEvictionManager:
@Inject
private var objectMapper: ObjectMapper = uninitialized
@Inject
private var meterRegistry: MeterRegistry = uninitialized
private val log = Logger.getLogger(classOf[CacheEvictionManager])
private var redisPrefix = "nowchess"
// scalafix:on DisableSyntax.var
@@ -38,8 +42,10 @@ class CacheEvictionManager:
redisPrefix = prefix
def evictStaleGames: Unit =
log.info("Starting cache eviction scan")
meterRegistry.timer("nowchess.coordinator.cache.eviction.duration").record((() => runEviction()): Runnable)
private def runEviction(): Unit =
log.info("Starting cache eviction scan")
val pattern = s"$redisPrefix:game:entry:*"
val keys = redis.key(classOf[String]).keys(pattern)
val now = System.currentTimeMillis()
@@ -56,6 +62,7 @@ class CacheEvictionManager:
try
coreGrpcClient.evictGames(instance.hostname, instance.grpcPort, List(gameId))
redis.key(classOf[String]).del(key)
meterRegistry.counter("nowchess.coordinator.cache.evictions").increment()
log.infof("Evicted idle game %s from %s", gameId, instance.instanceId)
count + 1
catch
@@ -6,6 +6,7 @@ import jakarta.inject.Inject
import de.nowchess.coordinator.config.CoordinatorConfig
import io.fabric8.kubernetes.client.KubernetesClient
import io.fabric8.kubernetes.api.model.Pod
import io.micrometer.core.instrument.MeterRegistry
import io.quarkus.redis.datasource.RedisDataSource
import scala.jdk.CollectionConverters.*
import org.jboss.logging.Logger
@@ -27,6 +28,9 @@ class HealthMonitor:
@Inject
private var redis: RedisDataSource = uninitialized
@Inject
private var meterRegistry: MeterRegistry = uninitialized
private val log = Logger.getLogger(classOf[HealthMonitor])
private var redisPrefix = "nowchess"
// scalafix:on DisableSyntax.var
@@ -39,6 +43,7 @@ class HealthMonitor:
redisPrefix = prefix
def checkInstanceHealth: Unit =
meterRegistry.counter("nowchess.coordinator.health.checks").increment()
val evicted = instanceRegistry.evictStaleInstances(config.instanceDeadTimeout)
if evicted.nonEmpty then log.warnf("Evicted %d stale instances: %s", evicted.size, evicted.mkString(", "))
val instances = instanceRegistry.getAllInstances
@@ -108,6 +113,7 @@ class HealthMonitor:
case Some(pod) =>
val isReady = isPodReady(pod)
if !isReady && inst.state == "HEALTHY" then
meterRegistry.counter("nowchess.coordinator.pods.unhealthy").increment()
log.warnf("Pod %s not ready, marking instance %s dead", pod.getMetadata.getName, inst.instanceId)
instanceRegistry.markInstanceDead(inst.instanceId)
case None =>
@@ -1,5 +1,6 @@
package de.nowchess.coordinator.service
import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import io.quarkus.redis.datasource.ReactiveRedisDataSource
@@ -9,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.coordinator.dto.InstanceMetadata
import java.util.concurrent.ConcurrentHashMap
import java.time.{Duration, Instant}
import io.micrometer.core.instrument.{Gauge, MeterRegistry}
import io.smallrye.mutiny.Uni
import org.jboss.logging.Logger
@@ -18,12 +20,22 @@ class InstanceRegistry:
@Inject
private var redis: ReactiveRedisDataSource = uninitialized
private var redisPrefix = "nowchess"
@Inject
private var meterRegistry: MeterRegistry = uninitialized
// scalafix:on DisableSyntax.var
private val log = Logger.getLogger(classOf[InstanceRegistry])
private val mapper = ObjectMapper()
private val instances = ConcurrentHashMap[String, InstanceMetadata]()
@PostConstruct
def initMetrics(): Unit =
Gauge
.builder("nowchess.coordinator.instances.active", instances, m => m.size().toDouble)
.register(meterRegistry)
()
def setRedisPrefix(prefix: String): Unit =
redisPrefix = prefix
@@ -45,6 +57,7 @@ class InstanceRegistry:
val isNew = !instances.containsKey(instanceId)
instances.put(instanceId, metadata)
if isNew then
meterRegistry.counter("nowchess.coordinator.instances.joined").increment()
log.infof("Instance %s joined registry (subscriptions=%d)", instanceId, metadata.subscriptionCount)
else
log.debugf(
@@ -68,6 +81,7 @@ class InstanceRegistry:
def removeInstance(instanceId: String): Unit =
instances.remove(instanceId)
meterRegistry.counter("nowchess.coordinator.instances.removed").increment()
log.infof("Instance %s removed from registry", instanceId)
def evictStaleInstances(maxAge: Duration): List[String] =
@@ -83,6 +97,7 @@ class InstanceRegistry:
.toList
stale.foreach { id =>
instances.remove(id)
meterRegistry.counter("nowchess.coordinator.instances.evicted").increment()
log.warnf("Evicted stale instance %s (heartbeat older than %s)", id, maxAge)
}
stale
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=15
MINOR=16
PATCH=0
+47
View File
@@ -888,3 +888,50 @@
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* 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))
## (2026-05-10)
### Features
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* **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))
* 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))
* **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-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* **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))
* 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))
* 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))
* **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))
* 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))
@@ -17,10 +17,12 @@ import de.nowchess.chess.observer.*
import de.nowchess.api.error.GameError
import de.nowchess.api.game.WinReason.{Checkmate, Resignation}
import de.nowchess.api.io.{GameContextExport, GameContextImport}
import de.nowchess.api.rules.RuleSet
import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
import io.micrometer.core.instrument.{Counter, MeterRegistry, Timer}
import java.time.Instant
import java.util.concurrent.{Executors, ScheduledExecutorService, ScheduledFuture, TimeUnit}
import java.util.concurrent.atomic.AtomicInteger
/** Pure game engine that manages game state and notifies observers of state changes. All rule queries delegate to the
* injected RuleSet. All user interactions go through Commands; state changes are broadcast via GameEvents.
@@ -33,6 +35,7 @@ class GameEngine(
initialDrawOffer: Option[Color] = None,
initialRedoStack: List[Move] = Nil,
initialTakebackRequest: Option[Color] = None,
private val meterRegistry: Option[MeterRegistry] = None,
) extends Observable:
// Ensure that initialBoard is set correctly for threefold repetition detection
private val contextWithInitialBoard =
@@ -57,6 +60,17 @@ class GameEngine(
@SuppressWarnings(Array("DisableSyntax.var"))
private var pendingTakebackRequest: Option[Color] = initialTakebackRequest
meterRegistry.foreach { reg =>
GameEngine.activeGamesCount.incrementAndGet()
reg.counter("nowchess.games.started").increment()
}
private def gamesCompletedCounter(result: String): Counter =
meterRegistry.map(_.counter("nowchess.games.completed", "result", result)).orNull
private def movesProcessedCounter: Counter =
meterRegistry.map(_.counter("nowchess.moves.processed")).orNull
private def movesDurationTimer: Timer =
meterRegistry.map(_.timer("nowchess.moves.duration")).orNull
// Start scheduler immediately for live clocks so passive expiry fires without waiting for a move.
clockState.foreach(scheduleExpiryCheck)
@@ -165,6 +179,8 @@ class GameEngine(
pendingTakebackRequest = None
stopClock()
redoStack = Nil
Option(gamesCompletedCounter("resignation")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(ResignEvent(currentContext, color))
}
@@ -197,6 +213,8 @@ class GameEngine(
pendingTakebackRequest = None
stopClock()
redoStack = Nil
Option(gamesCompletedCounter("draw.agreement")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, DrawReason.Agreement))
}
@@ -223,11 +241,15 @@ class GameEngine(
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
stopClock()
redoStack = Nil
Option(gamesCompletedCounter("draw.fifty_move")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
else if ruleSet.isThreefoldRepetition(currentContext) then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.ThreefoldRepetition)))
stopClock()
redoStack = Nil
Option(gamesCompletedCounter("draw.threefold")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, DrawReason.ThreefoldRepetition))
else notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.DrawCannotBeClaimed))
}
@@ -311,9 +333,18 @@ class GameEngine(
currentContext = currentContext.withResult(Some(GameResult.Draw(reason)))
stopClock()
redoStack = Nil
Option(gamesCompletedCounter(drawReasonTag(reason))).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, reason))
}
private def drawReasonTag(reason: DrawReason): String = reason match
case DrawReason.Agreement => "draw.agreement"
case DrawReason.FiftyMoveRule => "draw.fifty_move"
case DrawReason.ThreefoldRepetition => "draw.threefold"
case DrawReason.Stalemate => "draw.stalemate"
case DrawReason.InsufficientMaterial => "draw.insufficient"
/** Inject clock state directly (for testing). */
private[engine] def injectClockState(cs: Option[ClockState]): Unit = synchronized { clockState = cs }
@@ -334,6 +365,11 @@ class GameEngine(
pendingDrawOffer = None
pendingTakebackRequest = None
redoStack = Nil
val tag = result match
case GameResult.Draw(_) => "draw.insufficient"
case _ => "timeout"
Option(gamesCompletedCounter(tag)).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(TimeFlagEvent(currentContext, flagged))
private def scheduleExpiryCheck(cs: ClockState): Unit =
@@ -369,6 +405,12 @@ class GameEngine(
// ──── Private helpers ────
private def executeMove(move: Move): Unit =
Option(movesDurationTimer) match
case Some(timer) => timer.record((() => executeMoveBody(move)): Runnable)
case None => executeMoveBody(move)
Option(movesProcessedCounter).foreach(_.increment())
private def executeMoveBody(move: Move): Unit =
if !isRedoing then
redoStack = Nil
pendingTakebackRequest = None
@@ -391,28 +433,36 @@ class GameEngine(
)
val status = ruleSet.postMoveStatus(currentContext)
if currentContext.result.isEmpty then
if status.isCheckmate then
val winner = currentContext.turn.opposite
currentContext = currentContext.withResult(Some(GameResult.Win(winner, Checkmate)))
cancelScheduled()
notifyObservers(CheckmateEvent(currentContext, winner))
redoStack = Nil
else if status.isStalemate then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate)))
cancelScheduled()
notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
redoStack = Nil
else if status.isInsufficientMaterial then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial)))
cancelScheduled()
notifyObservers(DrawEvent(currentContext, DrawReason.InsufficientMaterial))
redoStack = Nil
else if status.isCheck then notifyObservers(CheckDetectedEvent(currentContext))
if currentContext.result.isEmpty then applyPostMoveStatus(status)
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
if status.isThreefoldRepetition then notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
private def applyPostMoveStatus(status: PostMoveStatus): Unit =
if status.isCheckmate then
val winner = currentContext.turn.opposite
currentContext = currentContext.withResult(Some(GameResult.Win(winner, Checkmate)))
cancelScheduled()
Option(gamesCompletedCounter("checkmate")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(CheckmateEvent(currentContext, winner))
redoStack = Nil
else if status.isStalemate then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate)))
cancelScheduled()
Option(gamesCompletedCounter("draw.stalemate")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
redoStack = Nil
else if status.isInsufficientMaterial then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial)))
cancelScheduled()
Option(gamesCompletedCounter("draw.insufficient")).foreach(_.increment())
GameEngine.activeGamesCount.decrementAndGet()
notifyObservers(DrawEvent(currentContext, DrawReason.InsufficientMaterial))
redoStack = Nil
else if status.isCheck then notifyObservers(CheckDetectedEvent(currentContext))
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
move.moveType match
case MoveType.CastleKingside => "O-O"
@@ -526,3 +576,6 @@ class GameEngine(
pendingTakebackRequest = None
notifyObservers(TakebackDeclinedEvent(currentContext, color))
}
object GameEngine:
val activeGamesCount: AtomicInteger = new AtomicInteger(0)
@@ -12,7 +12,9 @@ import de.nowchess.chess.grpc.RuleSetGrpcAdapter
import de.nowchess.chess.config.RedisConfig
import de.nowchess.chess.grpc.IoGrpcClientWrapper
import de.nowchess.chess.resource.GameDtoMapper
import io.micrometer.core.instrument.{Gauge, MeterRegistry}
import io.quarkus.redis.datasource.RedisDataSource
import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import org.eclipse.microprofile.rest.client.inject.RestClient
@@ -34,12 +36,20 @@ class RedisGameRegistry extends GameRegistry:
@Inject var ioClient: IoGrpcClientWrapper = uninitialized
@Inject var ruleSetAdapter: RuleSetGrpcAdapter = uninitialized
@Inject @RestClient var storeClient: StoreServiceClient = uninitialized
@Inject var meterRegistry: MeterRegistry = uninitialized
// scalafix:on
private val log = Logger.getLogger(classOf[RedisGameRegistry])
private val localEngines = ConcurrentHashMap[String, GameEntry]()
private val rng = new SecureRandom()
@PostConstruct
def initMetrics(): Unit =
Gauge
.builder("nowchess.games.active", GameEngine.activeGamesCount, _.get().toDouble)
.register(meterRegistry)
()
private def cacheKey(gameId: String) = s"${redisConfig.prefix}:game:entry:$gameId"
def generateId(): String =
@@ -58,14 +68,17 @@ class RedisGameRegistry extends GameRegistry:
)
def get(gameId: String): Option[GameEntry] =
Option(localEngines.get(gameId)) match
val result = Option(localEngines.get(gameId)) match
case Some(localEntry) =>
meterRegistry.counter("nowchess.games.cache.hits", "source", "local").increment()
readRedisDto(gameId).flatMap(dto => Try(reconstruct(dto)).toOption) match
case Some(redisEntry) if !sameSnapshot(localEntry, redisEntry) =>
localEngines.put(gameId, redisEntry)
Some(redisEntry)
case _ => Some(localEntry)
case None => fromRedis(gameId).orElse(fromDb(gameId))
if result.isEmpty then meterRegistry.counter("nowchess.games.cache.misses").increment()
result
def update(entry: GameEntry): Unit =
localEngines.put(entry.gameId, entry)
@@ -86,6 +99,7 @@ class RedisGameRegistry extends GameRegistry:
}
}
.map { entry =>
meterRegistry.counter("nowchess.games.cache.hits", "source", "redis").increment()
localEngines.put(gameId, entry)
log.infof("Loaded game %s from Redis cache", gameId)
entry
@@ -118,6 +132,7 @@ class RedisGameRegistry extends GameRegistry:
(dto, reconstruct(dto))
} match
case scala.util.Success((dto, entry)) =>
meterRegistry.counter("nowchess.games.cache.hits", "source", "db").increment()
log.infof("Loaded game %s from store service", gameId)
localEngines.put(gameId, entry)
redis.value(classOf[String]).setex(cacheKey(gameId), 1800L, objectMapper.writeValueAsString(dto))
@@ -19,6 +19,8 @@ import de.nowchess.coordinator.proto.{CoordinatorServiceGrpc, *}
import de.nowchess.coordinator.proto.CoordinatorServiceGrpc.CoordinatorServiceStub
import io.grpc.stub.StreamObserver
import io.grpc.Channel
import io.micrometer.core.instrument.{Gauge, MeterRegistry}
import java.util.concurrent.atomic.AtomicInteger
@ApplicationScoped
class InstanceHeartbeatService:
@@ -32,6 +34,9 @@ class InstanceHeartbeatService:
@Inject
private var mapper: ObjectMapper = uninitialized
@Inject
private var meterRegistry: MeterRegistry = uninitialized
@GrpcClient("coordinator-grpc")
private var channel: Channel = uninitialized
@@ -51,14 +56,15 @@ class InstanceHeartbeatService:
private var streamObserver: Option[StreamObserver[HeartbeatFrame]] = None
private var heartbeatExecutor = Executors.newScheduledThreadPool(1)
private var redisHeartbeatExecutor = Executors.newScheduledThreadPool(1)
private var subscriptionCount = 0
private var localCacheSize = 0
private var serviceActive = false
private var shuttingDown = false
// scalafix:on DisableSyntax.var
private val redisHeartbeatPending = new AtomicBoolean(false)
private val subscriptionCount = new AtomicInteger(0)
def onStart(@Observes event: StartupEvent): Unit =
Gauge.builder("nowchess.instance.subscriptions", subscriptionCount, _.get().toDouble).register(meterRegistry)
if coordinatorEnabled then
try
shuttingDown = false
@@ -90,7 +96,7 @@ class InstanceHeartbeatService:
redisPrefix = prefix
def setSubscriptionCount(count: Int): Unit =
subscriptionCount = count
subscriptionCount.set(count)
def setLocalCacheSize(count: Int): Unit =
localCacheSize = count
@@ -99,13 +105,13 @@ class InstanceHeartbeatService:
if coordinatorEnabled then
val setKey = s"$redisPrefix:instance:$instanceId:games"
redis.set(classOf[String]).sadd(setKey, gameId)
subscriptionCount += 1
subscriptionCount.incrementAndGet()
def removeGameSubscription(gameId: String): Unit =
if coordinatorEnabled then
val setKey = s"$redisPrefix:instance:$instanceId:games"
redis.set(classOf[String]).srem(setKey, gameId)
subscriptionCount = Math.max(0, subscriptionCount - 1)
subscriptionCount.updateAndGet(c => Math.max(0, c - 1))
private def generateInstanceId(): Unit =
val hostname =
@@ -162,13 +168,14 @@ class InstanceHeartbeatService:
.setHostname(getHostname)
.setHttpPort(httpPort)
.setGrpcPort(grpcPort)
.setSubscriptionCount(subscriptionCount)
.setSubscriptionCount(subscriptionCount.get())
.setLocalCacheSize(localCacheSize)
.setTimestampMillis(System.currentTimeMillis())
.build()
observer.onNext(frame)
catch
case ex: Exception =>
meterRegistry.counter("nowchess.heartbeat.failures").increment()
log.warnf(ex, "Failed to send heartbeat frame")
}
@@ -182,7 +189,7 @@ class InstanceHeartbeatService:
"hostname" -> getHostname,
"httpPort" -> httpPort,
"grpcPort" -> grpcPort,
"subscriptionCount" -> subscriptionCount,
"subscriptionCount" -> subscriptionCount.get(),
"localCacheSize" -> localCacheSize,
"lastHeartbeat" -> java.time.Instant.now().toString,
"state" -> "HEALTHY",
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=31
MINOR=32
PATCH=0
+13
View File
@@ -80,3 +80,16 @@
* 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))
## (2026-05-10)
### Features
* **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))
* 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))
@@ -7,6 +7,7 @@ import de.nowchess.bot.ai.Evaluation
import de.nowchess.bot.util.ZobristHash
import de.nowchess.api.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
import io.micrometer.core.instrument.MeterRegistry
import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}
final class AlphaBetaSearch(
@@ -14,6 +15,7 @@ final class AlphaBetaSearch(
tt: TranspositionTable = TranspositionTable(),
weights: Evaluation,
numThreads: Int = Runtime.getRuntime.availableProcessors,
meterRegistry: Option[MeterRegistry] = None,
):
private val INF = Int.MaxValue / 2
@@ -85,8 +87,8 @@ final class AlphaBetaSearch(
val rootHash = ZobristHash.hash(context)
@scala.annotation.tailrec
def loop(bestSoFar: Option[Move], prevScore: Int, depth: Int): Option[Move] =
if isOutOfTime then bestSoFar
def loop(bestSoFar: Option[Move], prevScore: Int, depth: Int, lastDepth: Int): (Option[Move], Int) =
if isOutOfTime then (bestSoFar, lastDepth)
else
val (alpha, beta) =
if depth == 1 then (-INF, INF) else (prevScore - ASPIRATION_DELTA, prevScore + ASPIRATION_DELTA)
@@ -99,9 +101,17 @@ final class AlphaBetaSearch(
rootHash,
excludedRootMoves,
)
loop(move.orElse(bestSoFar), score, depth + 1)
loop(move.orElse(bestSoFar), score, depth + 1, depth)
loop(None, 0, 1)
val (result, depthReached) = loop(None, 0, 1, 0)
recordSearchMetrics(depthReached)
result
private def recordSearchMetrics(depthReached: Int): Unit =
meterRegistry.foreach { mr =>
mr.summary("nowchess.bot.search.nodes").record(nodeCount.get().toDouble)
mr.summary("nowchess.bot.search.depth").record(depthReached.toDouble)
}
private def isOutOfTime: Boolean =
System.currentTimeMillis - timeStartMs.get >= timeLimitMs.get
@@ -6,6 +6,7 @@ import de.nowchess.bot.BotController
import de.nowchess.bot.BotDifficulty
import de.nowchess.bot.config.RedisConfig
import de.nowchess.io.fen.FenParser
import io.micrometer.core.instrument.MeterRegistry
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.runtime.StartupEvent
import jakarta.enterprise.context.ApplicationScoped
@@ -22,6 +23,7 @@ class OfficialBotService:
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var botController: BotController = uninitialized
@Inject var meterRegistry: MeterRegistry = uninitialized
// scalafix:on DisableSyntax.var
private val terminalStatuses =
@@ -85,7 +87,10 @@ class OfficialBotService:
val level = DifficultyMapper.fromElo(difficulty).getOrElse(BotDifficulty.Medium)
botController.getBot(botName).orElse(botController.getBot(level.toString.toLowerCase)).foreach { bot =>
FenParser.parseFen(fen).toOption.foreach { context =>
bot(context).foreach { move =>
val timer = meterRegistry.timer("nowchess.bot.move.duration", "bot", botName)
val moveOpt = timer.recordCallable[Option[Move]](() => bot(context))
moveOpt.foreach { move =>
meterRegistry.counter("nowchess.bot.moves.computed", "bot", botName).increment()
val uci = toUci(move)
val c2sTopic = s"${redisConfig.prefix}:game:$gameId:c2s"
val moveMsg = s"""{"type":"MOVE","uci":"$uci","playerId":"$botAccountId"}"""
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=8
MINOR=9
PATCH=0
+18
View File
@@ -109,3 +109,21 @@
### Bug Fixes
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
## (2026-05-10)
### Features
* **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))
* **config:** update application.yml to nest HTTP port configuration ([3efebd5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3efebd5ed0493159c51f7246d18d59bac58cf875))
* 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))
* 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))
### Bug Fixes
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
@@ -3,6 +3,7 @@ package de.nowchess.store.service
import de.nowchess.api.dto.GameWritebackEventDto
import de.nowchess.store.domain.GameRecord
import de.nowchess.store.repository.GameRecordRepository
import io.micrometer.core.instrument.{Counter, MeterRegistry, Timer}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.transaction.Transactional
@@ -12,64 +13,84 @@ import java.time.Instant
@ApplicationScoped
class GameWritebackService:
@Inject
// scalafix:off DisableSyntax.var
@Inject
var repository: GameRecordRepository = uninitialized
// scalafix:on
@Inject
var meterRegistry: MeterRegistry = uninitialized
// scalafix:on DisableSyntax.var
private lazy val writebackTimer: Timer =
meterRegistry.timer("nowchess.store.writeback.duration")
private lazy val writebackSkipped: Counter =
meterRegistry.counter("nowchess.store.writeback.skipped")
private def gamesWrittenCounter(operation: String): Counter =
meterRegistry.counter("nowchess.store.games.written", "operation", operation)
@Transactional
def writeBack(event: GameWritebackEventDto): Unit =
writebackTimer.record((() => doWriteBack(event)): Runnable)
private def doWriteBack(event: GameWritebackEventDto): Unit =
repository.findByGameId(event.gameId) match
case None =>
val record = new GameRecord
record.gameId = event.gameId
record.fen = event.fen
record.pgn = event.pgn
record.moveCount = event.moveCount
record.whiteId = event.whiteId
record.whiteName = event.whiteName
record.blackId = event.blackId
record.blackName = event.blackName
record.mode = event.mode
record.resigned = event.resigned
record.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
record.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
record.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
record.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
record.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
record.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
record.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
record.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
record.clockActiveColor = event.clockActiveColor.orNull
record.pendingDrawOffer = event.pendingDrawOffer.orNull
record.result = event.result.orNull
record.terminationReason = event.terminationReason.orNull
record.createdAt = Instant.now()
record.updatedAt = Instant.now()
repository.persist(record)
createRecord(event)
gamesWrittenCounter("create").increment()
case Some(r) if event.moveCount > r.moveCount || event.pgn != r.pgn =>
r.fen = event.fen
r.pgn = event.pgn
r.moveCount = event.moveCount
r.whiteId = event.whiteId
r.whiteName = event.whiteName
r.blackId = event.blackId
r.blackName = event.blackName
r.mode = event.mode
r.resigned = event.resigned
r.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
r.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
r.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
r.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
r.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
r.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
r.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
r.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
r.clockActiveColor = event.clockActiveColor.orNull
r.pendingDrawOffer = event.pendingDrawOffer.orNull
r.pendingTakebackOffer = event.pendingTakebackRequest.orNull
r.result = event.result.orNull
r.terminationReason = event.terminationReason.orNull
r.updatedAt = Instant.now()
repository.merge(r)
case _ => ()
updateRecord(r, event)
gamesWrittenCounter("update").increment()
case _ =>
writebackSkipped.increment()
private def createRecord(event: GameWritebackEventDto): Unit =
val record = new GameRecord
record.gameId = event.gameId
record.fen = event.fen
record.pgn = event.pgn
record.moveCount = event.moveCount
record.whiteId = event.whiteId
record.whiteName = event.whiteName
record.blackId = event.blackId
record.blackName = event.blackName
record.mode = event.mode
record.resigned = event.resigned
applyClockFields(record, event)
record.result = event.result.orNull
record.terminationReason = event.terminationReason.orNull
record.createdAt = Instant.now()
record.updatedAt = Instant.now()
repository.persist(record)
private def updateRecord(r: GameRecord, event: GameWritebackEventDto): Unit =
r.fen = event.fen
r.pgn = event.pgn
r.moveCount = event.moveCount
r.whiteId = event.whiteId
r.whiteName = event.whiteName
r.blackId = event.blackId
r.blackName = event.blackName
r.mode = event.mode
r.resigned = event.resigned
applyClockFields(r, event)
r.pendingDrawOffer = event.pendingDrawOffer.orNull
r.pendingTakebackOffer = event.pendingTakebackRequest.orNull
r.result = event.result.orNull
r.terminationReason = event.terminationReason.orNull
r.updatedAt = Instant.now()
repository.merge(r)
private def applyClockFields(r: GameRecord, event: GameWritebackEventDto): Unit =
r.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
r.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
r.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
r.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
r.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
r.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
r.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
r.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
r.clockActiveColor = event.clockActiveColor.orNull
r.pendingDrawOffer = event.pendingDrawOffer.orNull
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=9
MINOR=10
PATCH=0
+15
View File
@@ -91,3 +91,18 @@
* 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))
## (2026-05-10)
### Features
* **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))
@@ -1,6 +1,7 @@
package de.nowchess.ws.resource
import de.nowchess.ws.config.RedisConfig
import io.micrometer.core.instrument.{Counter, Gauge, MeterRegistry}
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.pubsub.PubSubCommands
import io.quarkus.websockets.next.*
@@ -26,10 +27,27 @@ class GameWebSocketResource:
@Inject
var jwtParser: JWTParser = uninitialized
@Inject
var meterRegistry: MeterRegistry = uninitialized
// scalafix:on DisableSyntax.var
private val connections = new ConcurrentHashMap[String, ConnectionMeta]()
private lazy val connectionsOpened: Counter =
meterRegistry.counter("nowchess.ws.connections.opened")
private lazy val connectionsClosed: Counter =
meterRegistry.counter("nowchess.ws.connections.closed")
private lazy val messagesReceived: Counter =
meterRegistry.counter("nowchess.ws.messages.received")
private lazy val activeGauge: Unit =
Gauge
.builder("nowchess.ws.connections.active", connections, _.size().toDouble)
.register(meterRegistry)
private def s2cTopic(gameId: String): String =
s"${redisConfig.prefix}:game:$gameId:s2c"
@@ -38,22 +56,19 @@ class GameWebSocketResource:
@OnOpen
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)
activeGauge
val gameId = connection.pathParam("gameId")
val playerId = resolvePlayerId(handshake)
log.infof("Game WebSocket opened — gameId=%s playerId=%s", gameId, playerId.getOrElse("anonymous"))
val handler: Consumer[String] = msg => connection.sendText(msg).subscribe().`with`(_ => (), _ => ())
val subscriber = redis.pubsub(classOf[String]).subscribe(s2cTopic(gameId), handler)
connections.put(connection.id(), ConnectionMeta(gameId, subscriber, playerId))
val connectedMsg = playerId match
case Some(pid) => s"""{"type":"CONNECTED","gameId":"$gameId","playerId":"$pid"}"""
case None => s"""{"type":"CONNECTED","gameId":"$gameId"}"""
redis.pubsub(classOf[String]).publish(c2sTopic(gameId), connectedMsg)
connectionsOpened.increment()
publishConnected(gameId, playerId)
@OnTextMessage
def onTextMessage(connection: WebSocketConnection, message: String): Unit =
messagesReceived.increment()
Option(connections.get(connection.id())).foreach { meta =>
val enriched = meta.playerId match
case Some(pid) => injectPlayerId(message, pid)
@@ -66,8 +81,21 @@ class GameWebSocketResource:
Option(connections.remove(connection.id())).foreach { meta =>
log.infof("Game WebSocket closed — gameId=%s", meta.gameId)
meta.subscriber.unsubscribe(s2cTopic(meta.gameId))
connectionsClosed.increment()
}
private def resolvePlayerId(handshake: HandshakeRequest): Option[String] =
Option(handshake.header("Authorization"))
.filter(_.nonEmpty)
.flatMap(token => Try(jwtParser.parse(token)).toOption)
.map(_.getSubject)
private def publishConnected(gameId: String, playerId: Option[String]): Unit =
val connectedMsg = playerId match
case Some(pid) => s"""{"type":"CONNECTED","gameId":"$gameId","playerId":"$pid"}"""
case None => s"""{"type":"CONNECTED","gameId":"$gameId"}"""
redis.pubsub(classOf[String]).publish(c2sTopic(gameId), connectedMsg)
private def injectPlayerId(msg: String, pid: String): String =
val trimmed = msg.trim
if trimmed.endsWith("}") then trimmed.dropRight(1) + s""","playerId":"$pid"}"""
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=8
MINOR=9
PATCH=0