Compare commits

...

8 Commits

Author SHA1 Message Date
Janis 9296db88b7 feat(account): implement token pair handling for login and refresh endpoints
Build & Test (NowChessSystems) TeamCity build failed
2026-05-19 14:41:21 +02:00
Janis ec22a9585e refactor(account): update time control structure and remove deprecated fields 2026-05-19 14:41:20 +02:00
TeamCity 411eed2453 ci: bump version with Build-102 2026-05-19 11:40:58 +00:00
Janis af6b0ed8b7 feat(redis): use ManagedExecutor for asynchronous writeback processing
Build & Test (NowChessSystems) TeamCity build finished
2026-05-19 13:16:07 +02:00
TeamCity bcd8257db2 ci: bump version with Build-101 2026-05-19 09:06:09 +00:00
Janis d61fe97b4c feat(redis): add @Startup annotation to GameWritebackStreamListener
Build & Test (NowChessSystems) TeamCity build finished
2026-05-19 10:44:51 +02:00
TeamCity 959bb53335 ci: bump version with Build-100 2026-05-19 08:13:51 +00:00
Janis b610678005 fix(redis): add log message for starting Writeback listener
Build & Test (NowChessSystems) TeamCity build finished
2026-05-19 09:48:21 +02:00
13 changed files with 230 additions and 42 deletions
+11 -5
View File
@@ -10,11 +10,6 @@ post {
auth: inherit auth: inherit
} }
settings {
encodeUrl: true
timeout: 0
}
headers { headers {
Content-Type: application/json Content-Type: application/json
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
@@ -22,9 +17,20 @@ headers {
body:json { body:json {
{ {
"color": "random",
"timeControl": { "timeControl": {
"limitSeconds": 600, "limitSeconds": 600,
"incrementSeconds": 5 "incrementSeconds": 5
} }
} }
} }
vars:pre-request {
username: bdc
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJub3djaGVzcyIsInN1YiI6ImY3MGQxNDIwLTdhZmEtNGVjMy05Mzg1LWViYWU0N2U3NGY4OCIsInVzZXJuYW1lIjoicGxheWVyMSIsImlhdCI6MTc3OTE5MjkwOCwiZXhwIjoxNzc5MTkzMjA4LCJqdGkiOiJkMDhlZmRhMi01ZjliLTQyNjgtOGM3MC1iNDA2OTViNDhiMTUifQ.q68R2bUdRQ5QwEIfcP0d2g_Wac94qd4K6BzP-PC94x-tDpT3leUY8ZpqY6YzoNv-ywp5sm47-WC539DyUqdzDgPVLLSmOMjRxG-HpaNdXwsXlp8_C7KGlgkK_XSMa3Gq6S4AfUbaXbRhPg5NZz2MRosu0BY2ed0ISvmEfX5XEdBRlKgD14BIKAsEFv3tHtarFS1RGpcSoHV2oeIk_m-VHUC78N-ciNHmBH6mZna_fVHgMocOIrEsTZuUms0Zacmebvh2tAcf36xux1Bm2awJff19zReD-A2o9jucrKcM3Im5BJ6JtuWAsHEae9KLmuol6S2pldvNOmUn3egFUYz0yQ
}
settings {
encodeUrl: true
timeout: 0
}
@@ -25,7 +25,8 @@ import io.quarkus.runtime.annotations.RegisterForReflection
classOf[DeclineReason], classOf[DeclineReason],
classOf[TimeControl], classOf[TimeControl],
classOf[LoginRequest], classOf[LoginRequest],
classOf[TokenResponse], classOf[RefreshRequest],
classOf[TokenPairResponse],
classOf[PlayerInfo], classOf[PlayerInfo],
classOf[PublicAccountDto], classOf[PublicAccountDto],
classOf[BotAccountDto], classOf[BotAccountDto],
@@ -6,7 +6,6 @@ import scala.compiletime.uninitialized
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
import scala.Conversion
@Entity @Entity
@Table(name = "challenges") @Table(name = "challenges")
@@ -34,13 +33,14 @@ class Challenge extends PanacheEntityBase:
@Column(nullable = true, columnDefinition = "varchar(255)") @Column(nullable = true, columnDefinition = "varchar(255)")
var declineReason: DeclineReason = uninitialized var declineReason: DeclineReason = uninitialized
var timeControlType: String = uninitialized @Column(nullable = true)
var limitSeconds: java.lang.Integer = uninitialized
@Column(nullable = true) @Column(nullable = true)
var timeControlLimit: java.lang.Integer = uninitialized var incrementSeconds: java.lang.Integer = uninitialized
@Column(nullable = true) @Column(nullable = true)
var timeControlIncrement: java.lang.Integer = uninitialized var daysPerMove: java.lang.Integer = uninitialized
var createdAt: Instant = uninitialized var createdAt: Instant = uninitialized
@@ -52,5 +52,6 @@ class Challenge extends PanacheEntityBase:
def gameIdOpt: Option[String] = Option(gameId) def gameIdOpt: Option[String] = Option(gameId)
def declineReasonOpt: Option[DeclineReason] = Option(declineReason) def declineReasonOpt: Option[DeclineReason] = Option(declineReason)
def timeControlLimitOpt: Option[Int] = Option(timeControlLimit).map(_.intValue()) def limitSecondsOpt: Option[Int] = Option(limitSeconds).map(_.intValue())
def timeControlIncrementOpt: Option[Int] = Option(timeControlIncrement).map(_.intValue()) def incrementSecondsOpt: Option[Int] = Option(incrementSeconds).map(_.intValue())
def daysPerMoveOpt: Option[Int] = Option(daysPerMove).map(_.intValue())
@@ -4,13 +4,15 @@ case class RegisterRequest(username: String, email: String, password: String)
case class LoginRequest(username: String, password: String) case class LoginRequest(username: String, password: String)
case class TokenResponse(token: String) case class RefreshRequest(refreshToken: String)
case class TokenPairResponse(accessToken: String, refreshToken: String)
case class PlayerInfo(id: String, name: String, rating: Int) case class PlayerInfo(id: String, name: String, rating: Int)
case class PublicAccountDto(id: String, username: String, rating: Int, createdAt: String) case class PublicAccountDto(id: String, username: String, rating: Int, createdAt: String)
case class TimeControlDto(`type`: String, limit: Option[Int], increment: Option[Int]) case class TimeControlDto(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
case class ChallengeRequest(color: String, timeControl: TimeControlDto) case class ChallengeRequest(color: String, timeControl: TimeControlDto)
@@ -4,6 +4,7 @@ enum AccountError:
case UsernameTaken(username: String) case UsernameTaken(username: String)
case EmailAlreadyRegistered(email: String) case EmailAlreadyRegistered(email: String)
case InvalidCredentials case InvalidCredentials
case InvalidRefreshToken
case UserNotFound case UserNotFound
case BotNotFound case BotNotFound
case BotLimitExceeded case BotLimitExceeded
@@ -15,6 +16,7 @@ enum AccountError:
case UsernameTaken(u) => s"Username '$u' is already taken" case UsernameTaken(u) => s"Username '$u' is already taken"
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered" case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
case InvalidCredentials => "Invalid credentials" case InvalidCredentials => "Invalid credentials"
case InvalidRefreshToken => "Invalid or expired refresh token"
case UserNotFound => "User not found" case UserNotFound => "User not found"
case BotNotFound => "Bot account not found" case BotNotFound => "Bot account not found"
case BotLimitExceeded => "Maximum of 5 bot accounts per user exceeded" case BotLimitExceeded => "Maximum of 5 bot accounts per user exceeded"
@@ -40,8 +40,19 @@ class AccountResource:
@Path("/login") @Path("/login")
def login(req: LoginRequest): Response = def login(req: LoginRequest): Response =
accountService.login(req) match accountService.login(req) match
case Right(token) => case Right((accessToken, refreshToken)) =>
Response.ok(TokenResponse(token)).build() Response.ok(TokenPairResponse(accessToken, refreshToken)).build()
case Left(AccountError.UserBanned) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build()
case Left(error) =>
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error.message)).build()
@POST
@Path("/refresh")
def refresh(req: RefreshRequest): Response =
accountService.refresh(req.refreshToken) match
case Right((accessToken, refreshToken)) =>
Response.ok(TokenPairResponse(accessToken, refreshToken)).build()
case Left(AccountError.UserBanned) => case Left(AccountError.UserBanned) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build() Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build()
case Left(error) => case Left(error) =>
@@ -6,6 +6,7 @@ import de.nowchess.account.error.AccountError
import de.nowchess.account.repository.{BotAccountRepository, OfficialBotAccountRepository, UserAccountRepository} import de.nowchess.account.repository.{BotAccountRepository, OfficialBotAccountRepository, UserAccountRepository}
import io.micrometer.core.instrument.MeterRegistry import io.micrometer.core.instrument.MeterRegistry
import io.quarkus.elytron.security.common.BcryptUtil import io.quarkus.elytron.security.common.BcryptUtil
import io.smallrye.jwt.auth.principal.JWTParser
import io.smallrye.jwt.build.Jwt import io.smallrye.jwt.build.Jwt
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
@@ -34,6 +35,9 @@ class AccountService:
@Inject @Inject
var meterRegistry: MeterRegistry = uninitialized var meterRegistry: MeterRegistry = uninitialized
@Inject
var jwtParser: JWTParser = uninitialized
// scalafix:on // scalafix:on
@PostConstruct @PostConstruct
@@ -66,7 +70,7 @@ class AccountService:
log.infof("User %s registered successfully", req.username) log.infof("User %s registered successfully", req.username)
Right(account) Right(account)
def login(req: LoginRequest): Either[AccountError, String] = def login(req: LoginRequest): Either[AccountError, (String, String)] =
val result = authenticateUser(req) val result = authenticateUser(req)
result match result match
case Right(_) => meterRegistry.counter("nowchess.auth.logins", "result", "success").increment() case Right(_) => meterRegistry.counter("nowchess.auth.logins", "result", "success").increment()
@@ -75,7 +79,7 @@ class AccountService:
meterRegistry.counter("nowchess.auth.login.failures", "reason", loginFailureReason(error)).increment() meterRegistry.counter("nowchess.auth.login.failures", "reason", loginFailureReason(error)).increment()
result result
private def authenticateUser(req: LoginRequest): Either[AccountError, String] = private def authenticateUser(req: LoginRequest): Either[AccountError, (String, String)] =
userAccountRepository.findByUsername(req.username) match userAccountRepository.findByUsername(req.username) match
case None => case None =>
log.warnf("Login failed for unknown user %s", req.username) log.warnf("Login failed for unknown user %s", req.username)
@@ -89,16 +93,39 @@ class AccountService:
Left(AccountError.UserBanned) Left(AccountError.UserBanned)
else else
log.infof("User %s logged in successfully", req.username) log.infof("User %s logged in successfully", req.username)
Right( Right((generateAccessToken(account), generateRefreshToken(account.id)))
Jwt
.issuer("nowchess") def refresh(refreshToken: String): Either[AccountError, (String, String)] =
.subject(account.id.toString) try
.claim("username", account.username) val parsed = jwtParser.parse(refreshToken)
.sign(), if parsed.getClaim[String]("type") != "refresh" then Left(AccountError.InvalidRefreshToken)
) else
val userId = UUID.fromString(parsed.getSubject)
userAccountRepository.findById(userId) match
case None => Left(AccountError.UserNotFound)
case Some(u) if u.banned => Left(AccountError.UserBanned)
case Some(u) => Right((generateAccessToken(u), generateRefreshToken(u.id)))
catch case _: Throwable => Left(AccountError.InvalidRefreshToken)
private def generateAccessToken(account: UserAccount): String =
Jwt
.issuer("nowchess")
.subject(account.id.toString)
.claim("username", account.username)
.expiresIn(3600)
.sign()
private def generateRefreshToken(userId: UUID): String =
Jwt
.issuer("nowchess")
.subject(userId.toString)
.claim("type", "refresh")
.expiresIn(30L * 24 * 3600)
.sign()
private def loginFailureReason(error: AccountError): String = error match private def loginFailureReason(error: AccountError): String = error match
case AccountError.InvalidCredentials => "invalid_credentials" case AccountError.InvalidCredentials => "invalid_credentials"
case AccountError.InvalidRefreshToken => "invalid_refresh_token"
case AccountError.UserBanned => "user_banned" case AccountError.UserBanned => "user_banned"
case AccountError.UsernameTaken(_) => "username_taken" case AccountError.UsernameTaken(_) => "username_taken"
case AccountError.EmailAlreadyRegistered(_) => "email_registered" case AccountError.EmailAlreadyRegistered(_) => "email_registered"
@@ -88,9 +88,9 @@ class ChallengeService:
challenge.destUser = destUser challenge.destUser = destUser
challenge.color = color challenge.color = color
challenge.status = ChallengeStatus.Created challenge.status = ChallengeStatus.Created
challenge.timeControlType = req.timeControl.`type` challenge.limitSeconds = req.timeControl.limitSeconds.map(java.lang.Integer.valueOf).orNull
challenge.timeControlLimit = req.timeControl.limit.map(java.lang.Integer.valueOf).orNull challenge.incrementSeconds = req.timeControl.incrementSeconds.map(java.lang.Integer.valueOf).orNull
challenge.timeControlIncrement = req.timeControl.increment.map(java.lang.Integer.valueOf).orNull challenge.daysPerMove = req.timeControl.daysPerMove.map(java.lang.Integer.valueOf).orNull
challenge.createdAt = Instant.now() challenge.createdAt = Instant.now()
challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS) challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS)
challengeRepository.persist(challenge) challengeRepository.persist(challenge)
@@ -200,10 +200,9 @@ class ChallengeService:
if ThreadLocalRandom.current().nextBoolean() then (challenger, destUser) else (destUser, challenger) if ThreadLocalRandom.current().nextBoolean() then (challenger, destUser) else (destUser, challenger)
private def buildTimeControl(challenge: Challenge): Option[CoreTimeControl] = private def buildTimeControl(challenge: Challenge): Option[CoreTimeControl] =
challenge.timeControlType match if challenge.limitSecondsOpt.isEmpty && challenge.incrementSecondsOpt.isEmpty && challenge.daysPerMoveOpt.isEmpty
case "unlimited" => None then None
case "correspondence" => Some(CoreTimeControl(None, None, challenge.timeControlLimitOpt)) else Some(CoreTimeControl(challenge.limitSecondsOpt, challenge.incrementSecondsOpt, challenge.daysPerMoveOpt))
case _ => Some(CoreTimeControl(challenge.timeControlLimitOpt, challenge.timeControlIncrementOpt, None))
private def parseColor(raw: String): Either[ChallengeError, ChallengeColor] = private def parseColor(raw: String): Either[ChallengeError, ChallengeColor] =
raw.toLowerCase match raw.toLowerCase match
@@ -227,7 +226,7 @@ class ChallengeService:
destUser = PlayerInfo(c.destUser.id.toString, c.destUser.username, c.destUser.rating), destUser = PlayerInfo(c.destUser.id.toString, c.destUser.username, c.destUser.rating),
variant = "standard", variant = "standard",
color = c.color.toString.toLowerCase, color = c.color.toString.toLowerCase,
timeControl = TimeControlDto(c.timeControlType, c.timeControlLimitOpt, c.timeControlIncrementOpt), timeControl = TimeControlDto(c.limitSecondsOpt, c.incrementSecondsOpt, c.daysPerMoveOpt),
status = c.status.toString.toLowerCase, status = c.status.toString.toLowerCase,
declineReason = c.declineReasonOpt.map(_.toString.toLowerCase), declineReason = c.declineReasonOpt.map(_.toString.toLowerCase),
gameId = c.gameIdOpt, gameId = c.gameIdOpt,
@@ -32,7 +32,24 @@ class AccountResourceTest:
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.extract() .extract()
.path[String]("token") .path[String]("accessToken")
private def registerAndLoginPair(username: String): (String, String) =
givenRequest()
.body(registerBody(username))
.when()
.post("/api/account")
.`then`()
.statusCode(200)
val resp = givenRequest()
.body(loginBody(username))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.extract()
.response()
(resp.path[String]("accessToken"), resp.path[String]("refreshToken"))
@Test @Test
def registerReturns200(): Unit = def registerReturns200(): Unit =
@@ -57,7 +74,7 @@ class AccountResourceTest:
.body("error", containsString("bob")) .body("error", containsString("bob"))
@Test @Test
def loginReturns200WithToken(): Unit = def loginReturns200WithTokenPair(): Unit =
givenRequest().body(registerBody("charlie")).when().post("/api/account") givenRequest().body(registerBody("charlie")).when().post("/api/account")
givenRequest() givenRequest()
.body(loginBody("charlie")) .body(loginBody("charlie"))
@@ -65,7 +82,8 @@ class AccountResourceTest:
.post("/api/account/login") .post("/api/account/login")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("token", notNullValue()) .body("accessToken", notNullValue())
.body("refreshToken", notNullValue())
@Test @Test
def loginUnauthorizedOnWrongPassword(): Unit = def loginUnauthorizedOnWrongPassword(): Unit =
@@ -105,3 +123,34 @@ class AccountResourceTest:
.get("/api/account/doesnotexist") .get("/api/account/doesnotexist")
.`then`() .`then`()
.statusCode(404) .statusCode(404)
@Test
def refreshReturnsNewTokenPair(): Unit =
val (_, refreshToken) = registerAndLoginPair("refresh_user")
givenRequest()
.body(s"""{"refreshToken":"$refreshToken"}""")
.when()
.post("/api/account/refresh")
.`then`()
.statusCode(200)
.body("accessToken", notNullValue())
.body("refreshToken", notNullValue())
@Test
def refreshWithInvalidTokenReturns401(): Unit =
givenRequest()
.body("""{"refreshToken":"invalid.token.value"}""")
.when()
.post("/api/account/refresh")
.`then`()
.statusCode(401)
@Test
def refreshWithAccessTokenReturns401(): Unit =
val accessToken = registerAndLogin("refresh_bad_type")
givenRequest()
.body(s"""{"refreshToken":"$accessToken"}""")
.when()
.post("/api/account/refresh")
.`then`()
.statusCode(401)
@@ -44,7 +44,7 @@ class ChallengeResourceTest:
.path[String]("token") .path[String]("token")
private val clockBody = private val clockBody =
"""{"color":"random","timeControl":{"type":"clock","limit":300,"increment":5}}""" """{"color":"random","timeControl":{"limitSeconds":300,"incrementSeconds":5}}"""
private def authed(token: String) = private def authed(token: String) =
givenRequest().header("Authorization", s"Bearer $token") givenRequest().header("Authorization", s"Bearer $token")
+81
View File
@@ -281,3 +281,84 @@
* 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)) * 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))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4)) * **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)) * remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
## (2026-05-19)
### 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))
* **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))
* 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))
* 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))
### Bug Fixes
* ensure full hierarchy registration for reflection in NativeReflectionConfig ([ebba729](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ebba729af3265df1619dfbe46fd1945b2a7e30b7))
* 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))
* **redis:** add log message for starting Writeback listener ([b610678](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b610678005de645115f48348e66aa9e6f5deb3d5))
* **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))
## (2026-05-19)
### 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))
* **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))
* 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))
* 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))
* **redis:** add @Startup annotation to GameWritebackStreamListener ([d61fe97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d61fe97b4c8e2db5e34b4a14d995297cc09f9435))
* 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))
### Bug Fixes
* ensure full hierarchy registration for reflection in NativeReflectionConfig ([ebba729](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ebba729af3265df1619dfbe46fd1945b2a7e30b7))
* 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))
* **redis:** add log message for starting Writeback listener ([b610678](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b610678005de645115f48348e66aa9e6f5deb3d5))
* **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))
## (2026-05-19)
### 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))
* **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))
* 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))
* 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))
* **redis:** add @Startup annotation to GameWritebackStreamListener ([d61fe97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d61fe97b4c8e2db5e34b4a14d995297cc09f9435))
* **redis:** use ManagedExecutor for asynchronous writeback processing ([af6b0ed](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/af6b0ed8b73724fcc8f20dfccbe6fe8f84fd792d))
* 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))
### Bug Fixes
* ensure full hierarchy registration for reflection in NativeReflectionConfig ([ebba729](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ebba729af3265df1619dfbe46fd1945b2a7e30b7))
* 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))
* **redis:** add log message for starting Writeback listener ([b610678](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b610678005de645115f48348e66aa9e6f5deb3d5))
* **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))
@@ -4,14 +4,17 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.dto.GameWritebackEventDto import de.nowchess.api.dto.GameWritebackEventDto
import de.nowchess.store.service.GameWritebackService import de.nowchess.store.service.GameWritebackService
import io.quarkus.redis.datasource.RedisDataSource import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.runtime.Startup
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject import jakarta.inject.Inject
import org.eclipse.microprofile.context.ManagedExecutor
import org.jboss.logging.Logger import org.jboss.logging.Logger
import scala.compiletime.uninitialized import scala.compiletime.uninitialized
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
import java.util.function.Consumer import java.util.function.Consumer
@Startup
@ApplicationScoped @ApplicationScoped
class GameWritebackStreamListener: class GameWritebackStreamListener:
@Inject @Inject
@@ -19,6 +22,7 @@ class GameWritebackStreamListener:
var redis: RedisDataSource = uninitialized var redis: RedisDataSource = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized
@Inject var writebackService: GameWritebackService = uninitialized @Inject var writebackService: GameWritebackService = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
// scalafix:on // scalafix:on
private val log = Logger.getLogger(classOf[GameWritebackStreamListener]) private val log = Logger.getLogger(classOf[GameWritebackStreamListener])
@@ -30,9 +34,14 @@ class GameWritebackStreamListener:
case Failure(ex) => case Failure(ex) =>
log.errorf(ex, "Failed to parse game-writeback event: %s", json) log.errorf(ex, "Failed to parse game-writeback event: %s", json)
case Success(event) => case Success(event) =>
Try(writebackService.writeBack(event)) match executor.submit(
case Failure(ex) => new Runnable:
log.errorf(ex, "Failed to write back game event for gameId=%s", event.gameId) def run(): Unit =
case Success(_) => () Try(writebackService.writeBack(event)) match
case Failure(ex) =>
log.errorf(ex, "Failed to write back game event for gameId=%s", event.gameId)
case Success(_) => (),
)
redis.pubsub(classOf[String]).subscribe("game-writeback", handler) redis.pubsub(classOf[String]).subscribe("game-writeback", handler)
log.infof("Started listening to Writebacks")
() ()
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0 MAJOR=0
MINOR=17 MINOR=20
PATCH=0 PATCH=0