Compare commits

..

4 Commits

Author SHA1 Message Date
TeamCity d9f30f0bfe ci: bump version with Build-146 2026-06-23 18:45:05 +00:00
Janis 1f4e9c8498 fix(tournament): sync native-server participants and route start (#78)
Build & Test (NowChessSystems) TeamCity build finished
Bots joining a published tournament directly on the native server were not
reflected in NowChess (0 players) and the tournament could not be started,
because create() kept a local copy plus a separate native copy whose id was
discarded — leaving the two records disconnected.

- Capture the native tournament id: createNative/publishNative now return the
  id instead of Boolean; persist it on Tournament.nativeTournamentId.
- Reverse-sync on read: get()/list() overlay nbPlayers/standing/status/round/
  winner from the native twin (with a fullName backfill for tournaments created
  before the id was captured).
- start(): proxy to the native twin (director token via authFor) so the native
  participants are used; mirror the started status locally.
- Skip the native server in the replicate loop (it has no /replicate endpoint),
  removing the per-create "Failed to replicate" warning.
- Isolate native integration in tournament unit tests (native-server-url no
  longer defaults to the live server).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com>
Reviewed-on: #78
2026-06-23 20:34:30 +02:00
TeamCity e2b13c0c8f ci: bump version with Build-145 2026-06-23 13:18:03 +00:00
Janis bfb15c7299 fix(official-bots): play games by polling state instead of NDJSON stream
Build & Test (NowChessSystems) TeamCity build finished
In the native image the JAX-RS client buffers streaming responses, so reading
the NDJSON game stream blocks forever — the bot discovered its game ("Playing
game …") but never saw its turn and never moved, with no error. Replace the
game-stream consumer with a poll loop over plain GET game-state calls (which
work natively): when it is our turn, compute and submit. Drop the now-unused
stream consumer, move helper, and game-stream opener. Auto-join no longer
spawns per-tournament event-stream threads; polling handles discovery + play.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:09:39 +02:00
10 changed files with 196 additions and 97 deletions
+45
View File
@@ -752,3 +752,48 @@
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-23)
### 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))
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* **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))
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
* **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))
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
@@ -43,6 +43,9 @@ class TournamentBotGamePlayer:
private val hardestDifficulty = "expert"
private val autoJoinIntervalMs = 15000L
private val gameTerminalStatuses =
Set("checkmate", "stalemate", "draw", "resigned", "timeout", "aborted", "finished")
// scalafix:off DisableSyntax.var
@volatile private var running = true
@volatile private var autoJoinToken: Option[String] = None
@@ -87,8 +90,7 @@ class TournamentBotGamePlayer:
open.foreach { tournamentId =>
if joinedTournaments.add(tournamentId) then
val cfg = TournamentBotConfig(autoJoinServerUrl, tournamentId, token, botId, hardestDifficulty)
if joinedOrParticipating(cfg) then startAsync(cfg)
else joinedTournaments.remove(tournamentId)
if !joinedOrParticipating(cfg) then joinedTournaments.remove(tournamentId)
}
playPendingGames(token, botId)
}
@@ -370,58 +372,36 @@ class TournamentBotGamePlayer:
private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
Try {
log.infof("Playing game %s as %s", gameId, color)
openGameStream(cfg, gameId).foreach(consumeGameStream(cfg, gameId, color, _))
pollGameLoop(cfg, gameId, color)
activeGames.remove(gameId)
} match
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
case Success(_) => ()
private def consumeGameStream(cfg: TournamentBotConfig, gameId: String, color: String, stream: InputStream): Unit =
val reader = new BufferedReader(new InputStreamReader(stream))
// The native JAX-RS client buffers streaming responses, so reading the NDJSON game stream blocks
// forever. Poll the game state with plain GETs (which work) and move when it is our turn.
private def pollGameLoop(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
// scalafix:off DisableSyntax.var
var done = false
var done = false
var lastFen = ""
// 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 "gameState" =>
maybeMove(
cfg,
gameId,
color,
node.path("turn").asText(),
node.path("status").asText(),
node.path("fen").asText(),
)
case "move" =>
maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText())
case "gameEnd" =>
log.infof(
"Game %s ended — status=%s winner=%s",
gameId,
node.path("status").asText(),
node.path("winner").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)
while running && !done do
fetchJson(cfg, target(cfg).path("game").path(gameId)) match
case None => sleep(2000)
case Some(node) =>
val status = node.path("status").asText()
if gameTerminalStatuses.contains(status) then
log.infof("Game %s ended — status=%s", gameId, status); done = true
else
val turn = node.path("turn").asText()
val fen = node.path("fen").asText()
if turn == color && status == "ongoing" && fen.nonEmpty && fen != lastFen then
lastFen = fen
log.infof("Our turn in game %s — computing move (fen=%s)", gameId, fen)
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)
sleep(1000)
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
FenParser.parseFen(fen) match
@@ -439,15 +419,6 @@ class TournamentBotGamePlayer:
case Failure(ex) => log.errorf(ex, "Error submitting move %s in game %s", uci, gameId)
case Success(_) => ()
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
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=31
MINOR=32
PATCH=0
+21
View File
@@ -94,3 +94,24 @@
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
## (2026-06-23)
### Features
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* NCS-121 pipeline for tournament ([#68](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/68)) ([145f467](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/145f4676483f92bfe6f2d9ca40e2cb4200982e87))
* 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 GameWritebackEventDto to native reflection configuration ([1aee39c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1aee39c1ad286984501ac4b47da2b72d60b58a6f))
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** remove dynamic server add/remove endpoints ([6d06edd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6d06edda69a50de65cd9efa27f26a4cc6b437f9d))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
### Bug Fixes
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
* **tournament:** sync native-server participants and route start ([#78](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/78)) ([1f4e9c8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1f4e9c8498f55d95ab48758df60c7618445bf6ca))
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
@@ -7,7 +7,7 @@ import java.time.Instant
@Entity
@Table(name = "tournaments")
class Tournament:
// scalafix:off DisableSyntax.var
// scalafix:off
@Id
var id: String = uninitialized
@@ -33,4 +33,7 @@ class Tournament:
@Column(nullable = true)
var originServerUrl: String = null
@Column(nullable = true)
var nativeTournamentId: String = null
// scalafix:on
@@ -52,9 +52,9 @@ class TournamentResource:
@PermitAll
def list(): Response =
val (created, started, finished) = tournamentService.list()
val internalCreated = created.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
val internalStarted = started.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
val internalFinished = finished.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
val internalCreated = created.map(nativeOverlay)
val internalStarted = started.map(nativeOverlay)
val internalFinished = finished.map(nativeOverlay)
val (extCreated, extStarted, extFinished) = registry
.serverUrls()
@@ -96,7 +96,7 @@ class TournamentResource:
val form = CreateTournamentForm(name, nbRounds, clockLimit, clockIncrement, rated)
val t = tournamentService.create(userId, form)
selfUrl.ifPresent { url =>
registry.serverUrls().foreach { remoteUrl =>
registry.serverUrls().filterNot(externalClient.isNativeServer).foreach { remoteUrl =>
if !externalClient.replicateTournament(remoteUrl, toReplicateRequest(t), url) then
log.warnf("Failed to replicate tournament %s to %s", t.id, remoteUrl)
}
@@ -115,8 +115,42 @@ class TournamentResource:
"rated" -> t.rated.toString,
),
)
if !externalClient.publishNative(nativeServerUrl, directorName, form) then
log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
externalClient.publishNative(nativeServerUrl, directorName, form) match
case Some(nativeId) => tournamentService.setNativeTournamentId(t.id, nativeId)
case None => log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
// Resolve the native-server twin of a local tournament. Backfills the stored id by matching
// fullName against the native list for tournaments created before the id was captured.
private def nativeIdFor(t: de.nowchess.tournament.domain.Tournament): Option[String] =
if nativeServerUrl.isEmpty then None
else
Option(t.nativeTournamentId).filter(_.nonEmpty).orElse {
val found = externalClient
.fetchList(nativeServerUrl)
.flatMap { node =>
Seq("created", "started", "finished").iterator
.flatMap(k => node.path(k).elements().asScala)
.find(_.path("fullName").asText() == t.fullName)
.map(_.path("id").asText())
.filter(_.nonEmpty)
}
found.foreach(id => tournamentService.setNativeTournamentId(t.id, id))
found
}
// Overlay live participant/standings/status fields from the native twin onto a local DTO so
// bots that joined directly on the native server are reflected in NowChess.
private def nativeOverlay(t: de.nowchess.tournament.domain.Tournament): JsonNode =
val standings = tournamentService.getStandings(t.id)
val dto = objectMapper.valueToTree[JsonNode](tournamentService.toDto(t, standings))
nativeIdFor(t).flatMap(nid => externalClient.fetch(nativeServerUrl, nid)) match
case Some(native) =>
val merged = dto.deepCopy[com.fasterxml.jackson.databind.node.ObjectNode]()
Seq("nbPlayers", "standing", "status", "round", "winner").foreach { field =>
if native.has(field) then merged.set(field, native.get(field))
}
merged
case None => dto
private def encodeForm(params: Map[String, String]): String =
params
@@ -132,8 +166,7 @@ class TournamentResource:
def get(@PathParam("id") id: String): Response =
tournamentService.get(id) match
case Some(t) =>
val standings = tournamentService.getStandings(id)
Response.ok(tournamentService.toDto(t, standings)).build()
Response.ok(nativeOverlay(t)).build()
case None =>
resolveServer(id)
.flatMap(url => externalClient.fetch(url, id).map(node => Response.ok(node).build()))
@@ -179,19 +212,26 @@ class TournamentResource:
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id/start", auth)
Response.status(status).entity(body).build()
case None =>
tournamentService.start(id, userId) match
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
case Left(error) =>
error match
case TournamentError.NotFound(_) =>
val auth = Option(headers.getHeaderString("Authorization"))
resolveServer(id)
.map { url =>
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
Response.status(status).entity(body).build()
}
.getOrElse(errorResponse(error))
case _ => errorResponse(error)
tournamentService.get(id).flatMap(nativeIdFor) match
case Some(nativeId) =>
val auth = Option(headers.getHeaderString("Authorization"))
val (status, body) = externalClient.proxyPost(nativeServerUrl, s"api/tournament/$nativeId/start", auth)
if status / 100 == 2 then tournamentService.markStatus(id, "started")
Response.status(status).entity(body).build()
case None =>
tournamentService.start(id, userId) match
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
case Left(error) =>
error match
case TournamentError.NotFound(_) =>
val auth = Option(headers.getHeaderString("Authorization"))
resolveServer(id)
.map { url =>
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
Response.status(status).entity(body).build()
}
.getOrElse(errorResponse(error))
case _ => errorResponse(error)
@POST
@Path("/{id}/join")
@@ -317,7 +357,8 @@ class TournamentResource:
tournamentService.get(id) match
case Some(t) if Option(t.originServerUrl).isDefined =>
val auth = Option(headers.getHeaderString("Authorization"))
externalClient.proxyGetStream(t.originServerUrl, s"api/tournament/$id/stream", auth)
externalClient
.proxyGetStream(t.originServerUrl, s"api/tournament/$id/stream", auth)
.map { inputStream =>
Response
.ok(new StreamingOutput {
@@ -334,10 +375,12 @@ class TournamentResource:
.`type`("application/x-ndjson")
.build()
}
.getOrElse(Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id stream unavailable")).build())
.getOrElse(
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id stream unavailable")).build(),
)
case Some(_) =>
val botId = Option(jwt.getSubject).getOrElse("")
val queue = new java.util.concurrent.LinkedBlockingQueue[Option[String]]()
val queue = new java.util.concurrent.LinkedBlockingQueue[Option[String]]()
val emitter = new io.smallrye.mutiny.subscription.MultiEmitter[String] {
def emit(item: String): io.smallrye.mutiny.subscription.MultiEmitter[String] =
queue.put(Some(item)); this
@@ -358,7 +401,7 @@ class TournamentResource:
var cont = true
while cont do
queue.take() match
case None => cont = false
case None => cont = false
case Some(line) =>
output.write((line + "\n").getBytes("UTF-8"))
output.flush()
@@ -440,7 +483,8 @@ class TournamentResource:
.getOrElse(Response.status(Response.Status.NOT_FOUND).build())
private def resolveServer(tournamentId: String): Option[String] =
tournamentService.get(tournamentId)
tournamentService
.get(tournamentId)
.flatMap(t => Option(t.originServerUrl))
.orElse(registry.findServerUrl(tournamentId))
.orElse {
@@ -14,7 +14,7 @@ import scala.util.Try
class ExternalTournamentClient:
// scalafix:off DisableSyntax.var
@Inject var objectMapper: ObjectMapper = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized
@volatile private var directorToken: Option[String] = None
@ConfigProperty(name = "nowchess.tournament.native-server-url", defaultValue = "http://141.37.123.132:8086")
@@ -30,7 +30,7 @@ class ExternalTournamentClient:
// RS256 user token to it — swap in the director token registered on that server.
private def normalize(url: String): String = url.stripSuffix("/")
private def isNativeServer(serverUrl: String): Boolean =
def isNativeServer(serverUrl: String): Boolean =
nativeServerUrl.nonEmpty && normalize(serverUrl) == normalize(nativeServerUrl)
private def directorBearer(): Option[String] =
@@ -45,18 +45,18 @@ class ExternalTournamentClient:
private def authFor(serverUrl: String, userAuth: Option[String]): Option[String] =
if isNativeServer(serverUrl) then directorBearer() else userAuth
def publishNative(serverUrl: String, directorName: String, form: String): Boolean =
def publishNative(serverUrl: String, directorName: String, form: String): Option[String] =
val token = directorToken.orElse {
val fresh = registerDirector(serverUrl, directorName)
directorToken = fresh
fresh
}
token.exists { tok =>
if createNative(serverUrl, tok, form) then true
else
token.flatMap { tok =>
createNative(serverUrl, tok, form).orElse {
val refreshed = registerDirector(serverUrl, directorName)
directorToken = refreshed
refreshed.exists(createNative(serverUrl, _, form))
refreshed.flatMap(createNative(serverUrl, _, form))
}
}
private def registerDirector(serverUrl: String, name: String): Option[String] =
@@ -76,7 +76,7 @@ class ExternalTournamentClient:
client.close()
}.getOrElse(None)
private def createNative(serverUrl: String, token: String, form: String): Boolean =
private def createNative(serverUrl: String, token: String, form: String): Option[String] =
Try {
val client = buildClient()
val response = client
@@ -84,11 +84,14 @@ class ExternalTournamentClient:
.request(MediaType.APPLICATION_JSON)
.header("Authorization", s"Bearer $token")
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED))
try response.getStatus / 100 == 2
try
if response.getStatus / 100 == 2 then
Option(objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText()).filter(_.nonEmpty)
else None
finally
response.close()
client.close()
}.getOrElse(false)
}.getOrElse(None)
def fetchList(serverUrl: String): Option[JsonNode] =
Try {
@@ -141,8 +144,8 @@ class ExternalTournamentClient:
def replicateTournament(serverUrl: String, req: ReplicateTournamentRequest, selfUrl: String): Boolean =
Try {
val client = buildClient()
val body = objectMapper.writeValueAsString(req)
val client = buildClient()
val body = objectMapper.writeValueAsString(req)
val response = client
.target(s"$serverUrl/api/tournament/replicate")
.request(MediaType.APPLICATION_JSON)
@@ -82,6 +82,14 @@ class TournamentService:
def get(id: String): Option[Tournament] =
tournamentRepository.findOptById(id)
@Transactional
def setNativeTournamentId(id: String, nativeId: String): Unit =
tournamentRepository.findOptById(id).foreach(_.nativeTournamentId = nativeId)
@Transactional
def markStatus(id: String, status: String): Unit =
tournamentRepository.findOptById(id).foreach(_.status = status)
def list(): (List[Tournament], List[Tournament], List[Tournament]) =
(
tournamentRepository.findByStatus("created"),
@@ -30,3 +30,7 @@ nowchess:
secret: test-secret
auth:
enabled: false
tournament:
self-url: ""
external-servers: ""
native-server-url: "http://localhost:1"
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=7
MINOR=8
PATCH=0