feat(redis): migrate from Redisson to Quarkus Redis client and update configuration
This commit is contained in:
@@ -74,7 +74,7 @@ dependencies {
|
||||
implementation(project(":modules:io"))
|
||||
implementation(project(":modules:rule"))
|
||||
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
|
||||
implementation("org.redisson:redisson:${versions["REDISSON"]!!}")
|
||||
implementation("io.quarkus:quarkus-redis-client")
|
||||
|
||||
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
|
||||
@@ -3,6 +3,8 @@ quarkus:
|
||||
port: 8088
|
||||
application:
|
||||
name: nowchess-official-bots
|
||||
redis:
|
||||
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
||||
smallrye-jwt:
|
||||
enabled: true
|
||||
log:
|
||||
|
||||
@@ -7,12 +7,6 @@ import scala.compiletime.uninitialized
|
||||
@ApplicationScoped
|
||||
class RedisConfig:
|
||||
// scalafix:off DisableSyntax.var
|
||||
@ConfigProperty(name = "nowchess.redis.host", defaultValue = "localhost")
|
||||
var host: String = uninitialized
|
||||
|
||||
@ConfigProperty(name = "nowchess.redis.port", defaultValue = "6379")
|
||||
var port: Int = uninitialized
|
||||
|
||||
@ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess")
|
||||
var prefix: String = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.nowchess.bot.config
|
||||
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.enterprise.inject.Produces
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.Redisson
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.redisson.config.Config
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class RedissonProducer:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject
|
||||
var redisConfig: RedisConfig = uninitialized
|
||||
|
||||
private var clientOpt: Option[RedissonClient] = None
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
def produceRedissonClient(): RedissonClient =
|
||||
val config = new Config()
|
||||
config.useSingleServer().setAddress(s"redis://${redisConfig.host}:${redisConfig.port}")
|
||||
config.useSingleServer().setConnectionMinimumIdleSize(1)
|
||||
config.useSingleServer().setConnectTimeout(500)
|
||||
val client = Redisson.create(config)
|
||||
clientOpt = Some(client)
|
||||
client
|
||||
|
||||
@PreDestroy
|
||||
def shutdown(): Unit =
|
||||
clientOpt.foreach(_.shutdown())
|
||||
+8
-18
@@ -6,19 +6,19 @@ import de.nowchess.bot.BotController
|
||||
import de.nowchess.bot.BotDifficulty
|
||||
import de.nowchess.bot.config.RedisConfig
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import io.quarkus.runtime.StartupEvent
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.enterprise.event.Observes
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.redisson.api.listener.MessageListener
|
||||
import scala.compiletime.uninitialized
|
||||
import java.util.function.Consumer
|
||||
|
||||
@ApplicationScoped
|
||||
class OfficialBotService:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject var redisson: RedissonClient = uninitialized
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var botController: BotController = uninitialized
|
||||
@@ -31,13 +31,8 @@ class OfficialBotService:
|
||||
BotController.listBots.foreach(subscribeToEventChannel)
|
||||
|
||||
private def subscribeToEventChannel(botName: String): Unit =
|
||||
val topic = redisson.getTopic(s"${redisConfig.prefix}:bot:$botName:events")
|
||||
topic.addListener(
|
||||
classOf[String],
|
||||
new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, msg: String): Unit =
|
||||
handleBotEvent(botName, msg),
|
||||
)
|
||||
val handler: Consumer[String] = msg => handleBotEvent(botName, msg)
|
||||
redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:bot:$botName:events", handler)
|
||||
()
|
||||
|
||||
private def handleBotEvent(botName: String, msg: String): Unit =
|
||||
@@ -52,13 +47,8 @@ class OfficialBotService:
|
||||
catch case _: Exception => ()
|
||||
|
||||
private def watchGame(botName: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
|
||||
val topic = redisson.getTopic(s"${redisConfig.prefix}:game:$gameId:s2c")
|
||||
topic.addListener(
|
||||
classOf[String],
|
||||
new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, msg: String): Unit =
|
||||
handleGameEvent(botName, gameId, playingAs, difficulty, botAccountId, msg),
|
||||
)
|
||||
val handler: Consumer[String] = msg => handleGameEvent(botName, gameId, playingAs, difficulty, botAccountId, msg)
|
||||
redis.pubsub(classOf[String]).subscribe(s"${redisConfig.prefix}:game:$gameId:s2c", handler)
|
||||
()
|
||||
|
||||
private def handleGameEvent(
|
||||
@@ -87,7 +77,7 @@ class OfficialBotService:
|
||||
val uci = toUci(move)
|
||||
val c2sTopic = s"${redisConfig.prefix}:game:$gameId:c2s"
|
||||
val moveMsg = s"""{"type":"MOVE","uci":"$uci","playerId":"$botAccountId"}"""
|
||||
redisson.getTopic(c2sTopic).publish(moveMsg)
|
||||
redis.pubsub(classOf[String]).publish(c2sTopic, moveMsg)
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,11 @@ import org.scalatest.matchers.should.Matchers
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("name returns expected format"):
|
||||
val botEasy = ClassicalBot(BotDifficulty.Easy)
|
||||
botEasy.name should include("ClassicalBot")
|
||||
botEasy.name should include("Easy")
|
||||
|
||||
val botMedium = ClassicalBot(BotDifficulty.Medium)
|
||||
botMedium.name should include("Medium")
|
||||
|
||||
|
||||
test("nextMove on initial position returns a move"):
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("nextMove returns None for position with no legal moves"):
|
||||
@@ -39,13 +32,13 @@ class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should be(None)
|
||||
|
||||
test("all BotDifficulty values work"):
|
||||
BotDifficulty.values.foreach { difficulty =>
|
||||
val bot = ClassicalBot(difficulty)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
// All difficulties should return a move on the initial position
|
||||
move should not be None
|
||||
}
|
||||
@@ -70,7 +63,7 @@ class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should be(Some(moveToReturn))
|
||||
|
||||
test("nextMove skips a move repeated three times in a row"):
|
||||
@@ -95,4 +88,4 @@ class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
val context = GameContext.initial.copy(moves = List(repeatedMove, repeatedMove, repeatedMove))
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
|
||||
bot.nextMove(context) should be(None)
|
||||
bot.apply(context) should be(None)
|
||||
|
||||
@@ -17,17 +17,12 @@ import scala.util.Using
|
||||
|
||||
class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("HybridBot name includes difficulty"):
|
||||
val bot = HybridBot(BotDifficulty.Easy)
|
||||
bot.name should include("HybridBot")
|
||||
bot.name should include("Easy")
|
||||
|
||||
test("HybridBot nextMove returns a move on the initial position"):
|
||||
test("HybridBot apply returns a move on the initial position"):
|
||||
val bot = HybridBot(BotDifficulty.Easy)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("HybridBot nextMove returns None when no legal moves"):
|
||||
test("HybridBot apply returns None when no legal moves"):
|
||||
val noMovesRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
@@ -40,13 +35,13 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
val bot = HybridBot(BotDifficulty.Easy, noMovesRules)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should be(None)
|
||||
|
||||
test("HybridBot with empty book falls through to search"):
|
||||
val emptyBook = PolyglotBook("/nonexistent/book.bin")
|
||||
val bot = HybridBot(BotDifficulty.Easy, book = Some(emptyBook))
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("HybridBot skips move repeated three times"):
|
||||
@@ -64,7 +59,7 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
val ctx = GameContext.initial.copy(moves = List(repeatedMove, repeatedMove, repeatedMove))
|
||||
val bot = HybridBot(BotDifficulty.Easy, onlyMoveRules)
|
||||
bot.nextMove(ctx) should be(None)
|
||||
bot.apply(ctx) should be(None)
|
||||
|
||||
test("HybridBot uses book move when available"):
|
||||
val tempFile = Files.createTempFile("hybrid_book", ".bin")
|
||||
@@ -82,7 +77,7 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val bot = HybridBot(BotDifficulty.Easy, book = Some(book))
|
||||
bot.nextMove(ctx) should be(Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())))
|
||||
bot.apply(ctx) should be(Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())))
|
||||
finally Files.deleteIfExists(tempFile)
|
||||
|
||||
test("HybridBot reports veto when classical and NNUE differ above threshold"):
|
||||
@@ -119,7 +114,7 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
vetoReporter = _ => reported.set(true),
|
||||
)
|
||||
|
||||
bot.nextMove(GameContext.initial) should be(Some(forcedMove))
|
||||
bot.apply(GameContext.initial) should be(Some(forcedMove))
|
||||
reported.get should be(true)
|
||||
|
||||
test("HybridBot default veto reporter prints when threshold is exceeded"):
|
||||
@@ -155,6 +150,6 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
)
|
||||
|
||||
val printed = Console.withOut(new java.io.ByteArrayOutputStream()) {
|
||||
bot.nextMove(GameContext.initial)
|
||||
bot.apply(GameContext.initial)
|
||||
}
|
||||
printed should be(Some(forcedMove))
|
||||
|
||||
@@ -96,7 +96,7 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
test("ClassicalBot without book falls back to search"):
|
||||
val ctx = GameContext.initial
|
||||
val bot = ClassicalBot(BotDifficulty.Easy) // no book
|
||||
val move = bot.nextMove(ctx)
|
||||
val move = bot.apply(ctx)
|
||||
move shouldNot be(None)
|
||||
// The move should be legal
|
||||
val allLegalMoves = DefaultRules.allLegalMoves(ctx)
|
||||
@@ -120,7 +120,7 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val botWithBook = ClassicalBot(BotDifficulty.Easy, book = Some(book))
|
||||
val move = botWithBook.nextMove(ctx)
|
||||
val move = botWithBook.apply(ctx)
|
||||
|
||||
// Book should return e2-e4
|
||||
move shouldEqual Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()))
|
||||
|
||||
Reference in New Issue
Block a user