Compare commits

..

16 Commits

Author SHA1 Message Date
Lala, Shahd a46cff5342 fix: Merge branch 'main' of git.janis-eccarius.de:NowChess/NowChessSystems into feat/NCS-82
Build & Test (NowChessSystems) TeamCity build failed
2026-06-04 13:46:59 +00:00
TeamCity 4c5302d525 ci: bump version with Build-111
Build & Test (NowChessSystems) TeamCity build failed
2026-06-04 11:14:24 +00:00
shosho996 a12f979023 fix(official-bots): NCS-70-auto-register official bots with account service (#59)
Co-authored-by: Janis <janis-e@gmx.de>
Reviewed-on: #59
2026-06-04 11:14:24 +00:00
TeamCity d0c09c6039 ci: bump version with Build-110 2026-06-04 11:14:24 +00:00
Janis e4011e7337 fix(store): cap game-writeback stream with MAXLEN trimming (#58)
Add approximate MAXLEN ~1000 to all xadd calls in
GameWritebackStreamListener. Without trimming, ACKed messages
accumulate in the stream indefinitely, wasting Redis memory.

Closes NCS-88
https://knockoutwhist.youtrack.cloud/issue/NCS-88

---------

Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com>
Reviewed-on: #58
2026-06-04 11:14:24 +00:00
Janis 554b56ff1d docs: add automated workflows for defect fixing, user story creation, and issue estimation 2026-06-04 11:14:24 +00:00
TeamCity 38992bfa41 ci: bump version with Build-108 2026-06-04 11:14:24 +00:00
Janis 5949b4cb60 fix(pgn): add SAN disambiguation and check/checkmate suffixes [NCS-42] (#56)
Two bugs in move notation causing PGN import failures in LiChess:

1. Disambiguation: when two pieces of same type can reach same square,
   SAN requires file/rank/full-square prefix (e.g. "Ndf3" not "Nf3").
   Added disambiguate() in PgnExporter and disambiguatePiece() in
   GameEngine, both querying allLegalMoves to find competing pieces.

2. Check/checkmate suffix: "+" and "#" were never appended.
   PgnExporter now threads ctxAfter through moveToAlgebraic and
   calls DefaultRules.isCheck/isCheckmate. GameEngine passes
   PostMoveStatus to translateMoveToNotation for the same result.

Also removes dead notation code in executeMoveBody (result was never
used — not passed to MoveExecutedEvent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com>
Reviewed-on: #56
2026-06-04 11:14:24 +00:00
TeamCity 000afb235a ci: bump version with Build-107 2026-06-04 11:14:24 +00:00
Janis Eccarius ad2cb7431c refactor: improve code formatting and readability 2026-06-04 11:14:24 +00:00
Janis Eccarius 4b2f3a256e fix(store): replace null check with Option for stream messages 2026-06-04 11:14:24 +00:00
Janis Eccarius 65ddf55575 ci(native-image): add x86_v2 arch to build matrix
Adds x86-64-v2 microarchitecture as a second build target alongside
default. Images tagged with -x86_v2 suffix; GHA cache scoped per arch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 11:14:24 +00:00
LQ63 8255731d50 feat(tournament): wire official bots into tournaments via JWT and Redis
Build & Test (NowChessSystems) TeamCity build failed
- Add token field to OfficialBotAccount and generate a non-expiring bot
  JWT on creation so official bots can authenticate
- Expose token in POST /api/account/official-bots response and open the
  endpoint to any authenticated user
- Bridge TournamentService to Redis: publish gameStart events to
  nowchess:bot:<name>:events after each pairing so OfficialBotService
  picks up games automatically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 00:06:06 +02:00
LQ63 acd8a26685 fix(ws): revert incompatible handshake.query usage
handshake.query(String) does not exist in the current Quarkus WebSocket API version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 00:05:48 +02:00
Lala, Shahd fd6698c7a7 fix: bot name and id return, websocket
Build & Test (NowChessSystems) TeamCity build failed
2026-05-31 21:28:04 +00:00
LQ63 5c586b9003 feat(tournament): add Swiss-system tournament module
Build & Test (NowChessSystems) TeamCity build failed
Implements the full tournament lifecycle: create, join, withdraw, start,
round progression, and finish with Buchholz tiebreak standings.

- REST resource covering all 11 endpoints from the OpenAPI spec
- Swiss pairing algorithm with bye support
- Per-bot NDJSON stream with targeted gameStart events (color field)
- Game result ingestion via Redis writeback stream
- H2-backed integration tests for resource and pairing service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 00:46:56 +02:00
78 changed files with 252 additions and 2809 deletions
-88
View File
@@ -1,88 +0,0 @@
# Create Defect in YouTrack
Automated defect creation workflow. Topic/hint: `$ARGUMENTS`
## Step 1 — Gather Context
Use `AskUserQuestion` tool to ask the user (max 4 questions at once):
1. **Component** — Where does the bug occur? (e.g. move generation, FEN parsing, castling, UI, API)
2. **What breaks** — What is the actual (broken) behavior?
3. **Expected** — What should happen instead?
4. **Reproducibility** — Is it always reproducible? Any known trigger conditions?
If `$ARGUMENTS` already answers some of these, skip those questions.
## Step 2 — Research (if needed)
If the bug involves domain logic or rules:
- Search repo for relevant code (`Grep`/`Bash`).
- Check test files for existing coverage of the broken area.
- Do NOT guess at root cause. Surface findings before drafting.
## Step 3 — Draft Defect
Compose the full defect report using this template:
```
Summary
[One-sentence description of what is broken.]
Steps to Reproduce
1. Step one
2. Step two
3. Step three
Expected Behavior
[What should happen.]
Actual Behavior
[What actually happens.]
Environment / Notes
[Any relevant context: FEN positions, game conditions, config, browser, OS — only if applicable.]
```
Rules:
- Steps must be minimal and reproducible.
- Expected vs actual: concrete and unambiguous.
- Omit "Environment / Notes" section if not relevant.
## Step 4 — Clarify
Show the draft to the user.
**Use `AskUserQuestion` tool to ask:**
- Are steps to reproduce complete and accurate?
- Severity: Blocker / Critical / Major / Minor / Trivial?
- Any related tickets or recent changes to link?
Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project
- Frontend / UI / UX → project: `NCWF`
- Backend / coordinator / systems / bot / engine → project: `NCS`
If ambiguous, ask the user.
## Step 6 — Create Issue
Call `mcp__youtrack__create_issue` with:
- `project`: determined in Step 5
- `summary`: concise title describing what is broken (≤72 chars, sentence case)
- `description`: full formatted defect report from Step 3 (Markdown)
- `type`: `Bug`
## Step 7 — Report
Display the created issue ID and URL.
Ask if a linked investigation or fix task is needed.
+2 -29
View File
@@ -66,17 +66,10 @@ Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project
> **Project routing rules (always apply these):**
> - Backend code (game engine, bots, API, services, coordinator) → `NCS`
> - Frontend code (UI, UX, web app) → `NCWF`
> - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI`
> - If ambiguous, ask the user.
- Frontend / UI / UX → project: `NCWF`
- Backend / coordinator / systems / bot / engine → project: `NCS`
- Kubernetes, pipelines, CI/CD, DB setup, infrastructure → project: `NCI`
If still ambiguous, ask the user.
If ambiguous, ask the user.
## Step 6 — Create Issue
@@ -86,27 +79,7 @@ Call `mcp__youtrack__create_issue` with:
- `description`: full formatted story from Step 3 (Markdown)
- `type`: `Feature` (or `Task` if purely technical with no user-facing value)
## Step 7 — Link Issues
After creation, **automatically** ask the user (use `AskUserQuestion` if interactive, otherwise infer from context):
> Are there related issues to link? (skip if none)
Collect any issue IDs the user mentions. For each, determine the correct relation and call `mcp__youtrack__link_issues`:
| Situation | Relation to use |
|-----------|----------------|
| This story must be done before another | `blocks` |
| Another story must be done before this | `is blocked by` |
| Stories share domain or are related | `relates to` |
| This is a child of an epic/story | `subtask of` |
| This is a parent grouping subtasks | `parent for` |
| This depends on another ticket's output | `depends on` |
If the user mentions an issue in the story description or implementation notes (e.g. "see NCS-42", "after NCS-12 is done"), auto-detect and suggest linking it — confirm before creating the link.
## Step 8 — Report
## Step 7 — Report
Display the created issue ID and URL.
List any links created (relation type + linked issue ID).
Ask if a linked sub-task or implementation ticket is needed.
+5 -41
View File
@@ -39,16 +39,12 @@ After root cause confirmed, assess scope:
To create subtasks:
1. Break fix into discrete, independently-completable tasks (e.g. "Fix validation in RuleSet", "Add regression test for castling edge case", "Update FenParser to handle X").
2. For each subtask call `mcp__youtrack__create_issue` with:
- `project`: based on subtask content — do **not** inherit from parent. Backend code → `NCS`; frontend/UI → `NCWF`; Kubernetes/pipelines/CI-CD/DB setup/infrastructure → `NCI`. If ambiguous, ask user.
- `project`: same project as parent ticket
- `summary`: concise action-oriented title
- `type`: `Task`
- `description`: what to do and why
3. Call `mcp__youtrack__link_issues` to link each subtask to `$ARGUMENTS` with relation `subtask of`.
4. Check if the ticket description or comments mention other issue IDs. For each mentioned ID, suggest a link and confirm with user:
- Fix depends on another fix finishing first → `is blocked by`
- This fix blocks another ticket → `blocks`
- Logically related but independent → `relates to`
5. List created subtask IDs and any additional links to user.
4. List created subtask IDs to user.
Then proceed to Step 4, implementing subtasks in order.
@@ -57,10 +53,7 @@ Then proceed to Step 4, implementing subtasks in order.
1. Implement fix (use `scala-implementer` agent for non-trivial changes; inline edits for small ones).
2. Run `./compile` — must be green.
3. Run `./test` — must be green.
4. Run `./gradlew spotlessScalaApply`**blocking, foreground only**. Wait for completion before continuing.
5. Run `./lint`**blocking, foreground only** (never `run_in_background`). Wait for exit code 0. Must be green.
- If lint fails, fix all issues and re-run until exit code 0.
- **Do NOT proceed to Step 5 until `./lint` has completed and returned exit code 0.**
4. Run `./lint` — must be green.
If any step fails, iterate until all pass.
## Step 5 — Review
@@ -68,19 +61,6 @@ If any step fails, iterate until all pass.
Spawn `cavecrew-reviewer` on the full diff.
Display findings grouped by severity.
## Step 5b — Apply Review Findings
If the review produced any findings (any severity):
1. Implement all agreed fixes.
2. Run `./compile` — must be green.
3. Run `./test` — must be green.
4. Run `./gradlew spotlessScalaApply`**blocking, foreground only**. Wait for completion.
5. Run `./lint`**blocking, foreground only**. Wait for exit code 0.
- If lint fails, fix all issues and re-run until exit code 0.
6. Re-spawn `cavecrew-reviewer` on the updated diff to confirm all findings are resolved.
Repeat until review is clean or user explicitly accepts remaining findings.
## Step 6 — Confirm + Push
Show summary: ticket, branch, files changed, review findings.
@@ -118,23 +98,7 @@ Files changed:
- <file2>
```
## Step 7b — Additional Links
After commenting, ask the user if `$ARGUMENTS` should be linked to any other issues not already linked:
| Situation | Relation |
|-----------|---------|
| This fix blocks another open ticket | `blocks` |
| Another ticket must ship first | `is blocked by` |
| Related defect or story | `relates to` |
| Duplicate of another defect | `duplicates` |
Scan the ticket description and comments for any issue IDs that were mentioned but not yet linked. Suggest those automatically.
Call `mcp__youtrack__link_issues` for each confirmed link.
## Step 8 — Cleanup
Call `ExitWorktree` with `discard_changes: true` to delete the worktree.
(Branch was pushed in step 6 — commits are safe on remote; `discard_changes: true` bypasses the local-ahead guard.)
Report: branch pushed, ticket commented, links created, worktree deleted, done.
Call `ExitWorktree` to delete the worktree.
Report: branch pushed, ticket commented, worktree deleted, done.
+2 -19
View File
@@ -33,10 +33,7 @@ All subsequent file work happens inside this worktree.
1. Implement feature (use `scala-implementer` agent for non-trivial changes; inline edits for small ones).
2. Run `./compile` — must be green.
3. Run `./test` — must be green (add new tests for new behaviour; do not modify existing tests unless requirements changed).
4. Run `./gradlew spotlessScalaApply`**blocking, foreground only**. Wait for completion before continuing.
5. Run `./lint`**blocking, foreground only** (never `run_in_background`). Wait for exit code 0. Must be green.
- If lint fails, fix all issues and re-run until exit code 0.
- **Do NOT proceed to Step 5 until `./lint` has completed and returned exit code 0.**
4. Run `./lint` — must be green.
If any step fails, iterate until all pass.
## Step 5 — Review
@@ -44,19 +41,6 @@ If any step fails, iterate until all pass.
Spawn `cavecrew-reviewer` on the full diff.
Display findings grouped by severity.
## Step 5b — Apply Review Findings
If the review produced any findings (any severity):
1. Implement all agreed fixes.
2. Run `./compile` — must be green.
3. Run `./test` — must be green.
4. Run `./gradlew spotlessScalaApply`**blocking, foreground only**. Wait for completion.
5. Run `./lint`**blocking, foreground only**. Wait for exit code 0.
- If lint fails, fix all issues and re-run until exit code 0.
6. Re-spawn `cavecrew-reviewer` on the updated diff to confirm all findings are resolved.
Repeat until review is clean or user explicitly accepts remaining findings.
## Step 6 — Confirm + Push
Show summary: ticket, branch, files changed, review findings.
@@ -96,6 +80,5 @@ Files changed:
## Step 8 — Cleanup
Call `ExitWorktree` with `discard_changes: true` to delete the worktree.
(Branch was pushed in step 6 — commits are safe on remote; `discard_changes: true` bypasses the local-ahead guard.)
Call `ExitWorktree` to delete the worktree.
Report: branch pushed, ticket commented, worktree deleted, done.
+4 -31
View File
@@ -59,15 +59,11 @@ Rules:
- Definition of Done: adjust per subtask — not all subtasks need the same criteria (e.g. a research spike has different DoD than an implementation task).
- Keep description short — one paragraph max.
## Step 5 — Determine Project per Subtask
## Step 5 — Determine Project
Assign each subtask's project based on its content — do **not** inherit blindly from parent:
- Backend code (game engine, bots, API, services, coordinator) → `NCS`
- Frontend code (UI, UX, web app) → `NCWF`
- Kubernetes, pipelines, CI/CD, DB setup, infrastructure → `NCI`
If a subtask's project is ambiguous, ask the user before creating it.
Inherit project from parent story (`$ARGUMENTS`):
- If parent is `NCWF-*` → subtasks go in `NCWF`
- If parent is `NCS-*` → subtasks go in `NCS`
## Step 6 — Create Subtasks
@@ -79,31 +75,8 @@ For each subtask call `mcp__youtrack__create_issue` with:
Then call `mcp__youtrack__link_issues` to link each created subtask to `$ARGUMENTS` with relation `subtask of`.
## Step 6b — Inter-Subtask Links
If subtasks must be done in sequence (one depends on output of another), add ordering links:
- For each dependency pair call `mcp__youtrack__link_issues` with relation `is blocked by` (subtask B is blocked by subtask A).
Ask the user to confirm sequencing before adding these links:
> Do any subtasks have ordering dependencies? (e.g. "Implement X must come before Add tests for X")
## Step 6c — External Links
Scan `$ARGUMENTS` description and implementation notes for any referenced issue IDs not already linked. For each:
| Situation | Relation |
|-----------|---------|
| Parent story blocks another epic/story | `blocks` |
| Story depends on another epic completing | `is blocked by` |
| Related story in same domain | `relates to` |
| This story duplicates or supersedes | `duplicates` |
Suggest links to the user and call `mcp__youtrack__link_issues` on confirmation.
## Step 7 — Report
List all created subtask IDs and summaries.
List all links created (subtask-of, blocking chains, external).
Display parent story link.
Ask if any subtask needs further splitting.
-1
View File
@@ -56,7 +56,6 @@ jobs:
- official-bots
- rule
- store
- tournament
- ws
arch:
- name: default
+1 -6
View File
@@ -48,13 +48,8 @@ graphify-out/
.DS_Store
/jacoco-reporter/.venv/
/.claude/settings.local.json
/.claude/worktrees/
modules/tournament/src/main/resources/keys/dev-public.pem
modules/account/src/main/resources/keys/dev-private.pem
modules/account/src/main/resources/keys/dev-public.pem
modules/core/src/main/resources/keys/dev-public.pem
*.hprof
### Embedded repos (not submodules) ###
GitOps/
frontend/
java_pid2736.hprof
-8
View File
@@ -57,14 +57,6 @@ Use consistently.
- **Tests are the spec.** Don't modify to pass. Fix requirements/code. Update only if requirements change.
- Never read build folders. Ask permission if needed.
- Keep file current with decisions + conventions.
- **NativeReflectionConfig (mandatory):** Every new type (class, case class, enum, sealed trait — anything serialized) must be registered in the `NativeReflectionConfig` of **every module that interacts with it**. Configs live at:
- `modules/account/src/main/scala/de/nowchess/account/config/NativeReflectionConfig.scala`
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/NativeReflectionConfig.scala`
- `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala`
- `modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala`
- `modules/rule/src/main/scala/de/nowchess/rules/config/NativeReflectionConfig.scala`
- `modules/store/src/main/scala/de/nowchess/store/config/NativeReflectionConfig.scala`
- `modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala`
---
-187
View File
@@ -382,190 +382,3 @@
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-05)
### Features
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **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))
* 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
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* 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
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
### Bug Fixes
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
### Bug Fixes
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-10)
### Features
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
### Bug Fixes
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
-1
View File
@@ -45,7 +45,6 @@ dependencies {
}
}
implementation(project(":modules:api"))
implementation(project(":modules:security"))
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
@@ -0,0 +1,28 @@
package de.nowchess.account.client
import de.nowchess.security.{InternalClientHeadersFactory, InternalSecretClientFilter}
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, RegisterProvider}
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
case class CorePlayerInfo(id: String, displayName: String)
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
case class CoreCreateGameRequest(
white: Option[CorePlayerInfo],
black: Option[CorePlayerInfo],
timeControl: Option[CoreTimeControl],
mode: Option[String],
)
case class CoreGameResponse(gameId: String)
@Path("/api/board/game")
@RegisterRestClient(configKey = "core-service")
@RegisterProvider(classOf[InternalSecretClientFilter])
@RegisterClientHeaders(classOf[InternalClientHeadersFactory])
trait CoreGameClient:
@POST
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def createGame(req: CoreCreateGameRequest): CoreGameResponse
@@ -1,10 +0,0 @@
package de.nowchess.account.client
case class CorePlayerInfo(id: String, displayName: String)
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
case class CoreCreateGameRequest(
white: Option[CorePlayerInfo],
black: Option[CorePlayerInfo],
timeControl: Option[CoreTimeControl],
mode: Option[String],
)
@@ -1,133 +0,0 @@
package de.nowchess.account.client
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.account.config.RedisConfig
import de.nowchess.api.dto.{GameCreationRequestDto, GameCreationResponseDto, PlayerInfoDto, TimeControlDto}
import de.nowchess.api.game.GameMode
import de.nowchess.api.player.PlayerType
import de.nowchess.api.event.{EventEnvelope, EventType}
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{StreamMessage, XAddArgs, XGroupCreateArgs, XReadGroupArgs}
import io.quarkus.runtime.StartupEvent
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Observes
import jakarta.inject.Inject
import org.eclipse.microprofile.config.inject.ConfigProperty
import org.eclipse.microprofile.context.ManagedExecutor
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.time.Duration
import java.util.UUID
import java.util.concurrent.{CompletableFuture, ConcurrentHashMap, TimeUnit}
@ApplicationScoped
class GameCreationStreamClient:
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@ConfigProperty(name = "nowchess.game-creation-stream.enabled", defaultValue = "true")
private var streamEnabled: Boolean = true
// scalafix:on DisableSyntax.var
private val log = Logger.getLogger(classOf[GameCreationStreamClient])
private val instanceId = UUID.randomUUID().toString
private val groupName = "account-game-creation"
private val consumerId = instanceId
private val maxStreamLen = 1000L
private val timeout = Duration.ofSeconds(10)
private val pending = new ConcurrentHashMap[String, CompletableFuture[GameCreationResponseDto]]()
private def requestStream: String = s"${redisConfig.prefix}:game-creation"
private def responseStream: String = s"${redisConfig.prefix}:game-creation-response"
def start(@Observes _ev: StartupEvent): Unit =
if streamEnabled then
createGroupIfAbsent()
executor.submit(
new Runnable:
def run(): Unit = pollLoop(),
)
log.infof("Game-creation response listener started (consumer=%s)", consumerId)
def createGame(req: CoreCreateGameRequest): GameCreationResponseDto =
val correlationId = UUID.randomUUID().toString
val future = new CompletableFuture[GameCreationResponseDto]()
pending.put(correlationId, future)
Try {
val payload = objectMapper.valueToTree[com.fasterxml.jackson.databind.JsonNode](toDto(req))
val envelope = EventEnvelope.of(EventType.GameCreationRequest, payload, Some(correlationId))
publish(requestStream, envelope)
future.get(timeout.toMillis, TimeUnit.MILLISECONDS)
} match
case Success(resp) =>
pending.remove(correlationId)
resp
case Failure(ex) =>
pending.remove(correlationId)
log.errorf(ex, "Game creation request %s failed", correlationId)
GameCreationResponseDto(None, Some("Game creation request timed out or failed"))
private def toDto(req: CoreCreateGameRequest): GameCreationRequestDto =
GameCreationRequestDto(
white = req.white.map(p => PlayerInfoDto(p.id, p.displayName, PlayerType.Human)),
black = req.black.map(p => PlayerInfoDto(p.id, p.displayName, PlayerType.Human)),
timeControl = req.timeControl.map(t => TimeControlDto(t.limitSeconds, t.incrementSeconds, t.daysPerMove)),
mode = req.mode.map(_ => GameMode.Authenticated),
)
private def createGroupIfAbsent(): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(responseStream, groupName, "0", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create response consumer group")
case Success(_) => ()
private def pollLoop(): Unit =
while true do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName,
consumerId,
responseStream,
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach(handleResponse))
} match
case Failure(ex) => log.warnf(ex, "Error in game-creation response poll loop")
case Success(_) => ()
private def handleResponse(msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data")
Try(objectMapper.readValue(json, classOf[EventEnvelope])) match
case Success(envelope) =>
envelope.correlationId.flatMap(id => Option(pending.remove(id))).foreach { future =>
Try(objectMapper.treeToValue(envelope.payload, classOf[GameCreationResponseDto])) match
case Success(resp) => future.complete(resp)
case Failure(ex) => future.completeExceptionally(ex)
}
case Failure(ex) => log.warnf(ex, "Unparseable game-creation response: %s", json)
ack(msg.id())
private def ack(id: String): Unit =
Try(redis.stream(classOf[String]).xack(responseStream, groupName, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack response %s", id)
case Success(_) => ()
private def publish(key: String, envelope: EventEnvelope): Unit =
val json = objectMapper.writeValueAsString(envelope)
redis
.stream(classOf[String])
.xadd(key, new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(), Map("data" -> json).asJava)
()
@@ -1,6 +1,6 @@
package de.nowchess.account.config
import de.nowchess.account.client.{CoreCreateGameRequest, CorePlayerInfo, CoreTimeControl}
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
import de.nowchess.account.domain.{
BotAccount,
Challenge,
@@ -12,19 +12,10 @@ import de.nowchess.account.domain.{
UserAccount,
}
import de.nowchess.account.dto.*
import de.nowchess.api.dto.{
GameCreationRequestDto,
GameCreationResponseDto,
PlayerInfoDto as ApiPlayerInfoDto,
TimeControlDto as ApiTimeControlDto,
}
import de.nowchess.api.event.{EventEnvelope, EventType}
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[EventEnvelope],
classOf[EventType],
classOf[UserAccount],
classOf[BotAccount],
classOf[OfficialBotAccount],
@@ -53,11 +44,8 @@ import io.quarkus.runtime.annotations.RegisterForReflection
classOf[CorePlayerInfo],
classOf[CoreTimeControl],
classOf[CoreCreateGameRequest],
classOf[CoreGameResponse],
classOf[OfficialChallengeResponse],
classOf[GameCreationRequestDto],
classOf[GameCreationResponseDto],
classOf[ApiPlayerInfoDto],
classOf[ApiTimeControlDto],
),
)
class NativeReflectionConfig
@@ -195,7 +195,7 @@ class AccountResource:
@POST
@Path("/official-bots")
@RolesAllowed(Array("Admin"))
@RolesAllowed(Array("**"))
def createOfficialBot(req: CreateBotAccountRequest): Response =
accountService.createOfficialBotAccount(req.name) match
case Right(bot) =>
@@ -1,6 +1,6 @@
package de.nowchess.account.resource
import de.nowchess.account.client.{CoreCreateGameRequest, CorePlayerInfo, GameCreationStreamClient}
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo}
import de.nowchess.account.dto.{ErrorDto, OfficialChallengeResponse}
import de.nowchess.account.service.{AccountService, EventPublisher}
import jakarta.annotation.security.RolesAllowed
@@ -9,6 +9,7 @@ import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.jwt.JsonWebToken
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
@@ -28,7 +29,8 @@ class OfficialChallengeResource:
@Inject var botEventPublisher: EventPublisher = uninitialized
@Inject
var gameCreationClient: GameCreationStreamClient = uninitialized
@RestClient
var coreGameClient: CoreGameClient = uninitialized
// scalafix:on
private val log = Logger.getLogger(classOf[OfficialChallengeResource])
@@ -70,7 +72,7 @@ class OfficialChallengeResource:
(CorePlayerInfo(bot.id.toString, bot.name), CorePlayerInfo(user.id.toString, user.username), "white")
val req = CoreCreateGameRequest(Some(white), Some(black), None, Some("Authenticated"))
val gameId =
try gameCreationClient.createGame(req).gameId.toRight("Failed to create game")
try Right(coreGameClient.createGame(req).gameId)
catch case _ => Left("Failed to create game")
gameId match
case Left(err) =>
@@ -217,8 +217,6 @@ class AccountService:
bot.name = name
bot.createdAt = Instant.now()
officialBotAccountRepository.persist(bot)
bot.token = generateBotToken(bot.id, bot.name)
officialBotAccountRepository.persist(bot)
log.infof("Auto-registered official bot: %s", name)
}
@@ -1,6 +1,12 @@
package de.nowchess.account.service
import de.nowchess.account.client.{CoreCreateGameRequest, CorePlayerInfo, CoreTimeControl, GameCreationStreamClient}
import de.nowchess.account.client.{
CoreCreateGameRequest,
CoreGameClient,
CoreGameResponse,
CorePlayerInfo,
CoreTimeControl,
}
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
import de.nowchess.account.dto.{
ChallengeDto,
@@ -17,6 +23,7 @@ import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.transaction.Transactional
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
@@ -38,7 +45,8 @@ class ChallengeService:
var challengeRepository: ChallengeRepository = uninitialized
@Inject
var gameCreationClient: GameCreationStreamClient = uninitialized
@RestClient
var coreGameClient: CoreGameClient = uninitialized
@Inject
var eventPublisher: EventPublisher = uninitialized
@@ -179,7 +187,7 @@ class ChallengeService:
val (white, black) = assignColors(challenge)
val tc = buildTimeControl(challenge)
val req = CoreCreateGameRequest(Some(white), Some(black), tc, Some("Authenticated"))
gameCreationClient.createGame(req).gameId.toRight(ChallengeError.GameCreationFailed)
Right(coreGameClient.createGame(req).gameId)
catch case _ => Left(ChallengeError.GameCreationFailed)
private def assignColors(challenge: Challenge): (CorePlayerInfo, CorePlayerInfo) =
@@ -1,67 +1,31 @@
package de.nowchess.account.service
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.account.config.RedisConfig
import de.nowchess.api.event.{EventEnvelope, EventType}
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.XAddArgs
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
@ApplicationScoped
class EventPublisher:
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on DisableSyntax.var
private val maxStreamLen = 1000L
def publishGameStart(botId: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
val payload = objectMapper.createObjectNode()
payload.put("gameId", gameId)
payload.put("playingAs", playingAs)
payload.put("difficulty", difficulty)
payload.put("botAccountId", botAccountId)
val envelope = EventEnvelope.of(EventType.BotGameStart, payload)
val json = objectMapper.writeValueAsString(envelope)
redis
.stream(classOf[String])
.xadd(
s"${redisConfig.prefix}:bot:$botId:events:stream",
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
Map("data" -> json).asJava,
)
val event =
s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":$difficulty,"botAccountId":"$botAccountId"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event)
()
def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit =
val payload = objectMapper.createObjectNode()
payload.put("challengeId", challengeId)
payload.put("challengerName", challengerName)
publishToUserStream(destUserId, EventType.ChallengeCreated, payload)
val event = s"""{"type":"challengeCreated","challengeId":"$challengeId","challengerName":"$challengerName"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$destUserId:events", event)
()
def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit =
val payload = objectMapper.createObjectNode()
payload.put("challengeId", challengeId)
payload.put("gameId", gameId)
publishToUserStream(challengerId, EventType.ChallengeAccepted, payload)
private def publishToUserStream(
userId: String,
eventType: EventType,
payload: com.fasterxml.jackson.databind.node.ObjectNode,
): Unit =
val envelope = EventEnvelope.of(eventType, payload)
val json = objectMapper.writeValueAsString(envelope)
redis
.stream(classOf[String])
.xadd(
s"${redisConfig.prefix}:user:$userId:events:stream",
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
Map("data" -> json).asJava,
)
val event = s"""{"type":"challengeAccepted","challengeId":"$challengeId","gameId":"$gameId"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$challengerId:events", event)
()
@@ -34,5 +34,3 @@ nowchess:
secret: test-secret
auth:
enabled: false
game-creation-stream:
enabled: false
@@ -1,11 +1,11 @@
package de.nowchess.account.resource
import de.nowchess.account.client.GameCreationStreamClient
import de.nowchess.api.dto.GameCreationResponseDto
import de.nowchess.account.client.{CoreGameClient, CoreGameResponse}
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.{BeforeEach, Test}
import org.mockito.{ArgumentMatchers, Mockito}
@@ -14,15 +14,14 @@ import org.mockito.{ArgumentMatchers, Mockito}
class ChallengeResourceTest:
@InjectMock
@RestClient
// scalafix:off DisableSyntax.var
var gameCreationClient: GameCreationStreamClient = scala.compiletime.uninitialized
var coreGameClient: CoreGameClient = scala.compiletime.uninitialized
// scalafix:on
@BeforeEach
def setup(): Unit =
Mockito
.when(gameCreationClient.createGame(ArgumentMatchers.any()))
.thenReturn(GameCreationResponseDto(Some("test-game-id")))
Mockito.when(coreGameClient.createGame(ArgumentMatchers.any())).thenReturn(CoreGameResponse("test-game-id"))
private def givenRequest() = RestAssured.`given`().contentType(ContentType.JSON)
@@ -1,56 +0,0 @@
package de.nowchess.account.service
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import de.nowchess.account.config.RedisConfig
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{StreamCommands, XAddArgs}
import org.junit.jupiter.api.{BeforeEach, Test}
import org.mockito.ArgumentMatchers.*
import org.mockito.Mockito.*
import scala.compiletime.uninitialized
class EventPublisherTest:
// scalafix:off DisableSyntax.var
private var redis: RedisDataSource = uninitialized
private var streamCmds: StreamCommands[String, String, Nothing] = uninitialized
private var redisConfig: RedisConfig = uninitialized
// scalafix:on DisableSyntax.var
private val objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
@BeforeEach
def setup(): Unit =
redis = mock(classOf[RedisDataSource])
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
redisConfig = mock(classOf[RedisConfig])
when(redis.stream(classOf[String])).thenReturn(streamCmds)
when(redisConfig.prefix).thenReturn("nowchess")
private def publisher: EventPublisher =
val p = new EventPublisher
p.redis = redis
p.redisConfig = redisConfig
p.objectMapper = objectMapper
p
@Test
def publishChallengeCreatedWritesToUserStream(): Unit =
publisher.publishChallengeCreated("user1", "ch1", "Alice")
verify(streamCmds).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:user:user1:events:stream"),
any(classOf[XAddArgs]),
any(),
)
verify(redis, never()).pubsub(any(classOf[Class[?]]))
@Test
def publishChallengeAcceptedWritesToUserStream(): Unit =
publisher.publishChallengeAccepted("user2", "ch1", "game42")
verify(streamCmds).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:user:user2:events:stream"),
any(classOf[XAddArgs]),
any(),
)
verify(redis, never()).pubsub(any(classOf[Class[?]]))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=24
MINOR=19
PATCH=0
-60
View File
@@ -145,63 +145,3 @@
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
## (2026-06-05)
### Features
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **dto:** update GameWritebackEventDto for JSON deserialization and remove unused mixin ([576e3fe](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/576e3fea9bf1082549ea53efd3288474c42be93d))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
## (2026-06-09)
### Features
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **dto:** update GameWritebackEventDto for JSON deserialization and remove unused mixin ([576e3fe](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/576e3fea9bf1082549ea53efd3288474c42be93d))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
## (2026-06-09)
### Features
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
* **dto:** update GameWritebackEventDto for JSON deserialization and remove unused mixin ([576e3fe](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/576e3fea9bf1082549ea53efd3288474c42be93d))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **dependencies:** correct Jackson databind dependency group ID ([008d72d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/008d72d826707c04205bac7de25170fae5fed861))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
-2
View File
@@ -50,8 +50,6 @@ dependencies {
}
}
implementation("com.fasterxml.jackson.core:jackson-databind:${versions["JACKSON"]!!}")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -1,10 +0,0 @@
package de.nowchess.api.dto
import de.nowchess.api.game.GameMode
final case class GameCreationRequestDto(
white: Option[PlayerInfoDto],
black: Option[PlayerInfoDto],
timeControl: Option[TimeControlDto],
mode: Option[GameMode] = None,
)
@@ -1,6 +0,0 @@
package de.nowchess.api.dto
final case class GameCreationResponseDto(
gameId: Option[String],
error: Option[String] = None,
)
@@ -1,27 +0,0 @@
package de.nowchess.api.event
import com.fasterxml.jackson.databind.JsonNode
import java.time.Instant
import java.util.UUID
final case class EventEnvelope(
eventId: UUID,
`type`: EventType,
payload: JsonNode,
timestamp: Instant,
correlationId: Option[String],
)
object EventEnvelope:
def of(
`type`: EventType,
payload: JsonNode,
correlationId: Option[String] = None,
): EventEnvelope =
EventEnvelope(
eventId = UUID.randomUUID(),
`type` = `type`,
payload = payload,
timestamp = Instant.now(),
correlationId = correlationId,
)
@@ -1,4 +0,0 @@
package de.nowchess.api.event
enum EventType:
case GameStart, GameCreationRequest, GameCreationResponse, BotGameStart, ChallengeCreated, ChallengeAccepted, GameOver
@@ -1,9 +0,0 @@
package de.nowchess.api.event
final case class GameOverPayload(
gameId: String,
result: String,
terminationReason: String,
whiteId: String,
blackId: String,
)
@@ -1,50 +0,0 @@
package de.nowchess.api.event
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class EventEnvelopeTest extends AnyFunSuite with Matchers:
private val mapper =
val m = new ObjectMapper()
m.registerModule(DefaultScalaModule)
m.findAndRegisterModules()
m
test("EventEnvelope round-trips through JSON") {
val payload = mapper.createObjectNode()
payload.put("gameId", "game-123")
payload.put("difficulty", 3)
val original = EventEnvelope.of(EventType.GameStart, payload, Some("corr-abc"))
val json = mapper.writeValueAsString(original)
val decoded = mapper.readValue(json, classOf[EventEnvelope])
decoded.eventId shouldBe original.eventId
decoded.`type` shouldBe original.`type`
decoded.payload shouldBe original.payload
decoded.timestamp shouldBe original.timestamp
decoded.correlationId shouldBe Some("corr-abc")
}
test("EventEnvelope serializes without correlationId") {
val payload = mapper.createObjectNode()
payload.put("challengeId", "ch-1")
val envelope = EventEnvelope.of(EventType.ChallengeCreated, payload)
val json = mapper.writeValueAsString(envelope)
val decoded = mapper.readValue(json, classOf[EventEnvelope])
decoded.`type` shouldBe EventType.ChallengeCreated
decoded.correlationId shouldBe None
}
test("EventEnvelope.of generates unique eventIds") {
val payload = mapper.createObjectNode()
val e1 = EventEnvelope.of(EventType.BotGameStart, payload)
val e2 = EventEnvelope.of(EventType.BotGameStart, payload)
e1.eventId should not equal e2.eventId
}
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=17
MINOR=14
PATCH=0
-19
View File
@@ -129,22 +129,3 @@
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
-1
View File
@@ -73,7 +73,6 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("io.quarkus:quarkus-junit")
testImplementation("io.quarkus:quarkus-junit5-mockito")
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-test-security")
@@ -2,18 +2,14 @@ package de.nowchess.botplatform.registry
import de.nowchess.botplatform.config.RedisConfig
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{XGroupCreateArgs, XReadGroupArgs}
import io.quarkus.redis.datasource.pubsub.PubSubCommands
import io.smallrye.mutiny.subscription.MultiEmitter
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import org.eclipse.microprofile.context.ManagedExecutor
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.time.Duration
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
@ApplicationScoped
class BotRegistry:
@@ -21,68 +17,31 @@ class BotRegistry:
private val log = Logger.getLogger(classOf[BotRegistry])
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on DisableSyntax.var
private val groupName = "bot-platform-consumer"
private val consumerId = UUID.randomUUID().toString
private val emitters = ConcurrentHashMap[String, MultiEmitter[? >: String]]()
private val connections = ConcurrentHashMap[String, (MultiEmitter[? >: String], PubSubCommands.RedisSubscriber)]()
def register(botId: String, emitter: MultiEmitter[? >: String]): Unit =
createGroupIfAbsent(botId)
emitters.put(botId, emitter)
executor.submit(
new Runnable:
def run(): Unit = pollLoop(botId, emitter),
)
log.infof("Bot %s registered on stream consumer group", botId)
val channel = s"${redisConfig.prefix}:bot:$botId:events"
val handler: Consumer[String] = msg => emitter.emit(msg)
val subscriber = redis.pubsub(classOf[String]).subscribe(channel, handler)
connections.put(botId, (emitter, subscriber))
log.infof("Bot %s registered", botId)
()
def unregister(botId: String): Unit =
emitters.remove(botId)
Option(connections.remove(botId)).foreach { (_, subscriber) =>
subscriber.unsubscribe(s"${redisConfig.prefix}:bot:$botId:events")
}
log.infof("Bot %s unregistered", botId)
def dispatch(botId: String, event: String): Unit =
log.debugf("Dispatching event to bot %s", botId)
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event)
()
def registeredBots: List[String] =
emitters.keys().asScala.toList
private def streamKey(botId: String): String =
s"${redisConfig.prefix}:bot:$botId:events:stream"
private def createGroupIfAbsent(botId: String): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(streamKey(botId), groupName, "$", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create consumer group for bot %s", botId)
case Success(_) => ()
private def pollLoop(botId: String, myEmitter: MultiEmitter[? >: String]): Unit =
while emitters.get(botId) eq myEmitter do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName,
consumerId,
streamKey(botId),
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach { msg =>
if emitters.get(botId) eq myEmitter then
myEmitter.emit(msg.payload().get("data"))
ack(botId, msg.id())
})
} match
case Failure(ex) => log.warnf(ex, "Error in poll loop for bot %s", botId)
case Success(_) => ()
private def ack(botId: String, id: String): Unit =
Try(redis.stream(classOf[String]).xack(streamKey(botId), groupName, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack message %s for bot %s", id, botId)
case Success(_) => ()
import scala.jdk.CollectionConverters.*
connections.keys().asScala.toList
@@ -1,83 +0,0 @@
package de.nowchess.botplatform.registry
import de.nowchess.botplatform.config.RedisConfig
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{StreamCommands, XGroupCreateArgs}
import io.smallrye.mutiny.subscription.MultiEmitter
import org.eclipse.microprofile.context.ManagedExecutor
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.function.Executable
import org.junit.jupiter.api.{BeforeEach, Test}
import org.mockito.ArgumentMatchers.*
import org.mockito.Mockito.*
class BotRegistryTest:
// scalafix:off DisableSyntax.var
private var registry: BotRegistry = scala.compiletime.uninitialized
private var redis: RedisDataSource = scala.compiletime.uninitialized
private var streamCmds: StreamCommands[String, String, Nothing] =
scala.compiletime.uninitialized
private var redisConfig: RedisConfig = scala.compiletime.uninitialized
private var executor: ManagedExecutor = scala.compiletime.uninitialized
// scalafix:on DisableSyntax.var
@BeforeEach
def setup(): Unit =
redis = mock(classOf[RedisDataSource])
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
redisConfig = mock(classOf[RedisConfig])
executor = mock(classOf[ManagedExecutor])
when(redis.stream(classOf[String])).thenReturn(streamCmds)
when(redisConfig.prefix).thenReturn("nowchess")
registry = new BotRegistry
registry.redis = redis
registry.redisConfig = redisConfig
registry.executor = executor
@Test
def registerStartsPollThread(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
registry.register("bot1", emitter)
verify(executor).submit(any(classOf[Runnable]))
@Test
def registerCreatesConsumerGroupWithMkstream(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
registry.register("bot1", emitter)
verify(streamCmds)
.xgroupCreate(
org.mockito.ArgumentMatchers.eq("nowchess:bot:bot1:events:stream"),
org.mockito.ArgumentMatchers.eq("bot-platform-consumer"),
org.mockito.ArgumentMatchers.eq("$"),
any(classOf[XGroupCreateArgs]),
)
@Test
def registerTracksBot(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
registry.register("bot42", emitter)
assertTrue(registry.registeredBots.contains("bot42"))
@Test
def unregisterRemovesBot(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
registry.register("botX", emitter)
registry.unregister("botX")
assertFalse(registry.registeredBots.contains("botX"))
@Test
def busyGroupExceptionIsIgnoredOnRegister(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
when(streamCmds.xgroupCreate(any(), any(), any(), any()))
.thenThrow(new RuntimeException("BUSYGROUP Consumer Group name already exists"))
val exec: Executable = () => registry.register("botBusy", emitter)
assertDoesNotThrow(exec)
@Test
def registerDoesNotInteractWithPubSub(): Unit =
val emitter = mock(classOf[MultiEmitter[String]])
registry.register("botNoPubSub", emitter)
verify(redis, never()).pubsub(any(classOf[Class[?]]))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=12
MINOR=11
PATCH=0
-210
View File
@@ -1827,213 +1827,3 @@
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
### Reverts
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
-2
View File
@@ -138,8 +138,6 @@ tasks.withType(org.gradle.api.tasks.scala.ScalaCompile::class).configureEach {
exclude("**/resource/GameDtoMapper.scala")
exclude("**/resource/GameResource.scala")
exclude("**/redis/GameRedis*.scala")
exclude("**/redis/GameCreationStreamListener.scala")
exclude("**/service/GameCreationService.scala")
}
}
}
@@ -2,7 +2,6 @@ package de.nowchess.chess.config
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.dto.*
import de.nowchess.api.event.{EventEnvelope, EventType, GameOverPayload}
import de.nowchess.api.game.{DrawReason, GameContext, GameMode, GameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.chess.registry.GameCacheDto
@@ -14,11 +13,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
classOf[GameCacheDto],
classOf[ClockDto],
classOf[CreateGameRequestDto],
classOf[GameCreationRequestDto],
classOf[GameCreationResponseDto],
classOf[EventEnvelope],
classOf[EventType],
classOf[GameOverPayload],
classOf[ErrorEventDto],
classOf[GameWritebackEventDto],
classOf[GameFullDto],
@@ -1,146 +0,0 @@
package de.nowchess.chess.redis
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.dto.{GameCreationRequestDto, GameCreationResponseDto}
import de.nowchess.api.event.{EventEnvelope, EventType}
import de.nowchess.chess.config.RedisConfig
import de.nowchess.chess.service.GameCreationService
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{StreamMessage, XAddArgs, XGroupCreateArgs, XReadGroupArgs}
import io.quarkus.runtime.StartupEvent
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Observes
import jakarta.inject.Inject
import org.eclipse.microprofile.config.inject.ConfigProperty
import org.eclipse.microprofile.context.ManagedExecutor
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.time.Duration
import java.util.UUID
@ApplicationScoped
class GameCreationStreamListener:
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var creationService: GameCreationService = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@ConfigProperty(name = "nowchess.game-creation-stream.enabled", defaultValue = "true")
private var streamEnabled: Boolean = true
// scalafix:on DisableSyntax.var
private val log = Logger.getLogger(classOf[GameCreationStreamListener])
private val groupName = "core-game-creation"
private val consumerId = UUID.randomUUID().toString
private val maxRetries = 3
private val maxStreamLen = 1000L
private def requestStream: String = s"${redisConfig.prefix}:game-creation"
private def responseStream: String = s"${redisConfig.prefix}:game-creation-response"
private def dlqStream: String = s"${redisConfig.prefix}:dlq"
def start(@Observes _ev: StartupEvent): Unit =
if streamEnabled then
createGroupIfAbsent()
executor.submit(
new Runnable:
def run(): Unit = pollLoop(),
)
log.infof("Game-creation request listener started (consumer=%s)", consumerId)
private def createGroupIfAbsent(): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(requestStream, groupName, "0", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create game-creation consumer group")
case Success(_) => ()
private def pollLoop(): Unit =
while true do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName,
consumerId,
requestStream,
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach(handleMessage))
} match
case Failure(ex) => log.warnf(ex, "Error in game-creation poll loop")
case Success(_) => ()
private def handleMessage(msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data")
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
Try(objectMapper.readValue(json, classOf[EventEnvelope])) match
case Failure(ex) =>
log.errorf(ex, "Unparseable game-creation event, sending to DLQ: %s", json)
toDlq(EventType.GameCreationRequest.toString, json, ex, attempt)
ack(msg.id())
case Success(envelope) =>
processEnvelope(msg, envelope, json, attempt)
private def processEnvelope(
msg: StreamMessage[String, String, String],
envelope: EventEnvelope,
json: String,
attempt: Int,
): Unit =
Try {
val req = objectMapper.treeToValue(envelope.payload, classOf[GameCreationRequestDto])
val entry = creationService.createGame(req)
publishResponse(envelope.correlationId, GameCreationResponseDto(Some(entry.gameId)))
} match
case Success(_) => ack(msg.id())
case Failure(ex) if attempt + 1 < maxRetries =>
log.warnf(ex, "Game creation failed (attempt %d), retrying", attempt)
retry(json, attempt + 1)
ack(msg.id())
case Failure(ex) =>
log.errorf(ex, "Game creation failed after %d attempts, sending to DLQ", maxRetries)
publishResponse(envelope.correlationId, GameCreationResponseDto(None, Some("Game creation failed")))
toDlq(envelope.`type`.toString, json, ex, attempt)
ack(msg.id())
private def publishResponse(correlationId: Option[String], resp: GameCreationResponseDto): Unit =
val payload = objectMapper.valueToTree[com.fasterxml.jackson.databind.JsonNode](resp)
val envelope = EventEnvelope.of(EventType.GameCreationResponse, payload, correlationId)
xadd(responseStream, Map("data" -> objectMapper.writeValueAsString(envelope)))
private def retry(json: String, attempt: Int): Unit =
xadd(requestStream, Map("data" -> json, "attempt" -> attempt.toString))
private def toDlq(eventType: String, json: String, error: Throwable, attempt: Int): Unit =
xadd(
dlqStream,
Map(
"data" -> json,
"eventType" -> eventType,
"error" -> Option(error.getMessage).getOrElse(error.getClass.getName),
"attempt" -> (attempt + 1).toString,
),
)
private def ack(id: String): Unit =
Try(redis.stream(classOf[String]).xack(requestStream, groupName, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack message %s", id)
case Success(_) => ()
private def xadd(key: String, fields: Map[String, String]): Unit =
Try(
redis
.stream(classOf[String])
.xadd(key, new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(), fields.asJava),
) match
case Failure(ex) => log.errorf(ex, "Failed to publish to stream %s", key)
case Success(_) => ()
@@ -1,18 +1,15 @@
package de.nowchess.chess.redis
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
import de.nowchess.api.board.Color
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.dto.{GameStateDto, GameStateEventDto, GameWritebackEventDto}
import de.nowchess.api.event.{EventEnvelope, EventType, GameOverPayload}
import de.nowchess.api.game.{CorrespondenceClockState, DrawReason, GameResult, LiveClockState, TimeControl, WinReason}
import de.nowchess.api.board.Color
import de.nowchess.chess.grpc.IoGrpcClientWrapper
import de.nowchess.chess.observer.{GameEvent, Observer}
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import de.nowchess.chess.resource.GameDtoMapper
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.XAddArgs
import org.jboss.logging.Logger
import scala.jdk.CollectionConverters.*
object GameRedisPublisher:
private val log = Logger.getLogger(classOf[GameRedisPublisher])
@@ -26,11 +23,8 @@ class GameRedisPublisher(
writebackEmit: String => Unit,
ioClient: IoGrpcClientWrapper,
onGameOver: String => Unit,
redisPrefix: String,
) extends Observer:
private val maxStreamLen = 1000L
def emitInitialWriteback(): Unit =
try
registry.get(gameId).foreach { entry =>
@@ -46,39 +40,10 @@ class GameRedisPublisher(
val dto = GameDtoMapper.toGameStateDto(entry, ioClient)
redis.pubsub(classOf[String]).publish(s2cTopicName, objectMapper.writeValueAsString(GameStateEventDto(dto)))
writebackEmit(objectMapper.writeValueAsString(buildWriteback(entry, dto)))
entry.engine.context.result.foreach { result =>
publishGameOver(entry, result)
onGameOver(gameId)
}
if entry.engine.context.result.isDefined then onGameOver(gameId)
}
catch case ex: Exception => GameRedisPublisher.log.warnf(ex, "Failed to publish game event for game %s", gameId)
private def publishGameOver(entry: GameEntry, result: GameResult): Unit =
val resultStr = result match
case GameResult.Win(Color.White, _) => "white"
case GameResult.Win(Color.Black, _) => "black"
case GameResult.Draw(_) => "draw"
val terminationReason = result match
case GameResult.Win(_, WinReason.Checkmate) => "checkmate"
case GameResult.Win(_, WinReason.Resignation) => "resignation"
case GameResult.Win(_, WinReason.TimeControl) => "timeout"
case GameResult.Draw(DrawReason.Stalemate) => "stalemate"
case GameResult.Draw(DrawReason.InsufficientMaterial) => "insufficient_material"
case GameResult.Draw(DrawReason.FiftyMoveRule) => "fifty_move"
case GameResult.Draw(DrawReason.ThreefoldRepetition) => "repetition"
case GameResult.Draw(DrawReason.Agreement) => "agreement"
val payload = objectMapper.valueToTree[JsonNode](
GameOverPayload(gameId, resultStr, terminationReason, entry.white.id.value, entry.black.id.value),
)
val envelope = EventEnvelope.of(EventType.GameOver, payload)
redis
.stream(classOf[String])
.xadd(
s"$redisPrefix:game-over",
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
Map("data" -> objectMapper.writeValueAsString(envelope)).asJava,
)
private def buildWriteback(entry: GameEntry, dto: GameStateDto): GameWritebackEventDto =
val clock = entry.engine.currentClockState
GameWritebackEventDto(
@@ -22,7 +22,7 @@ import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.Try
import java.util.concurrent.{ConcurrentHashMap, ExecutorService, Executors}
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
@ApplicationScoped
@@ -46,10 +46,6 @@ class GameRedisSubscriberManager:
private val c2sListeners = new ConcurrentHashMap[String, ReactivePubSubCommands.ReactiveRedisSubscriber]()
private val s2cObservers = new ConcurrentHashMap[String, Observer]()
// Per-game single-thread executor so c2s messages are handled off the Vert.x
// event loop (handleConnected/handleMove make blocking gRPC + Redis calls) while
// staying ordered per game.
private val c2sExecutors = new ConcurrentHashMap[String, ExecutorService]()
// scalafix:off DisableSyntax.var
private var clockExpireSubscriber: Option[ReactivePubSubCommands.ReactiveRedisSubscriber] = None
@@ -93,21 +89,13 @@ class GameRedisSubscriberManager:
writebackFn,
ioClient,
unsubscribeGame,
redisConfig.prefix,
)
s2cObservers.put(gameId, obs)
registry.get(gameId).foreach(_.engine.subscribe(obs))
obs.emitInitialWriteback()
heartbeatServiceOpt.foreach(_.addGameSubscription(gameId))
val executor = c2sExecutors.computeIfAbsent(gameId, _ => Executors.newSingleThreadExecutor())
val handler: Consumer[String] = msg =>
val task = new Runnable:
def run(): Unit =
try handleC2sMessage(gameId, msg)
catch case ex: Exception => log.warnf(ex, "Error handling c2s message for game %s", gameId)
Try(executor.execute(task))
()
val handler: Consumer[String] = msg => handleC2sMessage(gameId, msg)
try
val subscriber = reactiveRedis
.pubsub(classOf[String])
@@ -118,16 +106,6 @@ class GameRedisSubscriberManager:
log.debugf("Subscribed to game %s", gameId)
catch case ex: Exception => log.warnf(ex, "Redis subscription failed for game %s", gameId)
// Notify the official-bots service to start playing a side of a game. Mirrors
// the event the tournament service publishes; official-bots subscribes to
// "<prefix>:bot:*:events".
def publishBotGameStart(gameId: String, botId: String, playingAs: String): Unit =
val channel = s"${redisConfig.prefix}:bot:$botId:events"
val payload = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","botAccountId":"$botId"}"""
Try(redis.pubsub(classOf[String]).publish(channel, payload)) match
case scala.util.Failure(ex) => log.warnf(ex, "Failed to publish bot gameStart for game %s", gameId)
case scala.util.Success(_) => ()
def unsubscribeGame(gameId: String): Unit =
Option(c2sListeners.remove(gameId)).foreach { subscriber =>
subscriber.unsubscribe(c2sTopic(gameId)).subscribe().`with`(_ => (), _ => ())
@@ -135,7 +113,6 @@ class GameRedisSubscriberManager:
Option(s2cObservers.remove(gameId)).foreach { obs =>
registry.get(gameId).foreach(_.engine.unsubscribe(obs))
}
Option(c2sExecutors.remove(gameId)).foreach(_.shutdownNow())
heartbeatServiceOpt.foreach(_.removeGameSubscription(gameId))
log.debugf("Unsubscribed from game %s", gameId)
@@ -210,4 +187,3 @@ class GameRedisSubscriberManager:
clockExpireSubscriber.foreach(_.unsubscribe(clockExpireChannel).await().indefinitely())
c2sListeners.forEach((gameId, subscriber) => subscriber.unsubscribe(c2sTopic(gameId)).await().indefinitely())
s2cObservers.forEach((gameId, obs) => registry.get(gameId).foreach(_.engine.unsubscribe(obs)))
c2sExecutors.forEach((_, executor) => executor.shutdownNow())
@@ -25,7 +25,6 @@ import de.nowchess.chess.observer.*
import de.nowchess.chess.redis.GameRedisSubscriberManager
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import de.nowchess.security.InternalOnly
import jakarta.annotation.security.PermitAll
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
@@ -180,32 +179,6 @@ class GameResource:
)
created(GameDtoMapper.toGameFullDto(entry, ioClient))
// Player-facing game creation for "play vs bot". Unlike createGame this is not
// internal-only: a logged-in (or anonymous) player creates the game directly,
// and core notifies the official-bots service to play the bot side.
@POST
@Path("/vs-bot")
@PermitAll
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def createBotGame(body: CreateGameRequestDto): Response =
val req = Option(body).getOrElse(CreateGameRequestDto(None, None, None, None))
val white = playerInfoFrom(req.white, DefaultWhite)
val black = playerInfoFrom(req.black, DefaultBlack)
val tc = toTimeControl(req.timeControl)
val entry = newEntry(GameContext.initial, white, black, tc, GameMode.Open)
registry.store(entry)
subscriberManager.subscribeGame(entry.gameId)
notifyBotSide(entry)
log.infof("Bot game %s created — white=%s black=%s", entry.gameId, white.displayName, black.displayName)
created(GameDtoMapper.toGameFullDto(entry, ioClient))
private def notifyBotSide(entry: GameEntry): Unit =
if entry.black.id.value.startsWith("bot-") then
subscriberManager.publishBotGameStart(entry.gameId, entry.black.id.value, "black")
else if entry.white.id.value.startsWith("bot-") then
subscriberManager.publishBotGameStart(entry.gameId, entry.white.id.value, "white")
@GET
@Path("/{gameId}")
@Produces(Array(MediaType.APPLICATION_JSON))
@@ -1,71 +0,0 @@
package de.nowchess.chess.service
import de.nowchess.api.dto.{GameCreationRequestDto, PlayerInfoDto, TimeControlDto}
import de.nowchess.api.game.{GameContext, GameMode, TimeControl}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.chess.engine.GameEngine
import de.nowchess.chess.grpc.RuleSetGrpcAdapter
import de.nowchess.chess.redis.GameRedisSubscriberManager
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
@ApplicationScoped
class GameCreationService:
private val log = Logger.getLogger(classOf[GameCreationService])
// scalafix:off DisableSyntax.var
@Inject var registry: GameRegistry = uninitialized
@Inject var ruleSetAdapter: RuleSetGrpcAdapter = uninitialized
@Inject var subscriberManager: GameRedisSubscriberManager = uninitialized
// scalafix:on DisableSyntax.var
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
private val DefaultBlack = PlayerInfo(PlayerId("p2"), "Player 2")
def createGame(req: GameCreationRequestDto): GameEntry =
val white = playerInfoFrom(req.white, DefaultWhite)
val black = playerInfoFrom(req.black, DefaultBlack)
val tc = toTimeControl(req.timeControl)
val mode = req.mode.getOrElse(GameMode.Open)
val entry = newEntry(GameContext.initial, white, black, tc, mode)
registry.store(entry)
subscriberManager.subscribeGame(entry.gameId)
log.infof(
"Game %s created — white=%s black=%s mode=%s",
entry.gameId,
white.displayName,
black.displayName,
mode.toString,
)
entry
private def playerInfoFrom(dto: Option[PlayerInfoDto], default: PlayerInfo): PlayerInfo =
dto.fold(default)(d => PlayerInfo(PlayerId(d.id), d.displayName))
private def toTimeControl(dto: Option[TimeControlDto]): TimeControl =
dto match
case None => TimeControl.Unlimited
case Some(tc) =>
tc.daysPerMove match
case Some(d) => TimeControl.Correspondence(d)
case None =>
tc.limitSeconds.fold(TimeControl.Unlimited)(l => TimeControl.Clock(l, tc.incrementSeconds.getOrElse(0)))
private def newEntry(
ctx: GameContext,
white: PlayerInfo,
black: PlayerInfo,
tc: TimeControl,
mode: GameMode,
): GameEntry =
GameEntry(
registry.generateId(),
GameEngine(initialContext = ctx, ruleSet = ruleSetAdapter, timeControl = tc),
white,
black,
mode = mode,
)
@@ -18,8 +18,6 @@ nowchess:
enabled: false
coordinator:
enabled: false
game-creation-stream:
enabled: false
redis:
host: localhost
port: 6379
@@ -1,123 +0,0 @@
package de.nowchess.chess.redis
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import de.nowchess.api.board.Color
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.chess.client.CombinedExportResponse
import de.nowchess.chess.engine.GameEngine
import de.nowchess.chess.grpc.IoGrpcClientWrapper
import de.nowchess.chess.observer.GameEvent
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import de.nowchess.rules.sets.DefaultRules
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.pubsub.PubSubCommands
import io.quarkus.redis.datasource.stream.{StreamCommands, XAddArgs}
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.{BeforeEach, Test}
import org.mockito.ArgumentMatchers.*
import org.mockito.Mockito.*
import scala.compiletime.uninitialized
class GameRedisPublisherTest:
// scalafix:off DisableSyntax.var
private var redis: RedisDataSource = uninitialized
private var streamCmds: StreamCommands[String, String, Nothing] = uninitialized
private var pubsubCmds: PubSubCommands[String] = uninitialized
private var registry: GameRegistry = uninitialized
private var ioClient: IoGrpcClientWrapper = uninitialized
private var onGameOverCalled: Boolean = false
// scalafix:on DisableSyntax.var
private val objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
private val gameId = "game1"
private val whitePlayer = PlayerInfo(PlayerId("white1"), "Alice")
private val blackPlayer = PlayerInfo(PlayerId("black1"), "Bob")
@BeforeEach
def setup(): Unit =
redis = mock(classOf[RedisDataSource])
streamCmds = mock(classOf[StreamCommands[String, String, Nothing]])
pubsubCmds = mock(classOf[PubSubCommands[String]])
registry = mock(classOf[GameRegistry])
ioClient = mock(classOf[IoGrpcClientWrapper])
when(redis.stream(classOf[String])).thenReturn(streamCmds)
when(redis.pubsub(classOf[String])).thenReturn(pubsubCmds)
when(ioClient.exportCombined(any()))
.thenReturn(CombinedExportResponse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", ""))
onGameOverCalled = false
private def publisherWithResult(result: GameResult): GameRedisPublisher =
val ctx = GameContext.initial.copy(result = Some(result))
val engine = new GameEngine(initialContext = ctx, ruleSet = DefaultRules)
val entry = GameEntry(gameId, engine, whitePlayer, blackPlayer)
when(registry.get(gameId)).thenReturn(Some(entry))
new GameRedisPublisher(
gameId,
registry,
redis,
objectMapper,
s"nowchess:game:$gameId:s2c",
_ => (),
ioClient,
_ => onGameOverCalled = true,
"nowchess",
)
@Test
def publishesGameOverOnCheckmate(): Unit =
val publisher = publisherWithResult(GameResult.Win(Color.White, WinReason.Checkmate))
publisher.onGameEvent(mock(classOf[GameEvent]))
verify(streamCmds).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
any(classOf[XAddArgs]),
any(),
)
assertTrue(onGameOverCalled)
@Test
def publishesGameOverOnResignation(): Unit =
val publisher = publisherWithResult(GameResult.Win(Color.Black, WinReason.Resignation))
publisher.onGameEvent(mock(classOf[GameEvent]))
verify(streamCmds).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
any(classOf[XAddArgs]),
any(),
)
@Test
def publishesGameOverOnDraw(): Unit =
val publisher = publisherWithResult(GameResult.Draw(DrawReason.Agreement))
publisher.onGameEvent(mock(classOf[GameEvent]))
verify(streamCmds).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
any(classOf[XAddArgs]),
any(),
)
@Test
def doesNotPublishGameOverWhenNoResult(): Unit =
val ctx = GameContext.initial
val engine = new GameEngine(initialContext = ctx, ruleSet = DefaultRules)
val entry = GameEntry(gameId, engine, whitePlayer, blackPlayer)
when(registry.get(gameId)).thenReturn(Some(entry))
val publisher = new GameRedisPublisher(
gameId,
registry,
redis,
objectMapper,
s"nowchess:game:$gameId:s2c",
_ => (),
ioClient,
_ => onGameOverCalled = true,
"nowchess",
)
publisher.onGameEvent(mock(classOf[GameEvent]))
verify(streamCmds, never()).xadd(
org.mockito.ArgumentMatchers.eq("nowchess:game-over"),
any(classOf[XAddArgs]),
any(),
)
assertFalse(onGameOverCalled)
@@ -1,68 +0,0 @@
package de.nowchess.chess.service
import de.nowchess.api.dto.{GameCreationRequestDto, PlayerInfoDto, TimeControlDto}
import de.nowchess.api.game.{GameMode, TimeControl}
import de.nowchess.api.player.PlayerType
import de.nowchess.chess.client.CombinedExportResponse
import de.nowchess.chess.grpc.IoGrpcClientWrapper
import de.nowchess.chess.redis.GameRedisSubscriberManager
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.{verify, when}
import scala.compiletime.uninitialized
// scalafix:off
@QuarkusTest
@DisplayName("GameCreationService")
class GameCreationServiceTest:
@Inject
var service: GameCreationService = uninitialized
@InjectMock
var subscriberManager: GameRedisSubscriberManager = uninitialized
@InjectMock
var ioWrapper: IoGrpcClientWrapper = uninitialized
@BeforeEach
def setup(): Unit =
when(ioWrapper.exportCombined(any()))
.thenReturn(CombinedExportResponse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", ""))
private def player(id: String, name: String): PlayerInfoDto =
PlayerInfoDto(id, name, PlayerType.Human)
@Test
def createsGameAndSubscribes(): Unit =
val req =
GameCreationRequestDto(Some(player("w", "White")), Some(player("b", "Black")), None, Some(GameMode.Authenticated))
val entry = service.createGame(req)
assertNotNull(entry.gameId)
assertEquals("White", entry.white.displayName)
assertEquals("Black", entry.black.displayName)
assertEquals(GameMode.Authenticated, entry.mode)
verify(subscriberManager).subscribeGame(entry.gameId)
@Test
def defaultsToOpenModeAndDefaultPlayers(): Unit =
val entry = service.createGame(GameCreationRequestDto(None, None, None, None))
assertEquals(GameMode.Open, entry.mode)
assertEquals("Player 1", entry.white.displayName)
assertEquals("Player 2", entry.black.displayName)
@Test
def mapsClockTimeControl(): Unit =
val tc = TimeControlDto(Some(300), Some(5), None)
val entry = service.createGame(GameCreationRequestDto(None, None, Some(tc), None))
assertEquals(TimeControl.Clock(300, 5), entry.engine.timeControl)
@Test
def mapsCorrespondenceTimeControl(): Unit =
val tc = TimeControlDto(None, None, Some(3))
val entry = service.createGame(GameCreationRequestDto(None, None, Some(tc), None))
assertEquals(TimeControl.Correspondence(3), entry.engine.timeControl)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=50
MINOR=47
PATCH=0
-75
View File
@@ -179,78 +179,3 @@
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
@@ -1,7 +1,6 @@
package de.nowchess.bot.service
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.event.EventEnvelope
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.bot.BotController
import de.nowchess.bot.BotDifficulty
@@ -10,23 +9,16 @@ 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.redis.datasource.stream.{StreamMessage, XAddArgs, XGroupCreateArgs, XReadGroupArgs}
import io.quarkus.runtime.StartupEvent
import jakarta.annotation.PostConstruct
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Observes
import jakarta.inject.Inject
import org.eclipse.microprofile.context.ManagedExecutor
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.time.Duration
import java.util.UUID
import io.quarkus.redis.datasource.pubsub.PubSubCommands
import java.util.concurrent.{ConcurrentHashMap, TimeUnit}
import java.util.function.Consumer
import java.util.concurrent.TimeUnit
@ApplicationScoped
class OfficialBotService:
@@ -39,7 +31,6 @@ class OfficialBotService:
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var botController: BotController = uninitialized
@Inject var meterRegistry: MeterRegistry = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject
@RestClient
@@ -49,18 +40,6 @@ class OfficialBotService:
private val terminalStatuses =
Set("checkmate", "resign", "timeout", "stalemate", "insufficientMaterial", "draw")
private val groupName = "official-bot"
private val gameOverGroup = "official-bots-game-over"
private val consumerId = UUID.randomUUID().toString
private val maxRetries = 3
private val maxStreamLen = 1000L
private def eventStream(botName: String): String = s"${redisConfig.prefix}:bot:$botName:events:stream"
private def gameOverStream: String = s"${redisConfig.prefix}:game-over"
private def dlqStream: String = s"${redisConfig.prefix}:dlq"
private val gameWatches = new ConcurrentHashMap[String, (String, PubSubCommands.RedisSubscriber)]()
@PostConstruct
def initializeMetrics(): Unit =
BotController.listBots.foreach { bot =>
@@ -73,95 +52,22 @@ class OfficialBotService:
try accountServiceClient.syncBots(SyncOfficialBotsRequest(bots))
catch case ex: Exception => log.errorf(ex, "Failed to auto-register official bots with account service")
bots.foreach(subscribeToEventChannel)
subscribeToGameOverStream()
private def subscribeToEventChannel(botName: String): Unit =
createGroupIfAbsent(botName)
executor.submit(
new Runnable:
def run(): Unit = pollLoop(botName),
)
log.infof("Listening to bot event stream for %s (consumer=%s)", botName, consumerId)
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:bot:$botName:events", handler)
()
private def createGroupIfAbsent(botName: String): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(eventStream(botName), groupName, "0", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create bot event consumer group for %s", botName)
case Success(_) => ()
private def pollLoop(botName: String): Unit =
while true do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName,
consumerId,
eventStream(botName),
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach(msg => handleStreamMessage(botName, msg)))
} match
case Failure(ex) => log.warnf(ex, "Error in bot event poll loop for %s", botName)
case Success(_) => ()
private def handleStreamMessage(botName: String, msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data")
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
Try {
val envelope = objectMapper.readValue(json, classOf[EventEnvelope])
handleBotEvent(botName, envelope)
} match
case Success(_) => ack(botName, msg.id())
case Failure(ex) if attempt + 1 < maxRetries =>
log.warnf(ex, "Bot event handling failed for %s (attempt %d), retrying", botName, attempt)
retry(botName, json, attempt + 1)
ack(botName, msg.id())
case Failure(ex) =>
log.errorf(ex, "Bot event handling failed for %s after %d attempts, sending to DLQ", botName, maxRetries)
toDlq(json, ex, attempt)
ack(botName, msg.id())
private def handleBotEvent(botName: String, envelope: EventEnvelope): Unit =
val payload = envelope.payload
val gameId = payload.path("gameId").asText()
val playingAs = payload.path("playingAs").asText()
val difficulty = payload.path("difficulty").asInt(1400)
val botAccountId = payload.path("botAccountId").asText()
watchGame(botName, gameId, playingAs, difficulty, botAccountId)
private def ack(botName: String, id: String): Unit =
Try(redis.stream(classOf[String]).xack(eventStream(botName), groupName, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack bot event %s", id)
case Success(_) => ()
private def retry(botName: String, json: String, attempt: Int): Unit =
xadd(eventStream(botName), Map("data" -> json, "attempt" -> attempt.toString))
private def toDlq(json: String, error: Throwable, attempt: Int): Unit =
xadd(
dlqStream,
Map(
"data" -> json,
"eventType" -> "BotGameStart",
"error" -> Option(error.getMessage).getOrElse(error.getClass.getName),
"attempt" -> (attempt + 1).toString,
),
)
private def xadd(key: String, fields: Map[String, String]): Unit =
Try(
redis
.stream(classOf[String])
.xadd(key, new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(), fields.asJava),
) match
case Failure(ex) => log.errorf(ex, "Failed to publish to stream %s", key)
case Success(_) => ()
private def handleBotEvent(botName: String, msg: String): Unit =
try
val node = objectMapper.readTree(msg)
if node.path("type").asText() == "gameStart" then
val gameId = node.path("gameId").asText()
val playingAs = node.path("playingAs").asText()
val difficulty = node.path("difficulty").asInt(1400)
val botAccountId = node.path("botAccountId").asText()
watchGame(botName, gameId, playingAs, difficulty, botAccountId)
catch case _: Exception => ()
private def watchGame(
botName: String,
@@ -171,80 +77,9 @@ class OfficialBotService:
botAccountId: String,
): Unit =
val handler: Consumer[String] = msg => handleGameEvent(botName, gameId, playingAs, difficulty, botAccountId, msg)
val subscriber = redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:game:$gameId:s2c", handler)
gameWatches.put(gameId, (botName, subscriber))
redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:game:$gameId:s2c", handler)
()
private def subscribeToGameOverStream(): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(gameOverStream, gameOverGroup, "$", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create game-over consumer group")
case Success(_) => ()
executor.submit(
new Runnable:
def run(): Unit = gameOverPollLoop(),
)
log.infof("Listening to game-over stream (consumer=%s)", consumerId)
private def gameOverPollLoop(): Unit =
while true do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
gameOverGroup,
consumerId,
gameOverStream,
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach(msg => handleGameOverMessage(msg)))
} match
case Failure(ex) => log.warnf(ex, "Error in game-over poll loop")
case Success(_) => ()
private def handleGameOverMessage(msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data")
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
Try {
val node = objectMapper.readTree(json)
val gameId = node.path("payload").path("gameId").asText()
if gameId.nonEmpty then
Option(gameWatches.remove(gameId)).foreach { (botName, subscriber) =>
val topic = s"${redisConfig.prefix}:game:$gameId:s2c"
Try(subscriber.unsubscribe(topic)) match
case Failure(ex) => log.warnf(ex, "Failed to unsubscribe from game %s", gameId)
case Success(_) => log.infof("Bot %s cleaned up game %s after GameOver", botName, gameId)
}
} match
case Success(_) =>
ackGameOver(msg.id())
case Failure(ex) if attempt + 1 < maxRetries =>
log.warnf(ex, "GameOver handling failed (attempt %d), retrying", attempt)
xadd(gameOverStream, Map("data" -> json, "attempt" -> (attempt + 1).toString))
ackGameOver(msg.id())
case Failure(ex) =>
log.errorf(ex, "GameOver handling failed after %d attempts, sending to DLQ", maxRetries)
xadd(
dlqStream,
Map(
"data" -> json,
"eventType" -> "GameOver",
"error" -> Option(ex.getMessage).getOrElse(ex.getClass.getName),
"attempt" -> attempt.toString,
),
)
ackGameOver(msg.id())
private def ackGameOver(id: String): Unit =
Try(redis.stream(classOf[String]).xack(gameOverStream, gameOverGroup, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack game-over message %s", id)
case Success(_) => ()
private def handleGameEvent(
botName: String,
gameId: String,
@@ -1,35 +0,0 @@
package de.nowchess.bot.service
import com.fasterxml.jackson.databind.ObjectMapper
import scala.util.Try
final case class TournamentBotConfig(
serverUrl: String,
tournamentId: String,
token: String,
botId: String,
difficulty: String,
)
object TournamentBotConfig:
private val mapper = new ObjectMapper()
def fromEnv(env: Map[String, String]): Option[TournamentBotConfig] =
for
tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty)
token <- env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty)
botId <- jwtSubject(token)
serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://localhost:8089")
difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium")
yield TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty)
def jwtSubject(token: String): Option[String] =
Try {
val parts = token.split("\\.")
if parts.length >= 2 then
val payload = new String(java.util.Base64.getUrlDecoder.decode(parts(1)))
val sub = mapper.readTree(payload).path("sub").asText()
Option(sub).filter(_.nonEmpty)
else None
}.toOption.flatten
@@ -1,226 +0,0 @@
package de.nowchess.bot.service
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.bot.{Bot, BotController}
import de.nowchess.io.fen.FenParser
import io.quarkus.runtime.Startup
import jakarta.annotation.{PostConstruct, PreDestroy}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
import jakarta.ws.rs.core.MediaType
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.io.{BufferedReader, InputStream, InputStreamReader}
import java.util.concurrent.{ConcurrentHashMap, ExecutorService, Executors}
@Startup
@ApplicationScoped
class TournamentBotGamePlayer:
private val log = Logger.getLogger(classOf[TournamentBotGamePlayer])
// scalafix:off DisableSyntax.var
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var botController: BotController = uninitialized
// scalafix:on DisableSyntax.var
private val client: Client = ClientBuilder.newClient()
private val workers: ExecutorService = Executors.newCachedThreadPool()
private val activeGames = ConcurrentHashMap.newKeySet[String]()
private val config = TournamentBotConfig.fromEnv(System.getenv().asScala.toMap)
// scalafix:off DisableSyntax.var
@volatile private var running = true
// scalafix:on DisableSyntax.var
@PostConstruct
def initialize(): Unit =
config match
case None =>
log.info("Tournament bot disabled — set TOURNAMENT_ID and TOURNAMENT_BOT_TOKEN to enable")
case Some(cfg) =>
log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId)
val thread = new Thread(() => connect(cfg), s"TournamentBot-${cfg.tournamentId}")
thread.setDaemon(true)
thread.start()
@PreDestroy
def cleanup(): Unit =
running = false
workers.shutdownNow()
Try(client.close())
log.info("Tournament bot stopped")
private def connect(cfg: TournamentBotConfig): Unit =
if join(cfg) then
while running do
Try(streamEvents(cfg)) match
case Failure(ex) => log.warnf(ex, "Tournament event stream dropped — reconnecting"); sleep(5000)
case Success(_) => sleep(2000)
private def join(cfg: TournamentBotConfig): Boolean =
Try {
val response = authed(cfg, target(cfg).path("join"))
.post(Entity.entity("", MediaType.APPLICATION_JSON))
val ok = response.getStatus == 200
if ok then log.infof("Joined tournament %s", cfg.tournamentId)
else log.errorf("Failed to join tournament %s — status %d", cfg.tournamentId, response.getStatus)
response.close()
ok
}.getOrElse { log.error("Join request failed"); false }
private def streamEvents(cfg: TournamentBotConfig): Unit =
val response = authed(cfg, target(cfg).path("stream"))
.header("Accept", "application/x-ndjson")
.get()
if response.getStatus != 200 then
log.warnf("Tournament stream returned status %d", response.getStatus)
response.close()
sleep(5000)
else
log.infof("Listening to tournament %s event stream", cfg.tournamentId)
forEachLine(response.readEntity(classOf[InputStream])): line =>
parse(line).foreach: node =>
if node.path("type").asText() == "gameStart" then onGameStart(cfg, node.path("gameId").asText())
private def onGameStart(cfg: TournamentBotConfig, gameId: String): Unit =
if gameId.nonEmpty && activeGames.add(gameId) then
workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId) })
()
private def playGame(cfg: TournamentBotConfig, gameId: String): Unit =
Try {
colorFor(cfg, gameId) match
case None =>
log.debugf("Game %s is not ours — ignoring", gameId)
activeGames.remove(gameId)
case Some(color) =>
log.infof("Playing game %s as %s", gameId, color)
val stream = openGameStream(cfg, gameId)
maybeMoveFromCurrentState(cfg, gameId, color)
stream.foreach(consumeGameStream(cfg, gameId, color, _))
activeGames.remove(gameId)
} match
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
case Success(_) => ()
private def colorFor(cfg: TournamentBotConfig, gameId: String): Option[String] =
fetchGame(cfg, gameId).flatMap: game =>
val white = game.path("white").path("id").asText()
val black = game.path("black").path("id").asText()
if white == cfg.botId then Some("white")
else if black == cfg.botId then Some("black")
else None
private def maybeMoveFromCurrentState(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
fetchGame(cfg, gameId).foreach: game =>
maybeMove(cfg, gameId, color, game.path("turn").asText(), game.path("status").asText(), game.path("fen").asText())
private def consumeGameStream(cfg: TournamentBotConfig, gameId: String, color: String, stream: InputStream): Unit =
val reader = new BufferedReader(new InputStreamReader(stream))
// scalafix:off DisableSyntax.var
var done = false
// scalafix:on DisableSyntax.var
Iterator
.continually(reader.readLine())
.map(Option(_))
.takeWhile(opt => opt.isDefined && running && !done)
.flatten
.foreach { line =>
parse(line).foreach: node =>
node.path("type").asText() match
case "move" =>
maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText())
case "gameEnd" => log.infof("Game %s ended — status=%s", gameId, node.path("status").asText()); done = true
case _ => ()
}
private def maybeMove(
cfg: TournamentBotConfig,
gameId: String,
color: String,
turn: String,
status: String,
fen: String,
): Unit =
if turn == color && status == "ongoing" && fen.nonEmpty then
computeUci(cfg, fen) match
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
case Some(uci) => submitMove(cfg, gameId, uci)
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
FenParser.parseFen(fen) match
case Left(err) => log.warnf("FEN parse failed: %s (%s)", fen, err.toString); None
case Right(context) => engine(cfg).apply(context).map(toUci)
private def submitMove(cfg: TournamentBotConfig, gameId: String, uci: String): Unit =
Try {
val response = authed(cfg, target(cfg).path("game").path(gameId).path("move").path(uci))
.post(Entity.entity("", MediaType.APPLICATION_JSON))
if response.getStatus == 200 then log.infof("Played %s in game %s", uci, gameId)
else log.warnf("Move %s rejected in game %s — status %d", uci, gameId, response.getStatus)
response.close()
} match
case Failure(ex) => log.errorf(ex, "Error submitting move %s in game %s", uci, gameId)
case Success(_) => ()
private def fetchGame(cfg: TournamentBotConfig, gameId: String): Option[JsonNode] =
Try {
val response = target(cfg).path("game").path(gameId).request(MediaType.APPLICATION_JSON).get()
val node = if response.getStatus == 200 then Some(response.readEntity(classOf[JsonNode])) else None
response.close()
node
}.getOrElse(None)
private def openGameStream(cfg: TournamentBotConfig, gameId: String): Option[InputStream] =
Try {
val response = authed(cfg, target(cfg).path("game").path(gameId).path("stream"))
.header("Accept", "application/x-ndjson")
.get()
if response.getStatus == 200 then Some(response.readEntity(classOf[InputStream]))
else { log.warnf("Game stream %s returned status %d", gameId, response.getStatus); response.close(); None }
}.getOrElse(None)
private def engine(cfg: TournamentBotConfig): Bot =
botController.getBot(cfg.difficulty).orElse(botController.getBot("medium")).get
private def target(cfg: TournamentBotConfig) =
client.target(cfg.serverUrl).path("api").path("tournament").path(cfg.tournamentId)
private def authed(cfg: TournamentBotConfig, t: jakarta.ws.rs.client.WebTarget) =
t.request(MediaType.APPLICATION_JSON).header("Authorization", s"Bearer ${cfg.token}")
private def parse(line: String): Option[JsonNode] =
val trimmed = line.trim
if trimmed.isEmpty then None else Try(objectMapper.readTree(trimmed)).toOption
private def forEachLine(stream: InputStream)(handle: String => Unit): Unit =
val reader = new BufferedReader(new InputStreamReader(stream))
Iterator
.continually(reader.readLine())
.map(Option(_))
.takeWhile(opt => opt.isDefined && running)
.flatten
.foreach { line =>
Try(handle(line)).failed.foreach(ex => log.warnf(ex, "Error handling stream line"))
}
private def toUci(move: Move): String =
val base = s"${move.from}${move.to}"
move.moveType match
case MoveType.Promotion(piece) => base + promotionChar(piece)
case _ => base
private def promotionChar(piece: PromotionPiece): String =
piece match
case PromotionPiece.Knight => "n"
case PromotionPiece.Bishop => "b"
case PromotionPiece.Rook => "r"
case PromotionPiece.Queen => "q"
private def sleep(ms: Long): Unit = Try(Thread.sleep(ms))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=17
MINOR=14
PATCH=0
@@ -1,36 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/tournament-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/tournament-jvm
#
# This image uses the `run-java.sh` script to run the application.
# You can find more information about the UBI base runtime images and their configuration here:
# https://rh-openjdk.github.io/redhat-openjdk-containers/
###
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 build/quarkus-app/*.jar /deployments/
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -1,33 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.package.jar.type=legacy-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/tournament-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/tournament-legacy-jar
#
# This image uses the `run-java.sh` script to run the application.
# You can find more information about the UBI base runtime images and their configuration here:
# https://rh-openjdk.github.io/redhat-openjdk-containers/
###
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
ENV LANGUAGE='en_US:en'
COPY build/lib/* /deployments/lib/
COPY build/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -1,29 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
#
# Before building the container image run:
#
# ./gradlew :modules:tournament:build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f modules/tournament/src/main/docker/Dockerfile.native -t quarkus/tournament .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/tournament
#
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 modules/tournament/build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -1,32 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
# It uses a micro base image, tuned for Quarkus native executables.
# It reduces the size of the resulting container image.
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/tournament .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/tournament
#
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
###
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -1,27 +0,0 @@
{
"reflection": [
{ "type": "scala.Tuple1[]" },
{ "type": "scala.Tuple2[]" },
{ "type": "scala.Tuple3[]" },
{ "type": "scala.Tuple4[]" },
{ "type": "scala.Tuple5[]" },
{ "type": "scala.Tuple6[]" },
{ "type": "scala.Tuple7[]" },
{ "type": "scala.Tuple8[]" },
{ "type": "scala.Tuple9[]" },
{ "type": "scala.Tuple10[]" },
{ "type": "scala.Tuple11[]" },
{ "type": "scala.Tuple12[]" },
{ "type": "scala.Tuple13[]" },
{ "type": "scala.Tuple14[]" },
{ "type": "scala.Tuple15[]" },
{ "type": "scala.Tuple16[]" },
{ "type": "scala.Tuple17[]" },
{ "type": "scala.Tuple18[]" },
{ "type": "scala.Tuple19[]" },
{ "type": "scala.Tuple20[]" },
{ "type": "scala.Tuple21[]" },
{ "type": "scala.Tuple22[]" },
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
]
}
@@ -1,34 +0,0 @@
package de.nowchess.tournament.config
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
import de.nowchess.tournament.dto.*
import de.nowchess.tournament.error.TournamentError
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[Tournament],
classOf[TournamentPairing],
classOf[TournamentParticipant],
classOf[TournamentError],
classOf[BotRef],
classOf[Clock],
classOf[Variant],
classOf[CreateTournamentForm],
classOf[ResultDto],
classOf[Standing],
classOf[TournamentDto],
classOf[TournamentListDto],
classOf[PairingDto],
classOf[GameExportDto],
classOf[RoundPairingsDto],
classOf[ErrorDto],
classOf[OkDto],
classOf[CorePlayerInfo],
classOf[CoreTimeControl],
classOf[CoreCreateGameRequest],
classOf[CoreGameResponse],
),
)
class NativeReflectionConfig
@@ -14,10 +14,10 @@ class Tournament:
@Column(nullable = false)
var fullName: String = uninitialized
var nbRounds: Int = 0
var clockLimit: Int = 0
var nbRounds: Int = 0
var clockLimit: Int = 0
var clockIncrement: Int = 0
var rated: Boolean = true
var rated: Boolean = true
@Column(nullable = false)
var status: String = "created"
@@ -27,7 +27,7 @@ class Tournament:
@Column(nullable = false)
var createdBy: String = uninitialized
var startsAt: Instant = uninitialized
var winnerId: String = uninitialized
var startsAt: Instant = uninitialized
var winnerId: String = uninitialized
var winnerName: String = uninitialized
// scalafix:on
@@ -15,8 +15,8 @@ class TournamentPairing:
@Column(nullable = false)
var tournamentId: String = uninitialized
var round: Int = 0
var whiteId: String = uninitialized
var round: Int = 0
var whiteId: String = uninitialized
var whiteName: String = uninitialized
@Column(nullable = false)
@@ -25,7 +25,7 @@ class TournamentPairing:
@Column(nullable = false)
var blackName: String = uninitialized
var gameId: String = uninitialized
var winner: String = uninitialized
var gameId: String = uninitialized
var winner: String = uninitialized
var moveList: String = uninitialized
// scalafix:on
@@ -21,11 +21,11 @@ class TournamentParticipant:
@Column(nullable = false)
var botName: String = uninitialized
var points: Double = 0.0
var points: Double = 0.0
var tieBreak: Double = 0.0
var nbGames: Int = 0
var wins: Int = 0
var draws: Int = 0
var losses: Int = 0
var byeCount: Int = 0
var nbGames: Int = 0
var wins: Int = 0
var draws: Int = 0
var losses: Int = 0
var byeCount: Int = 0
// scalafix:on
@@ -1,10 +1,10 @@
package de.nowchess.tournament.error
enum TournamentError(val message: String):
case NotFound(id: String) extends TournamentError(s"Tournament $id not found")
case NotDirector extends TournamentError("Not the tournament director")
case NotFound(id: String) extends TournamentError(s"Tournament $id not found")
case NotDirector extends TournamentError("Not the tournament director")
case WrongStatus(expected: String) extends TournamentError(s"Tournament must be in $expected status")
case AlreadyJoined extends TournamentError("Already joined this tournament")
case NotJoined extends TournamentError("Not joined this tournament")
case NotEnoughParticipants extends TournamentError("Need at least 2 participants to start")
case NotABot extends TournamentError("Only bot accounts can join tournaments")
case AlreadyJoined extends TournamentError("Already joined this tournament")
case NotJoined extends TournamentError("Not joined this tournament")
case NotEnoughParticipants extends TournamentError("Need at least 2 participants to start")
case NotABot extends TournamentError("Only bot accounts can join tournaments")
@@ -21,11 +21,11 @@ import java.util.UUID
@ApplicationScoped
class GameResultStreamListener:
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var tournamentService: TournamentService = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var tournamentService: TournamentService = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on
private val log = Logger.getLogger(classOf[GameResultStreamListener])
@@ -37,9 +37,8 @@ class GameResultStreamListener:
@PostConstruct
def startListening(): Unit =
createGroupIfAbsent()
executor.submit(
new Runnable:
def run(): Unit = pollLoop(),
executor.submit(new Runnable:
def run(): Unit = pollLoop()
)
log.infof("Tournament result listener started (consumer=%s)", consumerId)
@@ -50,36 +49,20 @@ class GameResultStreamListener:
case Success(_) => ()
private def pollLoop(): Unit =
// scalafix:off DisableSyntax.var
var running = true
// scalafix:on DisableSyntax.var
while running do
while true do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName,
consumerId,
streamKey,
">",
new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach(msg => handleMessage(msg)))
val messages = redis.stream(classOf[String]).xreadgroup(
groupName,
consumerId,
streamKey,
">",
new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)),
)
if messages != null then messages.forEach(msg => handleMessage(msg))
} match
case Failure(ex) if isInterrupted(ex) =>
Thread.currentThread().interrupt()
running = false
case Failure(ex) => log.warnf(ex, "Error in result poll loop")
case Success(_) => ()
private def isInterrupted(ex: Throwable): Boolean =
ex match
case _: InterruptedException => true
case _ =>
Option(ex.getCause) match
case Some(_: InterruptedException) => true
case _ => false
private def handleMessage(msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data")
Try(objectMapper.readValue(json, classOf[GameWritebackEventDto])) match
@@ -41,7 +41,7 @@ class PairingRepository:
.headOption
def persist(p: TournamentPairing): TournamentPairing =
if Option(p.id).isEmpty then
if p.id == null then
em.persist(p)
p
else em.merge(p)
@@ -34,7 +34,7 @@ class ParticipantRepository:
.headOption
def persist(p: TournamentParticipant): TournamentParticipant =
if Option(p.id).isEmpty then
if p.id == null then
em.persist(p)
p
else em.merge(p)
@@ -119,13 +119,9 @@ class TournamentResource:
tournamentService.get(id) match
case None => Response.status(Response.Status.NOT_FOUND).entity("").build()
case Some(_) =>
val ndjson = tournamentService
.getResults(id)
.take(nb)
.map { r =>
s"""{"rank":${r.rank},"points":${r.points},"tieBreak":${r.tieBreak},"bot":{"id":"${r.bot.id}","name":"${r.bot.name}"},"nbGames":${r.nbGames},"wins":${r.wins},"draws":${r.draws},"losses":${r.losses}}"""
}
.mkString("\n")
val ndjson = tournamentService.getResults(id).take(nb).map { r =>
s"""{"rank":${r.rank},"points":${r.points},"tieBreak":${r.tieBreak},"bot":{"id":"${r.bot.id}","name":"${r.bot.name}"},"nbGames":${r.nbGames},"wins":${r.wins},"draws":${r.draws},"losses":${r.losses}}"""
}.mkString("\n")
Response.ok(ndjson).`type`("application/x-ndjson").build()
@GET
@@ -1,6 +1,6 @@
package de.nowchess.tournament.service
import de.nowchess.tournament.domain.{TournamentPairing, TournamentParticipant}
import de.nowchess.tournament.domain.{TournamentParticipant, TournamentPairing}
import java.util.concurrent.ThreadLocalRandom
object SwissPairingService:
@@ -9,9 +9,9 @@ object SwissPairingService:
participants: List[TournamentParticipant],
pastPairings: List[TournamentPairing],
): (List[(TournamentParticipant, TournamentParticipant)], Option[TournamentParticipant]) =
val sorted = sortParticipants(participants)
val sorted = sortParticipants(participants)
val (remaining, byeOpt) = extractByePlayer(sorted)
val pairs = buildPairs(remaining, pastPairings)
val pairs = buildPairs(remaining, pastPairings)
(pairs, byeOpt)
private def sortParticipants(participants: List[TournamentParticipant]): List[TournamentParticipant] =
@@ -37,21 +37,16 @@ object SwissPairingService:
): List[(TournamentParticipant, TournamentParticipant)] =
val arr = players.toArray
resolveConflicts(arr, pastPairings)
arr
.grouped(2)
.flatMap {
case Array(a, b) => Some(assignColors(a, b))
case _ => None
}
.toList
arr.grouped(2).flatMap {
case Array(a, b) => Some(assignColors(a, b))
case _ => None
}.toList
private def resolveConflicts(arr: Array[TournamentParticipant], pastPairings: List[TournamentPairing]): Unit =
// scalafix:off DisableSyntax.var
var i = 0
// scalafix:on DisableSyntax.var
while i < arr.length - 1 do
if havePlayedBefore(arr(i), arr(i + 1), pastPairings) && i + 2 < arr.length then
val tmp = arr(i + 1)
val tmp = arr(i + 1)
arr(i + 1) = arr(i + 2)
arr(i + 2) = tmp
i += 2
@@ -1,19 +1,9 @@
package de.nowchess.tournament.service
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo, CoreTimeControl}
import de.nowchess.tournament.config.RedisConfig
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
import de.nowchess.tournament.dto.{
BotRef,
Clock,
CreateTournamentForm,
PairingDto,
ResultDto,
Standing,
TournamentDto,
Variant,
}
import de.nowchess.tournament.dto.{BotRef, Clock, CreateTournamentForm, PairingDto, ResultDto, Standing, TournamentDto, Variant}
import de.nowchess.tournament.error.TournamentError
import de.nowchess.tournament.repository.{PairingRepository, ParticipantRepository, TournamentRepository}
import io.quarkus.redis.datasource.RedisDataSource
@@ -41,15 +31,14 @@ class TournamentService:
@RestClient
var coreGameClient: CoreGameClient = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on
@Transactional
def create(createdBy: String, form: CreateTournamentForm): Tournament =
val t = new Tournament()
t.id = java.util.UUID.randomUUID().toString.replace("-", "").take(8)
t.id = scala.util.Random.alphanumeric.take(6).mkString
t.fullName = form.name
t.nbRounds = form.nbRounds
t.clockLimit = form.clockLimit
@@ -77,7 +66,8 @@ class TournamentService:
t <- tournamentRepository.findOptById(id).toRight(TournamentError.NotFound(id))
_ <- Either.cond(t.createdBy == userId, (), TournamentError.NotDirector)
_ <- Either.cond(t.status == "created", (), TournamentError.WrongStatus("created"))
yield tournamentRepository.delete(t)
yield
tournamentRepository.delete(t)
@Transactional
def join(id: String, botId: String, botName: String): Either[TournamentError, Unit] =
@@ -125,7 +115,7 @@ class TournamentService:
Either.cond(ps.size >= 2, ps, TournamentError.NotEnoughParticipants)
private def startRound(t: Tournament, round: Int, participants: List[TournamentParticipant]): Unit =
val pastPairings = pairingRepository.findByTournamentId(t.id)
val pastPairings = pairingRepository.findByTournamentId(t.id)
val (pairs, byeOpt) = SwissPairingService.computePairings(participants, pastPairings)
byeOpt.foreach(bye => createByePairing(t.id, round, bye))
pairs.foreach { case (white, black) => createRealPairing(t.id, round, white, black, t) }
@@ -169,16 +159,8 @@ class TournamentService:
pairing.blackName = black.botName
pairing.gameId = resp.gameId
pairingRepository.persist(pairing)
streamManager.publishToBot(
tournamentId,
white.botId,
s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""",
)
streamManager.publishToBot(
tournamentId,
black.botId,
s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""",
)
streamManager.publishToBot(tournamentId, white.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""")
streamManager.publishToBot(tournamentId, black.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""")
publishBotGameStart(white.botName, resp.gameId, "white", white.botId)
publishBotGameStart(black.botName, resp.gameId, "black", black.botId)
@@ -189,15 +171,7 @@ class TournamentService:
botAccountId: String,
): Unit =
val channel = s"${redisConfig.prefix}:bot:$botName:events"
val payload = objectMapper.writeValueAsString(
Map(
"type" -> "gameStart",
"gameId" -> gameId,
"playingAs" -> playingAs,
"difficulty" -> 1500,
"botAccountId" -> botAccountId,
),
)
val payload = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":1500,"botAccountId":"$botAccountId"}"""
Try(redis.pubsub(classOf[String]).publish(channel, payload)) match
case Failure(ex) => log.warnf(ex, "Failed to publish gameStart to bot channel %s", channel)
case Success(_) => ()
@@ -238,7 +212,7 @@ class TournamentService:
private def checkRoundCompletion(tournamentId: String): Unit =
tournamentRepository.findOptById(tournamentId).foreach { t =>
val roundPairings = pairingRepository.findByTournamentIdAndRound(tournamentId, t.currentRound)
val allDone = roundPairings.nonEmpty && roundPairings.forall(p => Option(p.winner).isDefined)
val allDone = roundPairings.nonEmpty && roundPairings.forall(p => Option(p.winner).isDefined)
if allDone then onRoundComplete(t)
}
@@ -308,7 +282,7 @@ class TournamentService:
status = t.status,
round = t.currentRound,
standing = Standing(1, standings),
winner = Option(t.winnerId).map(id => BotRef(id, t.winnerName)),
winner = if t.winnerId != null then Some(BotRef(t.winnerId, t.winnerName)) else None,
)
private def toPairingDto(p: TournamentPairing): PairingDto =
@@ -14,12 +14,8 @@ class TournamentStreamManager:
private def botKey(tournamentId: String, botId: String): String = s"${tournamentId}:${botId}"
def register(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit =
tournamentEmitters
.computeIfAbsent(tournamentId, _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]())
.add(emitter)
botEmitters
.computeIfAbsent(botKey(tournamentId, botId), _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]())
.add(emitter)
tournamentEmitters.computeIfAbsent(tournamentId, _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]()).add(emitter)
botEmitters.computeIfAbsent(botKey(tournamentId, botId), _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]()).add(emitter)
def unregister(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit =
Option(tournamentEmitters.get(tournamentId)).foreach(_.remove(emitter))
@@ -7,14 +7,9 @@ class H2TestProfile extends QuarkusTestProfile:
override def getConfigOverrides(): JMap[String, String] =
JMap.of(
"quarkus.datasource.db-kind",
"h2",
"quarkus.datasource.jdbc.url",
"jdbc:h2:mem:nowchess-tournament;DB_CLOSE_DELAY=-1",
"quarkus.datasource.username",
"sa",
"quarkus.datasource.password",
"",
"quarkus.hibernate-orm.schema-management.strategy",
"drop-and-create",
"quarkus.datasource.db-kind", "h2",
"quarkus.datasource.jdbc.url", "jdbc:h2:mem:nowchess-tournament;DB_CLOSE_DELAY=-1",
"quarkus.datasource.username", "sa",
"quarkus.datasource.password", "",
"quarkus.hibernate-orm.schema-management.strategy", "drop-and-create",
)
@@ -46,12 +46,8 @@ class TournamentResourceTest:
.formParam("clockLimit", 300)
.formParam("clockIncrement", 5)
.formParam("rated", true)
.when()
.post("/api/tournament")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
.when().post("/api/tournament")
.`then`().statusCode(201).extract().path[String]("id")
private def postAndCheck(token: String, path: String, expectedStatus: Int): ValidatableResponse =
authed(token).when().post(path).`then`().statusCode(expectedStatus)
@@ -74,35 +70,25 @@ class TournamentResourceTest:
.formParam("clockLimit", 300)
.formParam("clockIncrement", 5)
.formParam("rated", true)
.when()
.post("/api/tournament")
.`then`()
.statusCode(201)
.when().post("/api/tournament")
.`then`().statusCode(201)
.body("fullName", is("Test Tour"))
.body("status", is("created"))
@Test
def returns401WhenUnauthenticated(): Unit =
RestAssured
.`given`()
.contentType(ContentType.URLENC)
RestAssured.`given`().contentType(ContentType.URLENC)
.formParam("name", "Test Tour")
.formParam("nbRounds", 3)
.formParam("clockLimit", 300)
.formParam("clockIncrement", 5)
.when()
.post("/api/tournament")
.`then`()
.statusCode(401)
.when().post("/api/tournament")
.`then`().statusCode(401)
@Test
def returnsEmptyListsOnFreshStart(): Unit =
RestAssured
.`given`()
.when()
.get("/api/tournament")
.`then`()
.statusCode(200)
RestAssured.`given`().when().get("/api/tournament")
.`then`().statusCode(200)
.body("created", notNullValue())
.body("started", notNullValue())
.body("finished", notNullValue())
@@ -110,12 +96,8 @@ class TournamentResourceTest:
@Test
def returnsCreatedTournamentInCreatedList(): Unit =
val id = createTournament(directorToken("director-list"), "ListTour")
RestAssured
.`given`()
.when()
.get("/api/tournament")
.`then`()
.statusCode(200)
RestAssured.`given`().when().get("/api/tournament")
.`then`().statusCode(200)
.body("created.id", hasItem(id))
@Test
@@ -125,12 +107,8 @@ class TournamentResourceTest:
@Test
def returnsTournamentWithStandings(): Unit =
val id = createTournament(directorToken("dir-get"), "GetTour")
RestAssured
.`given`()
.when()
.get(s"/api/tournament/$id")
.`then`()
.statusCode(200)
RestAssured.`given`().when().get(s"/api/tournament/$id")
.`then`().statusCode(200)
.body("id", is(id))
.body("standing", notNullValue())
@@ -158,10 +136,8 @@ class TournamentResourceTest:
def botJoinsSuccessfully(): Unit =
val id = createTournament(directorToken("dir-join"), "JoinTour")
authed(botToken("joinbot-1", "JoinBot1"))
.when()
.post(s"/api/tournament/$id/join")
.`then`()
.statusCode(200)
.when().post(s"/api/tournament/$id/join")
.`then`().statusCode(200)
.body("ok", is(true))
@Test
@@ -190,11 +166,8 @@ class TournamentResourceTest:
val id = createTournament(directorToken("dir-wd"), "WdTour")
val bt = botToken("wdbot-1", "WdBot1")
botJoin(id, "wdbot-1", "WdBot1")
authed(bt)
.when()
.post(s"/api/tournament/$id/withdraw")
.`then`()
.statusCode(200)
authed(bt).when().post(s"/api/tournament/$id/withdraw")
.`then`().statusCode(200)
.body("ok", is(true))
@Test
@@ -228,12 +201,8 @@ class TournamentResourceTest:
@Test
def resultsReturns200WithNdjsonContentType(): Unit =
val id = createTournament(directorToken("dir-res"), "ResTour")
RestAssured
.`given`()
.when()
.get(s"/api/tournament/$id/results")
.`then`()
.statusCode(200)
RestAssured.`given`().when().get(s"/api/tournament/$id/results")
.`then`().statusCode(200)
.contentType("application/x-ndjson")
@Test
@@ -257,11 +226,8 @@ class TournamentResourceTest:
@Test
def returnsNdjsonWhenAcceptApplicationXNdjson(): Unit =
val id = createTournament(directorToken("dir-ndjson"), "NdjsonTour")
RestAssured
.`given`()
RestAssured.`given`()
.header("Accept", "application/x-ndjson")
.when()
.get(s"/api/tournament/$id/export/games")
.`then`()
.statusCode(200)
.when().get(s"/api/tournament/$id/export/games")
.`then`().statusCode(200)
.contentType("application/x-ndjson")
@@ -6,12 +6,7 @@ import org.junit.jupiter.api.Assertions.*
class SwissPairingServiceTest:
private def makeParticipant(
botId: String,
botName: String,
points: Double = 0.0,
byeCount: Int = 0,
): TournamentParticipant =
private def makeParticipant(botId: String, botName: String, points: Double = 0.0, byeCount: Int = 0): TournamentParticipant =
val p = new TournamentParticipant()
p.botId = botId
p.botName = botName
@@ -21,21 +16,21 @@ class SwissPairingServiceTest:
@Test
def pairs2PlayersRandomlyAssignsColors(): Unit =
val p1 = makeParticipant("b1", "BotOne")
val p2 = makeParticipant("b2", "BotTwo")
val p1 = makeParticipant("b1", "BotOne")
val p2 = makeParticipant("b2", "BotTwo")
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2), Nil)
assertEquals(1, pairs.size)
assertTrue(bye.isEmpty)
val (white, black) = pairs.head
val ids = Set(white.botId, black.botId)
val ids = Set(white.botId, black.botId)
assertEquals(Set("b1", "b2"), ids)
@Test
def pairs4PlayersTopVsEachOther(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.0)
val p4 = makeParticipant("b4", "D", points = 0.0)
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.0)
val p4 = makeParticipant("b4", "D", points = 0.0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3, p4), Nil)
assertEquals(2, pairs.size)
assertTrue(bye.isEmpty)
@@ -46,9 +41,9 @@ class SwissPairingServiceTest:
@Test
def oddCountLowestRankedGetsBye(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.0)
val p3 = makeParticipant("b3", "C", points = 0.0)
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.0)
val p3 = makeParticipant("b3", "C", points = 0.0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil)
assertEquals(1, pairs.size)
assertTrue(bye.isDefined)
@@ -56,22 +51,22 @@ class SwissPairingServiceTest:
@Test
def avoidsRematchSwapsWhenPairAlreadyPlayed(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.5)
val p4 = makeParticipant("b4", "D", points = 0.0)
val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.5)
val p4 = makeParticipant("b4", "D", points = 0.0)
val pastPairing = new TournamentPairing()
pastPairing.whiteId = "b1"
pastPairing.blackId = "b2"
val (pairs, _) = SwissPairingService.computePairings(List(p1, p2, p3, p4), List(pastPairing))
val pair1Ids = Set(pairs(0)._1.botId, pairs(0)._2.botId)
val pair1Ids = Set(pairs(0)._1.botId, pairs(0)._2.botId)
assertFalse(pair1Ids == Set("b1", "b2"), "b1 and b2 should not be paired again")
@Test
def playerWithFewerByesGetsTheByeFirst(): Unit =
val p1 = makeParticipant("b1", "A", points = 1.0, byeCount = 1)
val p2 = makeParticipant("b2", "B", points = 0.5, byeCount = 0)
val p3 = makeParticipant("b3", "C", points = 0.0, byeCount = 0)
val p1 = makeParticipant("b1", "A", points = 1.0, byeCount = 1)
val p2 = makeParticipant("b2", "B", points = 0.5, byeCount = 0)
val p3 = makeParticipant("b3", "C", points = 0.0, byeCount = 0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil)
assertEquals(1, pairs.size)
assertTrue(bye.isDefined)
-3
View File
@@ -1,3 +0,0 @@
MAJOR=0
MINOR=1
PATCH=0
-55
View File
@@ -204,58 +204,3 @@
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-09)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([e5fe7d0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5fe7d07a58e018151bb24f4ee37c06e72608ded))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
### Bug Fixes
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-10)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([e5fe7d0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5fe7d07a58e018151bb24f4ee37c06e72608ded))
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
### Bug Fixes
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
@@ -2,18 +2,15 @@ package de.nowchess.ws.resource
import de.nowchess.ws.config.RedisConfig
import io.quarkus.redis.datasource.RedisDataSource
import io.quarkus.redis.datasource.stream.{XAddArgs, XGroupCreateArgs, XReadGroupArgs}
import io.quarkus.redis.datasource.pubsub.PubSubCommands
import io.quarkus.websockets.next.*
import io.smallrye.jwt.auth.principal.JWTParser
import jakarta.inject.Inject
import org.eclipse.microprofile.context.ManagedExecutor
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
import java.time.Duration
import java.util.UUID
import scala.util.Try
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
@WebSocket(path = "/api/user/ws")
class UserWebSocketResource:
@@ -21,22 +18,20 @@ class UserWebSocketResource:
private val log = Logger.getLogger(classOf[UserWebSocketResource])
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
@Inject var jwtParser: JWTParser = uninitialized
@Inject var executor: ManagedExecutor = uninitialized
@Inject
var redis: RedisDataSource = uninitialized
@Inject
var redisConfig: RedisConfig = uninitialized
@Inject
var jwtParser: JWTParser = uninitialized
// scalafix:on DisableSyntax.var
private val consumerId = UUID.randomUUID().toString
private val maxRetries = 3
private val maxStreamLen = 1000L
private val connections = new ConcurrentHashMap[String, (String, PubSubCommands.RedisSubscriber)]()
private val connections = new ConcurrentHashMap[String, (String, WebSocketConnection)]()
private def userStreamKey(userId: String): String =
s"${redisConfig.prefix}:user:$userId:events:stream"
private def dlqKey: String = s"${redisConfig.prefix}:dlq"
private def userTopic(userId: String): String =
s"${redisConfig.prefix}:user:$userId:events"
@OnOpen
def onOpen(connection: WebSocketConnection, handshake: HandshakeRequest): Unit =
@@ -50,81 +45,16 @@ class UserWebSocketResource:
log.warn("WebSocket opened with no valid JWT — closing connection")
connection.close().subscribe().`with`(_ => (), _ => ())
case Some(userId) =>
log.infof("User WebSocket opened — userId=%s connId=%s", userId, connection.id())
createGroupIfAbsent(userId, connection.id())
connections.put(connection.id(), (userId, connection))
executor.submit(
new Runnable:
def run(): Unit = pollLoop(connection.id(), userId, connection),
)
log.infof("User WebSocket opened — userId=%s", userId)
val handler: Consumer[String] = msg => connection.sendText(msg).subscribe().`with`(_ => (), _ => ())
val subscriber = redis.pubsub(classOf[String]).subscribe(userTopic(userId), handler)
connections.put(connection.id(), (userId, subscriber))
val connectedMsg = s"""{"type":"CONNECTED","userId":"$userId"}"""
connection.sendText(connectedMsg).subscribe().`with`(_ => (), _ => ())
@OnClose
def onClose(connection: WebSocketConnection): Unit =
log.infof("User WebSocket closed — connectionId=%s", connection.id())
val userIdOpt = Option(connections.remove(connection.id())).map(_._1)
userIdOpt.foreach { userId =>
Try(redis.stream(classOf[String]).xgroupDestroy(userStreamKey(userId), connection.id())) match
case Failure(ex) => log.warnf(ex, "Failed to destroy consumer group for connectionId=%s", connection.id())
case Success(_) => ()
Option(connections.remove(connection.id())).foreach { (userId, subscriber) =>
subscriber.unsubscribe(userTopic(userId))
}
private def createGroupIfAbsent(userId: String, groupName: String): Unit =
Try(
redis
.stream(classOf[String])
.xgroupCreate(userStreamKey(userId), groupName, "$", new XGroupCreateArgs().mkstream()),
) match
case Failure(ex) if Option(ex.getMessage).exists(_.contains("BUSYGROUP")) => ()
case Failure(ex) => log.warnf(ex, "Failed to create consumer group for userId=%s", userId)
case Success(_) => ()
private def pollLoop(connectionId: String, userId: String, myConnection: WebSocketConnection): Unit =
while Option(connections.get(connectionId)).exists(_._2 eq myConnection) do
Try {
val messages = redis
.stream(classOf[String])
.xreadgroup(
connectionId,
consumerId,
userStreamKey(userId),
">",
new XReadGroupArgs().count(10).block(Duration.ofSeconds(2)),
)
Option(messages).foreach(_.forEach { msg =>
if Option(connections.get(connectionId)).exists(_._2 eq myConnection) then
val json = msg.payload().get("data")
val attempt = Option(msg.payload().get("attempt")).flatMap(_.toIntOption).getOrElse(0)
Try(myConnection.sendText(json).await().atMost(Duration.ofSeconds(5))) match
case Success(_) =>
ack(connectionId, userId, msg.id())
case Failure(_) if attempt + 1 < maxRetries =>
xadd(userStreamKey(userId), json, attempt + 1)
ack(connectionId, userId, msg.id())
case Failure(ex) =>
log.warnf(ex, "Delivery failed for userId=%s after %d attempts, sending to DLQ", userId, maxRetries)
xadd(dlqKey, json, attempt)
ack(connectionId, userId, msg.id())
})
} match
case Failure(ex) => log.warnf(ex, "Error in poll loop for userId=%s", userId)
case Success(_) => ()
private def ack(groupName: String, userId: String, id: String): Unit =
Try(redis.stream(classOf[String]).xack(userStreamKey(userId), groupName, id)) match
case Failure(ex) => log.warnf(ex, "Failed to ack message %s for userId=%s", id, userId)
case Success(_) => ()
private def xadd(key: String, json: String, attempt: Int): Unit =
Try(
redis
.stream(classOf[String])
.xadd(
key,
new XAddArgs().maxlen(maxStreamLen).nearlyExactTrimming(),
Map("data" -> json, "attempt" -> attempt.toString).asJava,
),
) match
case Failure(ex) => log.warnf(ex, "Failed to publish to stream %s", key)
case Success(_) => ()
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=16
MINOR=14
PATCH=0