From 5d97c3c8b5cb3e755bcb89e5a22de58b2aa544de Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 28 Apr 2026 22:44:10 +0200 Subject: [PATCH] feat(redis): migrate from Redisson to Quarkus Redis client and update configuration --- build.gradle.kts | 5 +-- modules/account/build.gradle.kts | 2 +- .../src/main/resources/application.properties | 5 --- .../src/main/resources/application.yml | 2 + .../nowchess/account/config/RedisConfig.scala | 6 --- .../account/config/RedissonProducer.scala | 34 --------------- .../account/service/EventPublisher.scala | 10 ++--- .../src/test/resources/application.yml | 5 +++ modules/bot-platform/build.gradle.kts | 2 +- .../src/main/resources/application.yml | 2 + .../botplatform/config/RedisConfig.scala | 6 --- .../botplatform/config/RedissonProducer.scala | 35 --------------- .../botplatform/registry/BotRegistry.scala | 28 ++++++------ .../resource/BotEventResource.scala | 20 ++++----- modules/coordinator/build.gradle.kts | 2 +- .../src/main/resources/application.yml | 2 + .../coordinator/config/BeansProducer.scala | 20 --------- .../service/CacheEvictionManager.scala | 16 +++---- .../coordinator/service/FailoverService.scala | 14 +++--- .../coordinator/service/HealthMonitor.scala | 10 ++--- .../service/InstanceRegistry.scala | 9 ++-- .../coordinator/service/LoadBalancer.scala | 16 +++---- modules/core/build.gradle.kts | 2 +- .../core/src/main/resources/application.yml | 2 + .../nowchess/chess/config/RedisConfig.scala | 6 --- .../chess/config/RedissonProducer.scala | 35 --------------- .../chess/redis/GameRedisPublisher.scala | 6 +-- .../redis/GameRedisSubscriberManager.scala | 43 ++++++++----------- .../chess/registry/RedisGameRegistry.scala | 22 +++------- .../service/InstanceHeartbeatService.scala | 25 +++++------ .../core/src/test/resources/application.yml | 4 ++ ...cala => MockRedisDataSourceProducer.scala} | 8 ++-- .../GameResourceIntegrationTest.scala | 2 +- modules/io/src/test/resources/application.yml | 5 +++ modules/official-bots/build.gradle.kts | 2 +- .../src/main/resources/application.yml | 2 + .../de/nowchess/bot/config/RedisConfig.scala | 6 --- .../bot/config/RedissonProducer.scala | 35 --------------- .../bot/service/OfficialBotService.scala | 26 ++++------- .../de/nowchess/bot/ClassicalBotTest.scala | 19 +++----- .../scala/de/nowchess/bot/HybridBotTest.scala | 23 ++++------ .../de/nowchess/bot/PolyglotBookTest.scala | 4 +- .../rule/src/test/resources/application.yml | 5 +++ .../security/InternalAuthFilter.scala | 12 ++++-- .../InternalGrpcAuthInterceptor.scala | 7 ++- modules/store/build.gradle.kts | 2 +- .../store/src/main/resources/application.yml | 2 + .../nowchess/store/config/RedisConfig.scala | 8 +--- .../store/config/RedissonProducer.scala | 30 ------------- .../redis/GameWritebackStreamListener.scala | 15 +++---- modules/ws/build.gradle.kts | 2 +- modules/ws/src/main/resources/application.yml | 2 + .../ws/config/NativeReflectionConfig.scala | 6 +-- .../de/nowchess/ws/config/RedisConfig.scala | 6 --- .../nowchess/ws/config/RedissonProducer.scala | 35 --------------- .../nowchess/ws/resource/ConnectionMeta.scala | 4 +- .../ws/resource/GameWebSocketResource.scala | 25 +++++------ .../ws/resource/UserWebSocketResource.scala | 24 +++++------ 58 files changed, 209 insertions(+), 504 deletions(-) delete mode 100644 modules/account/src/main/resources/application.properties delete mode 100644 modules/account/src/main/scala/de/nowchess/account/config/RedissonProducer.scala delete mode 100644 modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedissonProducer.scala delete mode 100644 modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala rename modules/core/src/test/scala/de/nowchess/chess/config/{MockRedissonProducer.scala => MockRedisDataSourceProducer.scala} (60%) create mode 100644 modules/io/src/test/resources/application.yml delete mode 100644 modules/official-bots/src/main/scala/de/nowchess/bot/config/RedissonProducer.scala create mode 100644 modules/rule/src/test/resources/application.yml delete mode 100644 modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala delete mode 100644 modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala diff --git a/build.gradle.kts b/build.gradle.kts index b30d23f..af70e8f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ val coverageExclusions = listOf( "**/account/src/main/scala/de/nowchess/account/resource/**", // JacksonConfig / NativeReflectionConfig — Quarkus lifecycle hooks, no testable logic "**/account/src/main/scala/de/nowchess/account/config/**", - // WebSocket service — infrastructure CDI beans (RedisConfig, RedissonProducer) + // WebSocket service — infrastructure CDI beans (RedisConfig) "**/ws/src/main/scala/de/nowchess/ws/config/**", // GameWebSocketResource in core — replaced by ws module "**/core/src/main/scala/de/nowchess/chess/resource/GameWebSocketResource.scala", @@ -99,8 +99,7 @@ val versions = mapOf( "SCALA_PARSER_COMBINATORS" to "2.4.0", "FASTPARSE" to "3.0.2", "JACKSON" to "2.17.2", - "JACKSON_SCALA" to "2.17.2", - "REDISSON" to "3.32.0" + "JACKSON_SCALA" to "2.17.2" ) extra["VERSIONS"] = versions diff --git a/modules/account/build.gradle.kts b/modules/account/build.gradle.kts index 8c350d7..7589e77 100644 --- a/modules/account/build.gradle.kts +++ b/modules/account/build.gradle.kts @@ -62,7 +62,7 @@ dependencies { implementation("io.quarkus:quarkus-micrometer") implementation("io.quarkus:quarkus-smallrye-openapi") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - implementation("org.redisson:redisson:${versions["REDISSON"]!!}") + implementation("io.quarkus:quarkus-redis-client") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/modules/account/src/main/resources/application.properties b/modules/account/src/main/resources/application.properties deleted file mode 100644 index 62c410b..0000000 --- a/modules/account/src/main/resources/application.properties +++ /dev/null @@ -1,5 +0,0 @@ -quarkus.native.additional-build-args=\ ---initialize-at-run-time=org.redisson,\ ---initialize-at-run-time=io.netty - -%native.quarkus.arc.exclude-types=org.redisson.* \ No newline at end of file diff --git a/modules/account/src/main/resources/application.yml b/modules/account/src/main/resources/application.yml index b15eadd..d608b5a 100644 --- a/modules/account/src/main/resources/application.yml +++ b/modules/account/src/main/resources/application.yml @@ -3,6 +3,8 @@ quarkus: port: 8083 application: name: nowchess-account + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} rest-client: core-service: url: http://localhost:8080 diff --git a/modules/account/src/main/scala/de/nowchess/account/config/RedisConfig.scala b/modules/account/src/main/scala/de/nowchess/account/config/RedisConfig.scala index a85ad83..948bbf0 100644 --- a/modules/account/src/main/scala/de/nowchess/account/config/RedisConfig.scala +++ b/modules/account/src/main/scala/de/nowchess/account/config/RedisConfig.scala @@ -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 diff --git a/modules/account/src/main/scala/de/nowchess/account/config/RedissonProducer.scala b/modules/account/src/main/scala/de/nowchess/account/config/RedissonProducer.scala deleted file mode 100644 index bff2b14..0000000 --- a/modules/account/src/main/scala/de/nowchess/account/config/RedissonProducer.scala +++ /dev/null @@ -1,34 +0,0 @@ -package de.nowchess.account.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, TransportMode} - -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()) diff --git a/modules/account/src/main/scala/de/nowchess/account/service/EventPublisher.scala b/modules/account/src/main/scala/de/nowchess/account/service/EventPublisher.scala index 42e58a6..6e33f57 100644 --- a/modules/account/src/main/scala/de/nowchess/account/service/EventPublisher.scala +++ b/modules/account/src/main/scala/de/nowchess/account/service/EventPublisher.scala @@ -1,30 +1,30 @@ package de.nowchess.account.service import de.nowchess.account.config.RedisConfig +import io.quarkus.redis.datasource.RedisDataSource import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.RedissonClient import scala.compiletime.uninitialized @ApplicationScoped class EventPublisher: // scalafix:off DisableSyntax.var - @Inject var redisson: RedissonClient = uninitialized + @Inject var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized // scalafix:on DisableSyntax.var def publishGameStart(botId: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit = val event = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":$difficulty,"botAccountId":"$botAccountId"}""" - redisson.getTopic(s"${redisConfig.prefix}:bot:$botId:events").publish(event) + redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event) () def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit = val event = s"""{"type":"challengeCreated","challengeId":"$challengeId","challengerName":"$challengerName"}""" - redisson.getTopic(s"${redisConfig.prefix}:user:$destUserId:events").publish(event) + redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$destUserId:events", event) () def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit = val event = s"""{"type":"challengeAccepted","challengeId":"$challengeId","gameId":"$gameId"}""" - redisson.getTopic(s"${redisConfig.prefix}:user:$challengerId:events").publish(event) + redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$challengerId:events", event) () diff --git a/modules/account/src/test/resources/application.yml b/modules/account/src/test/resources/application.yml index 1db58d7..82fe950 100644 --- a/modules/account/src/test/resources/application.yml +++ b/modules/account/src/test/resources/application.yml @@ -29,3 +29,8 @@ smallrye: sign: key: location: keys/test-private.pem +nowchess: + internal: + secret: test-secret + auth: + enabled: false diff --git a/modules/bot-platform/build.gradle.kts b/modules/bot-platform/build.gradle.kts index 11f867b..e2963e1 100644 --- a/modules/bot-platform/build.gradle.kts +++ b/modules/bot-platform/build.gradle.kts @@ -62,7 +62,7 @@ dependencies { implementation("io.quarkus:quarkus-smallrye-health") implementation("io.quarkus:quarkus-smallrye-openapi") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - implementation("org.redisson:redisson:${versions["REDISSON"]!!}") + implementation("io.quarkus:quarkus-redis-client") implementation(project(":modules:api")) diff --git a/modules/bot-platform/src/main/resources/application.yml b/modules/bot-platform/src/main/resources/application.yml index 81db361..8367882 100644 --- a/modules/bot-platform/src/main/resources/application.yml +++ b/modules/bot-platform/src/main/resources/application.yml @@ -3,6 +3,8 @@ quarkus: port: 8087 application: name: nowchess-bot-platform + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} smallrye-jwt: enabled: true log: diff --git a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedisConfig.scala b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedisConfig.scala index d59dac1..e5a023e 100644 --- a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedisConfig.scala +++ b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedisConfig.scala @@ -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 diff --git a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedissonProducer.scala b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedissonProducer.scala deleted file mode 100644 index 6eb66d6..0000000 --- a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/config/RedissonProducer.scala +++ /dev/null @@ -1,35 +0,0 @@ -package de.nowchess.botplatform.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()) diff --git a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/registry/BotRegistry.scala b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/registry/BotRegistry.scala index 2f084f7..287c761 100644 --- a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/registry/BotRegistry.scala +++ b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/registry/BotRegistry.scala @@ -1,43 +1,39 @@ package de.nowchess.botplatform.registry import de.nowchess.botplatform.config.RedisConfig +import io.quarkus.redis.datasource.RedisDataSource +import io.quarkus.redis.datasource.pubsub.PubSubCommands import io.smallrye.mutiny.subscription.MultiEmitter import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.RedissonClient -import org.redisson.api.listener.MessageListener import scala.compiletime.uninitialized - import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer @ApplicationScoped class BotRegistry: // scalafix:off DisableSyntax.var - @Inject var redisson: RedissonClient = uninitialized + @Inject var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized // scalafix:on DisableSyntax.var - private val connections = ConcurrentHashMap[String, (MultiEmitter[?], Int)]() + private val connections = ConcurrentHashMap[String, (MultiEmitter[?], PubSubCommands.RedisSubscriber)]() def register(botId: String, emitter: MultiEmitter[? >: String]): Unit = - val topic = redisson.getTopic(s"${redisConfig.prefix}:bot:$botId:events") - val listenerId = topic.addListener( - classOf[String], - new MessageListener[String]: - def onMessage(channel: CharSequence, msg: String): Unit = - emitter.asInstanceOf[MultiEmitter[String]].emit(msg), - ) - connections.put(botId, (emitter, listenerId)) + val channel = s"${redisConfig.prefix}:bot:$botId:events" + val handler: Consumer[String] = msg => emitter.asInstanceOf[MultiEmitter[String]].emit(msg) + val subscriber = redis.pubsub(classOf[String]).subscribe(channel, handler) + connections.put(botId, (emitter, subscriber)) () def unregister(botId: String): Unit = - Option(connections.remove(botId)).foreach { (_, listenerId) => - redisson.getTopic(s"${redisConfig.prefix}:bot:$botId:events").removeListener(listenerId) + Option(connections.remove(botId)).foreach { (_, subscriber) => + subscriber.unsubscribe(s"${redisConfig.prefix}:bot:$botId:events") } def dispatch(botId: String, event: String): Unit = - redisson.getTopic(s"${redisConfig.prefix}:bot:$botId:events").publish(event) + redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event) () def registeredBots: List[String] = diff --git a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/resource/BotEventResource.scala b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/resource/BotEventResource.scala index 0e4406f..3875265 100644 --- a/modules/bot-platform/src/main/scala/de/nowchess/botplatform/resource/BotEventResource.scala +++ b/modules/bot-platform/src/main/scala/de/nowchess/botplatform/resource/BotEventResource.scala @@ -2,6 +2,7 @@ package de.nowchess.botplatform.resource import de.nowchess.botplatform.config.RedisConfig import de.nowchess.botplatform.registry.BotRegistry +import io.quarkus.redis.datasource.RedisDataSource import io.smallrye.mutiny.Multi import jakarta.annotation.security.RolesAllowed import jakarta.enterprise.context.ApplicationScoped @@ -9,9 +10,8 @@ import jakarta.inject.Inject import jakarta.ws.rs.* import jakarta.ws.rs.core.{MediaType, Response} import org.eclipse.microprofile.jwt.JsonWebToken -import org.redisson.api.RedissonClient -import org.redisson.api.listener.MessageListener import scala.compiletime.uninitialized +import java.util.function.Consumer @Path("/api/bot") @ApplicationScoped @@ -21,7 +21,7 @@ class BotEventResource: // scalafix:off DisableSyntax.var @Inject var registry: BotRegistry = uninitialized @Inject var jwt: JsonWebToken = uninitialized - @Inject var redisson: RedissonClient = uninitialized + @Inject var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized // scalafix:on DisableSyntax.var @@ -44,14 +44,10 @@ class BotEventResource: @Produces(Array(MediaType.SERVER_SENT_EVENTS)) def streamGame(@PathParam("gameId") gameId: String): Multi[String] = Multi.createFrom().emitter[String] { emitter => - val topicName = s"${redisConfig.prefix}:game:$gameId:s2c" - val topic = redisson.getTopic(topicName) - val listenerId = topic.addListener( - classOf[String], - new MessageListener[String]: - def onMessage(channel: CharSequence, msg: String): Unit = emitter.emit(msg), - ) - emitter.onTermination(() => topic.removeListener(listenerId)) + val topicName = s"${redisConfig.prefix}:game:$gameId:s2c" + val handler: Consumer[String] = msg => emitter.emit(msg) + val subscriber = redis.pubsub(classOf[String]).subscribe(topicName, handler) + emitter.onTermination(() => subscriber.unsubscribe(topicName)) } @POST @@ -63,5 +59,5 @@ class BotEventResource: ): Response = val playerId = Option(jwt.getSubject).getOrElse("") val moveMsg = s"""{"type":"MOVE","uci":"$uci","playerId":"$playerId"}""" - redisson.getTopic(s"${redisConfig.prefix}:game:$gameId:c2s").publish(moveMsg) + redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:game:$gameId:c2s", moveMsg) Response.ok().build() diff --git a/modules/coordinator/build.gradle.kts b/modules/coordinator/build.gradle.kts index 05d82bb..8da781b 100644 --- a/modules/coordinator/build.gradle.kts +++ b/modules/coordinator/build.gradle.kts @@ -72,7 +72,7 @@ dependencies { implementation("io.quarkus:quarkus-rest-client") implementation("io.quarkus:quarkus-rest-client-jackson") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - implementation("org.redisson:redisson:${versions["REDISSON"]!!}") + implementation("io.quarkus:quarkus-redis-client") implementation("io.fabric8:kubernetes-client:6.13.0") testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}")) diff --git a/modules/coordinator/src/main/resources/application.yml b/modules/coordinator/src/main/resources/application.yml index 0880dcb..83d3d87 100644 --- a/modules/coordinator/src/main/resources/application.yml +++ b/modules/coordinator/src/main/resources/application.yml @@ -3,6 +3,8 @@ quarkus: name: nowchess-coordinator http: port: 8086 + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} grpc: server: port: 9086 diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala index 757faf7..88fdc01 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala @@ -4,29 +4,9 @@ import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.inject.Produces import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.KubernetesClient -import org.redisson.Redisson -import org.redisson.api.RedissonClient -import org.redisson.config.Config -import org.eclipse.microprofile.config.inject.ConfigProperty -import jakarta.inject.Inject -import scala.compiletime.uninitialized @ApplicationScoped class BeansProducer: - @Inject - @ConfigProperty(name = "nowchess.redis.host", defaultValue = "localhost") - private var redisHost: String = uninitialized - - @Inject - @ConfigProperty(name = "nowchess.redis.port", defaultValue = "6379") - private var redisPort: Int = uninitialized - - @Produces - @ApplicationScoped - def redissonClient: RedissonClient = - val config = Config() - config.useSingleServer().setAddress(s"redis://$redisHost:$redisPort") - Redisson.create(config) @Produces @ApplicationScoped diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala index 6479e87..c45aa33 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala @@ -2,7 +2,7 @@ package de.nowchess.coordinator.service import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.RedissonClient +import io.quarkus.redis.datasource.RedisDataSource import de.nowchess.coordinator.config.CoordinatorConfig import com.fasterxml.jackson.databind.ObjectMapper import scala.jdk.CollectionConverters.* @@ -15,7 +15,7 @@ import de.nowchess.coordinator.grpc.CoreGrpcClient @ApplicationScoped class CacheEvictionManager: @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized @Inject private var config: CoordinatorConfig = uninitialized @@ -39,7 +39,7 @@ class CacheEvictionManager: log.info("Starting cache eviction scan") val pattern = s"$redisPrefix:game:entry:*" - val keys = redissonClient.getKeys.getKeysByPattern(pattern, 100) + val keys = redis.key(classOf[String]).keys(pattern) val now = System.currentTimeMillis() val idleThresholdMs = config.gameIdleThreshold.toMillis @@ -47,8 +47,7 @@ class CacheEvictionManager: var evictedCount = 0 keys.asScala.foreach { key => try - val bucket = redissonClient.getBucket[String](key) - val value = bucket.get() + val value = redis.value(classOf[String]).get(key) if value != null then val gameId = key.stripPrefix(s"$redisPrefix:game:entry:") val lastUpdated = extractLastUpdatedTimestamp(value) @@ -57,7 +56,7 @@ class CacheEvictionManager: findInstanceWithGame(gameId).foreach { instance => try coreGrpcClient.evictGames(instance.hostname, instance.grpcPort, List(gameId)) - bucket.delete() + redis.key(classOf[String]).del(key) evictedCount += 1 log.infof("Evicted idle game %s from %s", gameId, instance.instanceId) catch @@ -82,9 +81,8 @@ class CacheEvictionManager: private def findInstanceWithGame(gameId: String): Option[de.nowchess.coordinator.dto.InstanceMetadata] = try instanceRegistry.getAllInstances.find { instance => - val setKey = s"$redisPrefix:instance:${instance.instanceId}:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.contains(gameId) + val setKey = s"$redisPrefix:instance:${instance.instanceId}:games" + redis.set(classOf[String]).sismember(setKey, gameId) } catch case ex: Exception => diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala index 40f724d..dbcd965 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala @@ -2,7 +2,7 @@ package de.nowchess.coordinator.service import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.RedissonClient +import io.quarkus.redis.datasource.RedisDataSource import scala.jdk.CollectionConverters.* import scala.compiletime.uninitialized import org.jboss.logging.Logger @@ -12,7 +12,7 @@ import de.nowchess.coordinator.grpc.CoreGrpcClient @ApplicationScoped class FailoverService: @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized @Inject private var instanceRegistry: InstanceRegistry = uninitialized @@ -50,9 +50,8 @@ class FailoverService: cleanupDeadInstance(instanceId) private def getOrphanedGames(instanceId: String): List[String] = - val setKey = s"$redisPrefix:instance:$instanceId:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.readAll.asScala.toList + val setKey = s"$redisPrefix:instance:$instanceId:games" + redis.set(classOf[String]).smembers(setKey).asScala.toList private def distributeGames( gameIds: List[String], @@ -87,7 +86,6 @@ class FailoverService: } private def cleanupDeadInstance(instanceId: String): Unit = - val setKey = s"$redisPrefix:instance:$instanceId:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.delete() + val setKey = s"$redisPrefix:instance:$instanceId:games" + redis.key(classOf[String]).del(setKey) log.infof("Cleaned up games set for instance %s", instanceId) diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala index 93d0a4d..03c9361 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala @@ -5,7 +5,7 @@ import jakarta.inject.Inject import de.nowchess.coordinator.config.CoordinatorConfig import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.api.model.Pod -import org.redisson.api.RedissonClient +import io.quarkus.redis.datasource.RedisDataSource import scala.jdk.CollectionConverters.* import org.jboss.logging.Logger import scala.compiletime.uninitialized @@ -23,7 +23,7 @@ class HealthMonitor: private var instanceRegistry: InstanceRegistry = uninitialized @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized private val log = Logger.getLogger(classOf[HealthMonitor]) private var redisPrefix = "nowchess" @@ -47,10 +47,8 @@ class HealthMonitor: private def checkRedisHeartbeat(instanceId: String): Boolean = try - val key = s"$redisPrefix:instances:$instanceId" - val bucket = redissonClient.getBucket[String](key) - val ttl = bucket.remainTimeToLive() - ttl > 0 + val key = s"$redisPrefix:instances:$instanceId" + redis.key(classOf[String]).pttl(key) > 0 catch case ex: Exception => log.debugf(ex, "Redis heartbeat check failed for %s", instanceId) diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala index dd2798f..af1d706 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala @@ -2,7 +2,7 @@ package de.nowchess.coordinator.service import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.RedissonClient +import io.quarkus.redis.datasource.RedisDataSource import scala.jdk.CollectionConverters.* import scala.compiletime.uninitialized import com.fasterxml.jackson.databind.ObjectMapper @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap @ApplicationScoped class InstanceRegistry: @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized private val mapper = ObjectMapper() private val instances = ConcurrentHashMap[String, InstanceMetadata]() @@ -28,9 +28,8 @@ class InstanceRegistry: instances.values.asScala.toList def updateInstanceFromRedis(instanceId: String): Unit = - val key = s"$redisPrefix:instances:$instanceId" - val bucket = redissonClient.getBucket[String](key) - val value = bucket.get() + val key = s"$redisPrefix:instances:$instanceId" + val value = redis.value(classOf[String]).get(key) if value != null then try val metadata = mapper.readValue(value, classOf[InstanceMetadata]) diff --git a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala index 28e8b15..7dcbdf2 100644 --- a/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala +++ b/modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala @@ -3,7 +3,7 @@ package de.nowchess.coordinator.service import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject import de.nowchess.coordinator.config.CoordinatorConfig -import org.redisson.api.RedissonClient +import io.quarkus.redis.datasource.RedisDataSource import org.jboss.logging.Logger import scala.compiletime.uninitialized import scala.concurrent.duration.* @@ -19,7 +19,7 @@ class LoadBalancer: private var instanceRegistry: InstanceRegistry = uninitialized @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized @Inject private var coreGrpcClient: CoreGrpcClient = uninitialized @@ -107,9 +107,8 @@ class LoadBalancer: private def getGamesToMove(instanceId: String, count: Int): List[String] = try - val setKey = s"$redisPrefix:instance:$instanceId:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.readAll.asScala.toList.take(count) + val setKey = s"$redisPrefix:instance:$instanceId:games" + redis.set(classOf[String]).smembers(setKey).asScala.toList.take(count) catch case ex: Exception => log.debugf(ex, "Failed to get games for %s", instanceId) @@ -120,12 +119,9 @@ class LoadBalancer: val fromKey = s"$redisPrefix:instance:$fromInstanceId:games" val toKey = s"$redisPrefix:instance:$toInstanceId:games" - val fromSet = redissonClient.getSet[String](fromKey) - val toSet = redissonClient.getSet[String](toKey) - gameIds.foreach { gameId => - fromSet.remove(gameId) - toSet.add(gameId) + redis.set(classOf[String]).srem(fromKey, gameId) + redis.set(classOf[String]).sadd(toKey, gameId) } catch case ex: Exception => diff --git a/modules/core/build.gradle.kts b/modules/core/build.gradle.kts index f870d1a..dda74de 100644 --- a/modules/core/build.gradle.kts +++ b/modules/core/build.gradle.kts @@ -71,7 +71,7 @@ dependencies { implementation("io.quarkus:quarkus-websockets-next") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - implementation("org.redisson:redisson:${versions["REDISSON"]!!}") + implementation("io.quarkus:quarkus-redis-client") testImplementation(project(":modules:io")) testImplementation(project(":modules:rule")) diff --git a/modules/core/src/main/resources/application.yml b/modules/core/src/main/resources/application.yml index 265ad97..eccddc5 100644 --- a/modules/core/src/main/resources/application.yml +++ b/modules/core/src/main/resources/application.yml @@ -3,6 +3,8 @@ quarkus: port: 8080 application: name: nowchess-core + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} grpc: clients: rule-grpc: diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala b/modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala index c583f1f..e453e59 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala @@ -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 diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala b/modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala deleted file mode 100644 index 12ac5b5..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala +++ /dev/null @@ -1,35 +0,0 @@ -package de.nowchess.chess.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()) diff --git a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala index 381a12e..5ac4f98 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala @@ -9,12 +9,12 @@ import de.nowchess.api.board.Color import de.nowchess.chess.observer.{GameEvent, Observer} import de.nowchess.chess.registry.GameRegistry import de.nowchess.chess.resource.GameDtoMapper -import org.redisson.api.RTopic +import io.quarkus.redis.datasource.RedisDataSource class GameRedisPublisher( gameId: String, registry: GameRegistry, - redisson: org.redisson.api.RedissonClient, + redis: RedisDataSource, objectMapper: ObjectMapper, s2cTopicName: String, writebackEmit: String => Unit, @@ -26,7 +26,7 @@ class GameRedisPublisher( registry.get(gameId).foreach { entry => val dto = GameDtoMapper.toGameStateDto(entry, ioClient) val json = objectMapper.writeValueAsString(GameStateEventDto(dto)) - redisson.getTopic(s2cTopicName).publish(json) + redis.pubsub(classOf[String]).publish(s2cTopicName, json) val clock = entry.engine.currentClockState val wb = GameWritebackEventDto( diff --git a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala index 2b328fe..60b78ee 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala @@ -10,20 +10,21 @@ import de.nowchess.chess.observer.Observer import de.nowchess.chess.registry.GameRegistry import de.nowchess.chess.resource.GameDtoMapper import de.nowchess.chess.service.InstanceHeartbeatService +import io.quarkus.redis.datasource.RedisDataSource +import io.quarkus.redis.datasource.pubsub.PubSubCommands import jakarta.annotation.PreDestroy import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.listener.MessageListener -import org.redisson.api.RedissonClient import scala.compiletime.uninitialized import scala.util.Try import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer @ApplicationScoped class GameRedisSubscriberManager: // scalafix:off DisableSyntax.var - @Inject var redisson: RedissonClient = uninitialized + @Inject var redis: RedisDataSource = uninitialized @Inject var registry: GameRegistry = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized @Inject var redisConfig: RedisConfig = uninitialized @@ -31,7 +32,7 @@ class GameRedisSubscriberManager: @Inject var heartbeatService: InstanceHeartbeatService = null // scalafix:on DisableSyntax.var - private val c2sListeners = new ConcurrentHashMap[String, Int]() + private val c2sListeners = new ConcurrentHashMap[String, PubSubCommands.RedisSubscriber]() private val s2cObservers = new ConcurrentHashMap[String, Observer]() private def c2sTopic(gameId: String): String = @@ -42,21 +43,15 @@ class GameRedisSubscriberManager: def subscribeGame(gameId: String): Unit = try - val topic = redisson.getTopic(c2sTopic(gameId)) - val listenerId = topic.addListener( - classOf[String], - new MessageListener[String]: - def onMessage(channel: CharSequence, msg: String): Unit = - handleC2sMessage(gameId, msg), - ) - c2sListeners.put(gameId, listenerId) + val handler: Consumer[String] = msg => handleC2sMessage(gameId, msg) + val subscriber = redis.pubsub(classOf[String]).subscribe(c2sTopic(gameId), handler) + c2sListeners.put(gameId, subscriber) - val writebackTopic = redisson.getTopic("game-writeback") - val writebackFn: String => Unit = json => writebackTopic.publish(json) + val writebackFn: String => Unit = json => redis.pubsub(classOf[String]).publish("game-writeback", json) val obs = new GameRedisPublisher( gameId, registry, - redisson, + redis, objectMapper, s2cTopicName(gameId), writebackFn, @@ -73,8 +68,8 @@ class GameRedisSubscriberManager: () def unsubscribeGame(gameId: String): Unit = - Option(c2sListeners.remove(gameId)).foreach { listenerId => - redisson.getTopic(c2sTopic(gameId)).removeListener(listenerId) + Option(c2sListeners.remove(gameId)).foreach { subscriber => + subscriber.unsubscribe(c2sTopic(gameId)) } Option(s2cObservers.remove(gameId)).foreach { obs => registry.get(gameId).foreach(_.engine.unsubscribe(obs)) @@ -84,16 +79,16 @@ class GameRedisSubscriberManager: private def handleC2sMessage(gameId: String, msg: String): Unit = parseC2sMessage(msg) match - case Some(C2sMessage.Connected) => handleConnected(gameId) + case Some(C2sMessage.Connected) => handleConnected(gameId) case Some(C2sMessage.Move(uci, playerId)) => handleMove(gameId, uci, playerId) - case Some(C2sMessage.Ping) => () - case None => () + case Some(C2sMessage.Ping) => () + case None => () private def handleConnected(gameId: String): Unit = registry.get(gameId).foreach { entry => val dto = GameDtoMapper.toGameFullDto(entry, ioClient) val json = objectMapper.writeValueAsString(GameFullEventDto(dto)) - redisson.getTopic(s2cTopicName(gameId)).publish(json) + redis.pubsub(classOf[String]).publish(s2cTopicName(gameId), json) } private def handleMove(gameId: String, uci: String, playerId: Option[String]): Unit = @@ -120,8 +115,8 @@ class GameRedisSubscriberManager: val pid = Option(node.get("playerId")).map(_.asText()).filter(_.nonEmpty) C2sMessage.Move(u.asText(), pid) } - case "PING" => Some(C2sMessage.Ping) - case _ => None + case "PING" => Some(C2sMessage.Ping) + case _ => None } } @@ -157,5 +152,5 @@ class GameRedisSubscriberManager: @PreDestroy def cleanup(): Unit = - c2sListeners.forEach((gameId, listenerId) => redisson.getTopic(c2sTopic(gameId)).removeListener(listenerId)) + c2sListeners.forEach((gameId, subscriber) => subscriber.unsubscribe(c2sTopic(gameId))) s2cObservers.forEach((gameId, obs) => registry.get(gameId).foreach(_.engine.unsubscribe(obs))) diff --git a/modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala b/modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala index d6a3567..49152b2 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala @@ -12,23 +12,22 @@ import de.nowchess.chess.grpc.RuleSetGrpcAdapter import de.nowchess.chess.config.RedisConfig import de.nowchess.chess.grpc.IoGrpcClientWrapper import de.nowchess.chess.resource.GameDtoMapper +import io.quarkus.redis.datasource.RedisDataSource import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject import org.eclipse.microprofile.rest.client.inject.RestClient -import org.redisson.api.RedissonClient -import scala.annotation.nowarn import scala.compiletime.uninitialized import scala.util.Try import java.nio.charset.StandardCharsets import java.security.{MessageDigest, SecureRandom} import java.time.Instant -import java.util.concurrent.{ConcurrentHashMap, TimeUnit} +import java.util.concurrent.ConcurrentHashMap @ApplicationScoped class RedisGameRegistry extends GameRegistry: @Inject // scalafix:off DisableSyntax.var - var redisson: RedissonClient = uninitialized + var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized @Inject var ioClient: IoGrpcClientWrapper = uninitialized @@ -40,7 +39,6 @@ class RedisGameRegistry extends GameRegistry: private val rng = new SecureRandom() private def cacheKey(gameId: String) = s"${redisConfig.prefix}:game:entry:$gameId" - private def bucket(gameId: String) = redisson.getBucket[String](cacheKey(gameId)) def generateId(): String = val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" @@ -49,9 +47,7 @@ class RedisGameRegistry extends GameRegistry: def store(entry: GameEntry): Unit = localEngines.put(entry.gameId, entry) val combined = ioClient.exportCombined(entry.engine.context) - val b = bucket(entry.gameId) - b.set(toJson(entry, combined.fen, combined.pgn)) - (b.expire(30, TimeUnit.MINUTES): @nowarn) + redis.value(classOf[String]).setex(cacheKey(entry.gameId), 1800L, toJson(entry, combined.fen, combined.pgn)) def get(gameId: String): Option[GameEntry] = Option(localEngines.get(gameId)) match @@ -66,12 +62,10 @@ class RedisGameRegistry extends GameRegistry: def update(entry: GameEntry): Unit = localEngines.put(entry.gameId, entry) val combined = ioClient.exportCombined(entry.engine.context) - val b = bucket(entry.gameId) - b.set(toJson(entry, combined.fen, combined.pgn)) - (b.expire(30, TimeUnit.MINUTES): @nowarn) + redis.value(classOf[String]).setex(cacheKey(entry.gameId), 1800L, toJson(entry, combined.fen, combined.pgn)) private def readRedisDto(gameId: String): Option[GameCacheDto] = - Try(Option(bucket(gameId).get())).toOption.flatten.flatMap { json => + Try(Option(redis.value(classOf[String]).get(cacheKey(gameId)))).toOption.flatten.flatMap { json => Try(objectMapper.readValue(json, classOf[GameCacheDto])).toOption } @@ -111,9 +105,7 @@ class RedisGameRegistry extends GameRegistry: }.toOption .map { case (dto, entry) => localEngines.put(gameId, entry) - val b = bucket(gameId) - b.set(objectMapper.writeValueAsString(dto)) - (b.expire(30, TimeUnit.MINUTES): @nowarn) + redis.value(classOf[String]).setex(cacheKey(gameId), 1800L, objectMapper.writeValueAsString(dto)) entry } diff --git a/modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala b/modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala index ea851eb..af4ddda 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala @@ -7,8 +7,7 @@ import io.quarkus.runtime.StartupEvent import io.quarkus.runtime.ShutdownEvent import io.quarkus.grpc.GrpcClient import org.eclipse.microprofile.config.inject.ConfigProperty -import org.redisson.api.RedissonClient -import scala.annotation.nowarn +import io.quarkus.redis.datasource.RedisDataSource import scala.compiletime.uninitialized import java.util.concurrent.{Executors, TimeUnit} import java.net.InetAddress @@ -22,7 +21,7 @@ import io.grpc.Channel @ApplicationScoped class InstanceHeartbeatService: @Inject - private var redissonClient: RedissonClient = uninitialized + private var redis: RedisDataSource = uninitialized @GrpcClient("coordinator-grpc") private var channel: Channel = uninitialized @@ -95,17 +94,15 @@ class InstanceHeartbeatService: def addGameSubscription(gameId: String): Unit = if !coordinatorEnabled then return - val setKey = s"$redisPrefix:instance:$instanceId:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.add(gameId) + val setKey = s"$redisPrefix:instance:$instanceId:games" + redis.set(classOf[String]).sadd(setKey, gameId) subscriptionCount += 1 def removeGameSubscription(gameId: String): Unit = if !coordinatorEnabled then return - val setKey = s"$redisPrefix:instance:$instanceId:games" - val gameSet = redissonClient.getSet[String](setKey) - gameSet.remove(gameId) + val setKey = s"$redisPrefix:instance:$instanceId:games" + redis.set(classOf[String]).srem(setKey, gameId) subscriptionCount = Math.max(0, subscriptionCount - 1) private def generateInstanceId(): Unit = @@ -177,8 +174,7 @@ class InstanceHeartbeatService: private def refreshRedisHeartbeat(): Unit = try - val key = s"$redisPrefix:instances:$instanceId" - val bucket = redissonClient.getBucket[String](key) + val key = s"$redisPrefix:instances:$instanceId" val metadata = Map( "instanceId" -> instanceId, @@ -192,8 +188,7 @@ class InstanceHeartbeatService: ) val json = mapper.writeValueAsString(metadata) - bucket.set(json) - (bucket.expire(5, TimeUnit.SECONDS): @nowarn) + redis.value(classOf[String]).setex(key, 5L, json) catch case ex: Exception => log.warnf(ex, "Failed to refresh Redis heartbeat") @@ -208,10 +203,10 @@ class InstanceHeartbeatService: if instanceId.nonEmpty then val key = s"$redisPrefix:instances:$instanceId" - redissonClient.getBucket[String](key).delete() + redis.key(classOf[String]).del(key) val setKey = s"$redisPrefix:instance:$instanceId:games" - redissonClient.getSet[String](setKey).delete() + redis.key(classOf[String]).del(setKey) heartbeatExecutor.shutdown() redisHeartbeatExecutor.shutdown() diff --git a/modules/core/src/test/resources/application.yml b/modules/core/src/test/resources/application.yml index 1fdafa6..af2dd01 100644 --- a/modules/core/src/test/resources/application.yml +++ b/modules/core/src/test/resources/application.yml @@ -12,6 +12,10 @@ quarkus: url: http://localhost:8085 nowchess: + internal: + secret: test-secret + auth: + enabled: false coordinator: enabled: false redis: diff --git a/modules/core/src/test/scala/de/nowchess/chess/config/MockRedissonProducer.scala b/modules/core/src/test/scala/de/nowchess/chess/config/MockRedisDataSourceProducer.scala similarity index 60% rename from modules/core/src/test/scala/de/nowchess/chess/config/MockRedissonProducer.scala rename to modules/core/src/test/scala/de/nowchess/chess/config/MockRedisDataSourceProducer.scala index 7ba923d..ad2837a 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/config/MockRedissonProducer.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/config/MockRedisDataSourceProducer.scala @@ -1,17 +1,17 @@ package de.nowchess.chess.config +import io.quarkus.redis.datasource.RedisDataSource import jakarta.annotation.Priority import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.inject.Alternative import jakarta.enterprise.inject.Produces import org.mockito.Mockito -import org.redisson.api.RedissonClient @Alternative @Priority(1) @ApplicationScoped -class MockRedissonProducer: +class MockRedisDataSourceProducer: @Produces @ApplicationScoped - def produceRedissonClient(): RedissonClient = - Mockito.mock(classOf[RedissonClient], Mockito.RETURNS_DEEP_STUBS) + def produceRedisDataSource(): RedisDataSource = + Mockito.mock(classOf[RedisDataSource], Mockito.RETURNS_DEEP_STUBS) diff --git a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala index 470cee1..b61229d 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala @@ -40,7 +40,7 @@ class GameResourceIntegrationTest: @BeforeEach def setupMocks(): Unit = - when(jwt.getClaim[AnyRef]("type")).thenReturn("bot") + when(jwt.getClaim[AnyRef]("type")).thenReturn("user") when(ioWrapper.importFen(any[String]())).thenReturn(GameContext.initial) when(ioWrapper.importPgn(any[String]())).thenAnswer((inv: InvocationOnMock) => diff --git a/modules/io/src/test/resources/application.yml b/modules/io/src/test/resources/application.yml new file mode 100644 index 0000000..21664c7 --- /dev/null +++ b/modules/io/src/test/resources/application.yml @@ -0,0 +1,5 @@ +nowchess: + internal: + secret: test-secret + auth: + enabled: false diff --git a/modules/official-bots/build.gradle.kts b/modules/official-bots/build.gradle.kts index aae0cc7..01a366b 100644 --- a/modules/official-bots/build.gradle.kts +++ b/modules/official-bots/build.gradle.kts @@ -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") diff --git a/modules/official-bots/src/main/resources/application.yml b/modules/official-bots/src/main/resources/application.yml index 6318458..42b63f1 100644 --- a/modules/official-bots/src/main/resources/application.yml +++ b/modules/official-bots/src/main/resources/application.yml @@ -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: diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedisConfig.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedisConfig.scala index ceabe14..fb4bd11 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedisConfig.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedisConfig.scala @@ -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 diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedissonProducer.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedissonProducer.scala deleted file mode 100644 index ac1663e..0000000 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/config/RedissonProducer.scala +++ /dev/null @@ -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()) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala index f2d76b3..4f40e0c 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala @@ -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) () } } diff --git a/modules/official-bots/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala b/modules/official-bots/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala index bf53263..71b070e 100644 --- a/modules/official-bots/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala +++ b/modules/official-bots/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala @@ -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) diff --git a/modules/official-bots/src/test/scala/de/nowchess/bot/HybridBotTest.scala b/modules/official-bots/src/test/scala/de/nowchess/bot/HybridBotTest.scala index bf806c2..900a85f 100644 --- a/modules/official-bots/src/test/scala/de/nowchess/bot/HybridBotTest.scala +++ b/modules/official-bots/src/test/scala/de/nowchess/bot/HybridBotTest.scala @@ -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)) diff --git a/modules/official-bots/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala b/modules/official-bots/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala index e61e2d2..9ba3a9b 100644 --- a/modules/official-bots/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala +++ b/modules/official-bots/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala @@ -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())) diff --git a/modules/rule/src/test/resources/application.yml b/modules/rule/src/test/resources/application.yml new file mode 100644 index 0000000..21664c7 --- /dev/null +++ b/modules/rule/src/test/resources/application.yml @@ -0,0 +1,5 @@ +nowchess: + internal: + secret: test-secret + auth: + enabled: false diff --git a/modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala b/modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala index 0ba59ff..c2cf102 100644 --- a/modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala +++ b/modules/security/src/main/scala/de/nowchess/security/InternalAuthFilter.scala @@ -12,12 +12,16 @@ import scala.compiletime.uninitialized @ApplicationScoped class InternalAuthFilter extends ContainerRequestFilter: - @ConfigProperty(name = "nowchess.internal.secret") + @ConfigProperty(name = "nowchess.internal.secret", defaultValue = "") // scalafix:off DisableSyntax.var var secret: String = uninitialized + + @ConfigProperty(name = "nowchess.internal.auth.enabled", defaultValue = "true") + var authEnabled: Boolean = uninitialized // scalafix:on DisableSyntax.var override def filter(ctx: ContainerRequestContext): Unit = - val header = ctx.getHeaderString("X-Internal-Secret") - if header == null || header != secret then - ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()) + if authEnabled then + val header = ctx.getHeaderString("X-Internal-Secret") + if header == null || header != secret then + ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()) diff --git a/modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala b/modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala index ac6c447..1b2b698 100644 --- a/modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala +++ b/modules/security/src/main/scala/de/nowchess/security/InternalGrpcAuthInterceptor.scala @@ -12,9 +12,12 @@ class InternalGrpcAuthInterceptor extends ServerInterceptor: private val secretKey = Metadata.Key.of("x-internal-secret", Metadata.ASCII_STRING_MARSHALLER) - @ConfigProperty(name = "nowchess.internal.secret") + @ConfigProperty(name = "nowchess.internal.secret", defaultValue = "") // scalafix:off DisableSyntax.var var secret: String = uninitialized + + @ConfigProperty(name = "nowchess.internal.auth.enabled", defaultValue = "true") + var authEnabled: Boolean = uninitialized // scalafix:on DisableSyntax.var override def interceptCall[Req, Resp]( @@ -23,7 +26,7 @@ class InternalGrpcAuthInterceptor extends ServerInterceptor: next: ServerCallHandler[Req, Resp], ): ServerCall.Listener[Req] = val token = Option(headers.get(secretKey)).getOrElse("") - if token != secret then + if authEnabled && token != secret then call.close(Status.UNAUTHENTICATED.withDescription("Missing or invalid internal secret"), new Metadata()) new ServerCall.Listener[Req] {} else diff --git a/modules/store/build.gradle.kts b/modules/store/build.gradle.kts index 098f720..3753202 100644 --- a/modules/store/build.gradle.kts +++ b/modules/store/build.gradle.kts @@ -54,7 +54,7 @@ dependencies { implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-smallrye-health") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - implementation("org.redisson:redisson:${versions["REDISSON"]!!}") + implementation("io.quarkus:quarkus-redis-client") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/modules/store/src/main/resources/application.yml b/modules/store/src/main/resources/application.yml index 103bdf3..a62b788 100644 --- a/modules/store/src/main/resources/application.yml +++ b/modules/store/src/main/resources/application.yml @@ -2,6 +2,8 @@ quarkus: application.name: nowchess-store http.port: 8085 config.yaml.enabled: true + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} datasource: db-kind: postgresql username: ${DB_USER:nowchess} diff --git a/modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala b/modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala index 032cfa2..48e3eb4 100644 --- a/modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala +++ b/modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala @@ -6,11 +6,7 @@ import scala.compiletime.uninitialized @ApplicationScoped class RedisConfig: - @ConfigProperty(name = "nowchess.redis.host", defaultValue = "localhost") - var host: String = uninitialized - - @ConfigProperty(name = "nowchess.redis.port", defaultValue = "6379") - var port: Int = uninitialized - + // scalafix:off DisableSyntax.var @ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess") var prefix: String = uninitialized + // scalafix:on DisableSyntax.var diff --git a/modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala b/modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala deleted file mode 100644 index 321fb6e..0000000 --- a/modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala +++ /dev/null @@ -1,30 +0,0 @@ -package de.nowchess.store.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: - @Inject - var redisConfig: RedisConfig = uninitialized - - @Produces - @ApplicationScoped - def redissonClient(): RedissonClient = - val config = new Config() - config - .useSingleServer() - .setAddress(s"redis://${redisConfig.host}:${redisConfig.port}") - .setConnectionMinimumIdleSize(1) - .setConnectTimeout(500) - Redisson.create(config) - - @PreDestroy - def close(client: RedissonClient): Unit = - client.shutdown() diff --git a/modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala b/modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala index 4f9b53a..d4e6af8 100644 --- a/modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala +++ b/modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala @@ -2,28 +2,27 @@ package de.nowchess.store.redis import com.fasterxml.jackson.databind.ObjectMapper import de.nowchess.store.service.GameWritebackService +import io.quarkus.redis.datasource.RedisDataSource import jakarta.annotation.PostConstruct import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject -import org.redisson.api.listener.MessageListener -import org.redisson.api.RedissonClient import scala.compiletime.uninitialized import scala.util.Try +import java.util.function.Consumer @ApplicationScoped class GameWritebackStreamListener: @Inject // scalafix:off DisableSyntax.var - var redisson: RedissonClient = uninitialized + var redis: RedisDataSource = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized @Inject var writebackService: GameWritebackService = uninitialized // scalafix:on @PostConstruct def startListening(): Unit = - val topic = redisson.getTopic("game-writeback") - topic.addListener( - classOf[String], - (channel: CharSequence, json: String) => Try(objectMapper.readValue(json, classOf[GameWritebackEventDto])).toOption + val handler: Consumer[String] = json => + Try(objectMapper.readValue(json, classOf[GameWritebackEventDto])).toOption .foreach(writebackService.writeBack) - ) + redis.pubsub(classOf[String]).subscribe("game-writeback", handler) + () diff --git a/modules/ws/build.gradle.kts b/modules/ws/build.gradle.kts index 05992e6..fe67808 100644 --- a/modules/ws/build.gradle.kts +++ b/modules/ws/build.gradle.kts @@ -54,7 +54,7 @@ dependencies { implementation("io.quarkus:quarkus-config-yaml") implementation("io.quarkus:quarkus-smallrye-health") implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") - 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") diff --git a/modules/ws/src/main/resources/application.yml b/modules/ws/src/main/resources/application.yml index 5710691..15f707a 100644 --- a/modules/ws/src/main/resources/application.yml +++ b/modules/ws/src/main/resources/application.yml @@ -3,6 +3,8 @@ quarkus: port: 8084 application: name: nowchess-ws + redis: + hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} swagger-ui: always-include: true path: /swagger-ui diff --git a/modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala b/modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala index 31285dd..0489bb4 100644 --- a/modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala +++ b/modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala @@ -1,12 +1,8 @@ package de.nowchess.ws.config -import de.nowchess.ws.resource.ConnectionMeta import io.quarkus.runtime.annotations.RegisterForReflection @RegisterForReflection( - targets = Array( - classOf[ConnectionMeta], - ), + targets = Array(), ) class NativeReflectionConfig - diff --git a/modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala b/modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala index 360049e..cde1ab7 100644 --- a/modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala +++ b/modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala @@ -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 diff --git a/modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala b/modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala deleted file mode 100644 index 1fc4a17..0000000 --- a/modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala +++ /dev/null @@ -1,35 +0,0 @@ -package de.nowchess.ws.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()) diff --git a/modules/ws/src/main/scala/de/nowchess/ws/resource/ConnectionMeta.scala b/modules/ws/src/main/scala/de/nowchess/ws/resource/ConnectionMeta.scala index af1043d..7726823 100644 --- a/modules/ws/src/main/scala/de/nowchess/ws/resource/ConnectionMeta.scala +++ b/modules/ws/src/main/scala/de/nowchess/ws/resource/ConnectionMeta.scala @@ -1,7 +1,9 @@ package de.nowchess.ws.resource +import io.quarkus.redis.datasource.pubsub.PubSubCommands + final case class ConnectionMeta( gameId: String, - listenerId: Int, + subscriber: PubSubCommands.RedisSubscriber, playerId: Option[String], ) diff --git a/modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala b/modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala index 6d84874..3821a07 100644 --- a/modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala +++ b/modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala @@ -1,22 +1,22 @@ package de.nowchess.ws.resource import de.nowchess.ws.config.RedisConfig +import io.quarkus.redis.datasource.RedisDataSource +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.redisson.api.listener.MessageListener -import org.redisson.api.RedissonClient - import scala.compiletime.uninitialized import scala.util.Try import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer @WebSocket(path = "/api/board/game/{gameId}/ws") class GameWebSocketResource: // scalafix:off DisableSyntax.var @Inject - var redisson: RedissonClient = uninitialized + var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized @@ -40,18 +40,13 @@ class GameWebSocketResource: .filter(_.nonEmpty) .flatMap(token => Try(jwtParser.parse(token)).toOption) .map(_.getSubject) - val topic = redisson.getTopic(s2cTopic(gameId)) - val listenerId = topic.addListener( - classOf[String], - new MessageListener[String]: - def onMessage(channel: CharSequence, msg: String): Unit = - connection.sendText(msg).subscribe().`with`(_ => (), _ => ()), - ) - connections.put(connection.id(), ConnectionMeta(gameId, listenerId, playerId)) + val handler: Consumer[String] = msg => connection.sendText(msg).subscribe().`with`(_ => (), _ => ()) + val subscriber = redis.pubsub(classOf[String]).subscribe(s2cTopic(gameId), handler) + connections.put(connection.id(), ConnectionMeta(gameId, subscriber, playerId)) val connectedMsg = playerId match case Some(pid) => s"""{"type":"CONNECTED","gameId":"$gameId","playerId":"$pid"}""" case None => s"""{"type":"CONNECTED","gameId":"$gameId"}""" - redisson.getTopic(c2sTopic(gameId)).publish(connectedMsg) + redis.pubsub(classOf[String]).publish(c2sTopic(gameId), connectedMsg) @OnTextMessage def onTextMessage(connection: WebSocketConnection, message: String): Unit = @@ -59,13 +54,13 @@ class GameWebSocketResource: val enriched = meta.playerId match case Some(pid) => injectPlayerId(message, pid) case None => message - redisson.getTopic(c2sTopic(meta.gameId)).publish(enriched) + redis.pubsub(classOf[String]).publish(c2sTopic(meta.gameId), enriched) } @OnClose def onClose(connection: WebSocketConnection): Unit = Option(connections.remove(connection.id())).foreach { meta => - redisson.getTopic(s2cTopic(meta.gameId)).removeListener(meta.listenerId) + meta.subscriber.unsubscribe(s2cTopic(meta.gameId)) } private def injectPlayerId(msg: String, pid: String): String = diff --git a/modules/ws/src/main/scala/de/nowchess/ws/resource/UserWebSocketResource.scala b/modules/ws/src/main/scala/de/nowchess/ws/resource/UserWebSocketResource.scala index cc3734e..9fb3cb8 100644 --- a/modules/ws/src/main/scala/de/nowchess/ws/resource/UserWebSocketResource.scala +++ b/modules/ws/src/main/scala/de/nowchess/ws/resource/UserWebSocketResource.scala @@ -1,22 +1,22 @@ package de.nowchess.ws.resource import de.nowchess.ws.config.RedisConfig +import io.quarkus.redis.datasource.RedisDataSource +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.redisson.api.listener.MessageListener -import org.redisson.api.RedissonClient - import scala.compiletime.uninitialized import scala.util.Try import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer @WebSocket(path = "/api/user/ws") class UserWebSocketResource: // scalafix:off DisableSyntax.var @Inject - var redisson: RedissonClient = uninitialized + var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized @@ -25,7 +25,7 @@ class UserWebSocketResource: var jwtParser: JWTParser = uninitialized // scalafix:on DisableSyntax.var - private val connections = new ConcurrentHashMap[String, (String, Int)]() + private val connections = new ConcurrentHashMap[String, (String, PubSubCommands.RedisSubscriber)]() private def userTopic(userId: String): String = s"${redisConfig.prefix}:user:$userId:events" @@ -40,18 +40,14 @@ class UserWebSocketResource: userIdOpt match case None => connection.close().subscribe().`with`(_ => (), _ => ()) case Some(userId) => - val listenerId = redisson.getTopic(userTopic(userId)).addListener( - classOf[String], - new MessageListener[String]: - def onMessage(channel: CharSequence, msg: String): Unit = - connection.sendText(msg).subscribe().`with`(_ => (), _ => ()), - ) - connections.put(connection.id(), (userId, listenerId)) + 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 = - Option(connections.remove(connection.id())).foreach { (userId, listenerId) => - redisson.getTopic(userTopic(userId)).removeListener(listenerId) + Option(connections.remove(connection.id())).foreach { (userId, subscriber) => + subscriber.unsubscribe(userTopic(userId)) }