fix(official-bots): auto-register official bots with account service
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
On startup, OfficialBotService now calls POST /api/account/official-bots/sync (internal endpoint) to ensure all bots in BotController are registered in the account DB. Without this, OfficialChallengeResource returns 404 for all official bot challenges because no entries exist. The sync is idempotent: existing bots are skipped. Startup failure in the account service is logged but does not prevent the bot service from starting. Closes NCS-70 https://knockoutwhist.youtrack.cloud/issue/NCS-70 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,3 +49,5 @@ case class RotatedTokenDto(token: String)
|
|||||||
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
||||||
|
|
||||||
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
||||||
|
|
||||||
|
case class SyncOfficialBotsRequest(bots: List[String])
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ class OfficialBotAccountRepository:
|
|||||||
def findAll(): List[OfficialBotAccount] =
|
def findAll(): List[OfficialBotAccount] =
|
||||||
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
|
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
|
||||||
|
|
||||||
|
def findByName(name: String): Option[OfficialBotAccount] =
|
||||||
|
em.createQuery("FROM OfficialBotAccount WHERE name = :name", classOf[OfficialBotAccount])
|
||||||
|
.setParameter("name", name)
|
||||||
|
.getResultList
|
||||||
|
.asScala
|
||||||
|
.headOption
|
||||||
|
|
||||||
def persist(bot: OfficialBotAccount): OfficialBotAccount =
|
def persist(bot: OfficialBotAccount): OfficialBotAccount =
|
||||||
em.persist(bot)
|
em.persist(bot)
|
||||||
bot
|
bot
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
|
|||||||
import de.nowchess.account.dto.*
|
import de.nowchess.account.dto.*
|
||||||
import de.nowchess.account.error.AccountError
|
import de.nowchess.account.error.AccountError
|
||||||
import de.nowchess.account.service.AccountService
|
import de.nowchess.account.service.AccountService
|
||||||
|
import de.nowchess.security.InternalOnly
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
@@ -179,6 +180,13 @@ class AccountResource:
|
|||||||
createdAt = bot.createdAt.toString,
|
createdAt = bot.createdAt.toString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/official-bots/sync")
|
||||||
|
@InternalOnly
|
||||||
|
def syncOfficialBots(req: SyncOfficialBotsRequest): Response =
|
||||||
|
accountService.syncOfficialBots(req.bots)
|
||||||
|
Response.noContent().build()
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/official-bots")
|
@Path("/official-bots")
|
||||||
def getOfficialBots: Response =
|
def getOfficialBots: Response =
|
||||||
|
|||||||
@@ -206,6 +206,17 @@ class AccountService:
|
|||||||
officialBotAccountRepository.persist(bot)
|
officialBotAccountRepository.persist(bot)
|
||||||
Right(bot)
|
Right(bot)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def syncOfficialBots(botNames: List[String]): Unit =
|
||||||
|
botNames.foreach { name =>
|
||||||
|
if officialBotAccountRepository.findByName(name).isEmpty then
|
||||||
|
val bot = new OfficialBotAccount()
|
||||||
|
bot.name = name
|
||||||
|
bot.createdAt = Instant.now()
|
||||||
|
officialBotAccountRepository.persist(bot)
|
||||||
|
log.infof("Auto-registered official bot: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
||||||
officialBotAccountRepository.findAll()
|
officialBotAccountRepository.findAll()
|
||||||
|
|
||||||
|
|||||||
@@ -154,3 +154,34 @@ class AccountResourceTest:
|
|||||||
.post("/api/account/refresh")
|
.post("/api/account/refresh")
|
||||||
.`then`()
|
.`then`()
|
||||||
.statusCode(401)
|
.statusCode(401)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def syncOfficialBotsCreatesNewBots(): Unit =
|
||||||
|
givenRequest()
|
||||||
|
.body("""{"bots":["sync-easy","sync-hard"]}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
RestAssured.`given`()
|
||||||
|
.when()
|
||||||
|
.get("/api/account/official-bots")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("name", hasItems("sync-easy", "sync-hard"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def syncOfficialBotsIsIdempotent(): Unit =
|
||||||
|
val body = """{"bots":["idempotent-bot"]}"""
|
||||||
|
givenRequest()
|
||||||
|
.body(body)
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
givenRequest()
|
||||||
|
.body(body)
|
||||||
|
.when()
|
||||||
|
.post("/api/account/official-bots/sync")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(204)
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ dependencies {
|
|||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
implementation(project(":modules:io"))
|
implementation(project(":modules:io"))
|
||||||
implementation(project(":modules:rule"))
|
implementation(project(":modules:rule"))
|
||||||
|
implementation(project(":modules:security"))
|
||||||
|
implementation("io.quarkus:quarkus-rest-client-jackson")
|
||||||
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
||||||
implementation("io.quarkus:quarkus-redis-client")
|
implementation("io.quarkus:quarkus-redis-client")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ quarkus:
|
|||||||
name: nowchess-official-bots
|
name: nowchess-official-bots
|
||||||
redis:
|
redis:
|
||||||
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
||||||
|
rest-client:
|
||||||
|
account-service:
|
||||||
|
url: http://localhost:8083
|
||||||
smallrye-jwt:
|
smallrye-jwt:
|
||||||
enabled: true
|
enabled: true
|
||||||
log:
|
log:
|
||||||
@@ -15,6 +18,8 @@ nowchess:
|
|||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
prefix: nowchess
|
prefix: nowchess
|
||||||
|
internal:
|
||||||
|
secret: 123abc
|
||||||
|
|
||||||
"%deployed":
|
"%deployed":
|
||||||
quarkus:
|
quarkus:
|
||||||
@@ -28,8 +33,13 @@ nowchess:
|
|||||||
exporter:
|
exporter:
|
||||||
otlp:
|
otlp:
|
||||||
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
||||||
|
rest-client:
|
||||||
|
account-service:
|
||||||
|
url: ${ACCOUNT_SERVICE_URL}
|
||||||
nowchess:
|
nowchess:
|
||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:localhost}
|
host: ${REDIS_HOST:localhost}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
prefix: ${REDIS_PREFIX:nowchess}
|
prefix: ${REDIS_PREFIX:nowchess}
|
||||||
|
internal:
|
||||||
|
secret: ${INTERNAL_SECRET}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package de.nowchess.bot.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 SyncOfficialBotsRequest(bots: List[String])
|
||||||
|
|
||||||
|
@Path("/api/account/official-bots")
|
||||||
|
@RegisterRestClient(configKey = "account-service")
|
||||||
|
@RegisterProvider(classOf[InternalSecretClientFilter])
|
||||||
|
@RegisterClientHeaders(classOf[InternalClientHeadersFactory])
|
||||||
|
trait AccountServiceClient:
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/sync")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def syncBots(req: SyncOfficialBotsRequest): Unit
|
||||||
+13
-1
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.bot.BotController
|
import de.nowchess.bot.BotController
|
||||||
import de.nowchess.bot.BotDifficulty
|
import de.nowchess.bot.BotDifficulty
|
||||||
|
import de.nowchess.bot.client.{AccountServiceClient, SyncOfficialBotsRequest}
|
||||||
import de.nowchess.bot.config.RedisConfig
|
import de.nowchess.bot.config.RedisConfig
|
||||||
import de.nowchess.io.fen.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
import io.micrometer.core.instrument.MeterRegistry
|
||||||
@@ -13,6 +14,8 @@ import jakarta.annotation.PostConstruct
|
|||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.enterprise.event.Observes
|
import jakarta.enterprise.event.Observes
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
import org.jboss.logging.Logger
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -20,12 +23,18 @@ import java.util.concurrent.TimeUnit
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class OfficialBotService:
|
class OfficialBotService:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[OfficialBotService])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject var redis: RedisDataSource = uninitialized
|
@Inject var redis: RedisDataSource = uninitialized
|
||||||
@Inject var redisConfig: RedisConfig = uninitialized
|
@Inject var redisConfig: RedisConfig = uninitialized
|
||||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||||
@Inject var botController: BotController = uninitialized
|
@Inject var botController: BotController = uninitialized
|
||||||
@Inject var meterRegistry: MeterRegistry = uninitialized
|
@Inject var meterRegistry: MeterRegistry = uninitialized
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
var accountServiceClient: AccountServiceClient = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
private val terminalStatuses =
|
private val terminalStatuses =
|
||||||
@@ -39,7 +48,10 @@ class OfficialBotService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def onStart(@Observes event: StartupEvent): Unit =
|
def onStart(@Observes event: StartupEvent): Unit =
|
||||||
BotController.listBots.foreach(subscribeToEventChannel)
|
val bots = BotController.listBots
|
||||||
|
try accountServiceClient.syncBots(SyncOfficialBotsRequest(bots))
|
||||||
|
catch case ex: Exception => log.errorf(ex, "Failed to auto-register official bots with account service")
|
||||||
|
bots.foreach(subscribeToEventChannel)
|
||||||
|
|
||||||
private def subscribeToEventChannel(botName: String): Unit =
|
private def subscribeToEventChannel(botName: String): Unit =
|
||||||
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user