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 OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
||||
|
||||
case class SyncOfficialBotsRequest(bots: List[String])
|
||||
|
||||
@@ -89,6 +89,13 @@ class OfficialBotAccountRepository:
|
||||
def findAll(): List[OfficialBotAccount] =
|
||||
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 =
|
||||
em.persist(bot)
|
||||
bot
|
||||
|
||||
@@ -4,6 +4,7 @@ import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
|
||||
import de.nowchess.account.dto.*
|
||||
import de.nowchess.account.error.AccountError
|
||||
import de.nowchess.account.service.AccountService
|
||||
import de.nowchess.security.InternalOnly
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
@@ -179,6 +180,13 @@ class AccountResource:
|
||||
createdAt = bot.createdAt.toString,
|
||||
)
|
||||
|
||||
@POST
|
||||
@Path("/official-bots/sync")
|
||||
@InternalOnly
|
||||
def syncOfficialBots(req: SyncOfficialBotsRequest): Response =
|
||||
accountService.syncOfficialBots(req.bots)
|
||||
Response.noContent().build()
|
||||
|
||||
@GET
|
||||
@Path("/official-bots")
|
||||
def getOfficialBots: Response =
|
||||
|
||||
@@ -206,6 +206,17 @@ class AccountService:
|
||||
officialBotAccountRepository.persist(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] =
|
||||
officialBotAccountRepository.findAll()
|
||||
|
||||
|
||||
@@ -154,3 +154,34 @@ class AccountResourceTest:
|
||||
.post("/api/account/refresh")
|
||||
.`then`()
|
||||
.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:io"))
|
||||
implementation(project(":modules:rule"))
|
||||
implementation(project(":modules:security"))
|
||||
implementation("io.quarkus:quarkus-rest-client-jackson")
|
||||
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
||||
implementation("io.quarkus:quarkus-redis-client")
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ quarkus:
|
||||
name: nowchess-official-bots
|
||||
redis:
|
||||
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
||||
rest-client:
|
||||
account-service:
|
||||
url: http://localhost:8083
|
||||
smallrye-jwt:
|
||||
enabled: true
|
||||
log:
|
||||
@@ -15,6 +18,8 @@ nowchess:
|
||||
host: localhost
|
||||
port: 6379
|
||||
prefix: nowchess
|
||||
internal:
|
||||
secret: 123abc
|
||||
|
||||
"%deployed":
|
||||
quarkus:
|
||||
@@ -28,8 +33,13 @@ nowchess:
|
||||
exporter:
|
||||
otlp:
|
||||
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
||||
rest-client:
|
||||
account-service:
|
||||
url: ${ACCOUNT_SERVICE_URL}
|
||||
nowchess:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
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.bot.BotController
|
||||
import de.nowchess.bot.BotDifficulty
|
||||
import de.nowchess.bot.client.{AccountServiceClient, SyncOfficialBotsRequest}
|
||||
import de.nowchess.bot.config.RedisConfig
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
@@ -13,6 +14,8 @@ import jakarta.annotation.PostConstruct
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.enterprise.event.Observes
|
||||
import jakarta.inject.Inject
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||
import org.jboss.logging.Logger
|
||||
import scala.compiletime.uninitialized
|
||||
import java.util.function.Consumer
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -20,12 +23,18 @@ import java.util.concurrent.TimeUnit
|
||||
@ApplicationScoped
|
||||
class OfficialBotService:
|
||||
|
||||
private val log = Logger.getLogger(classOf[OfficialBotService])
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var botController: BotController = uninitialized
|
||||
@Inject var meterRegistry: MeterRegistry = uninitialized
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
var accountServiceClient: AccountServiceClient = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val terminalStatuses =
|
||||
@@ -39,7 +48,10 @@ class OfficialBotService:
|
||||
}
|
||||
|
||||
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 =
|
||||
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
||||
|
||||
Reference in New Issue
Block a user