feat(logging): deepen coordinator and core logging (NCS-72)
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
Coordinator: InstanceRegistry logs instance join/update/dead/remove; CoordinatorGrpcServer logs new stream + first heartbeat per instance; CoreGrpcClient logs channel open/evict and RPC success counts. Core: GameResource replaces println with logger; RedisGameRegistry logs store/Redis-load/DB-load and surfaces silent failures; CoordinatorServiceHandler logs inbound gRPC commands; IoGrpcClientWrapper and RuleSetGrpcAdapter wrap gRPC calls with WARN on failure. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@ quarkus:
|
|||||||
server:
|
server:
|
||||||
port: 9086
|
port: 9086
|
||||||
rest-client:
|
rest-client:
|
||||||
connection-timeout: 5000
|
|
||||||
read-timeout: 10000
|
read-timeout: 10000
|
||||||
smallrye-openapi:
|
smallrye-openapi:
|
||||||
info-title: NowChess Coordinator Service
|
info-title: NowChess Coordinator Service
|
||||||
|
|||||||
+11
@@ -27,13 +27,24 @@ class CoordinatorGrpcServer extends CoordinatorServiceGrpc.CoordinatorServiceImp
|
|||||||
override def heartbeatStream(
|
override def heartbeatStream(
|
||||||
responseObserver: StreamObserver[CoordinatorCommand],
|
responseObserver: StreamObserver[CoordinatorCommand],
|
||||||
): StreamObserver[HeartbeatFrame] =
|
): StreamObserver[HeartbeatFrame] =
|
||||||
|
log.info("New heartbeat stream connection established")
|
||||||
new StreamObserver[HeartbeatFrame]:
|
new StreamObserver[HeartbeatFrame]:
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
private var lastInstanceId = ""
|
private var lastInstanceId = ""
|
||||||
|
private var firstFrameSeen = false
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
override def onNext(frame: HeartbeatFrame): Unit =
|
override def onNext(frame: HeartbeatFrame): Unit =
|
||||||
lastInstanceId = frame.getInstanceId
|
lastInstanceId = frame.getInstanceId
|
||||||
|
if !firstFrameSeen then
|
||||||
|
firstFrameSeen = true
|
||||||
|
log.infof(
|
||||||
|
"First heartbeat from instance %s (host=%s http=%d grpc=%d)",
|
||||||
|
frame.getInstanceId,
|
||||||
|
frame.getHostname,
|
||||||
|
frame.getHttpPort,
|
||||||
|
frame.getGrpcPort,
|
||||||
|
)
|
||||||
instanceRegistry
|
instanceRegistry
|
||||||
.updateInstanceFromRedis(frame.getInstanceId)
|
.updateInstanceFromRedis(frame.getInstanceId)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|||||||
+19
-5
@@ -16,10 +16,18 @@ class CoreGrpcClient:
|
|||||||
private val channels = ConcurrentHashMap[String, ManagedChannel]()
|
private val channels = ConcurrentHashMap[String, ManagedChannel]()
|
||||||
|
|
||||||
private def getChannel(host: String, port: Int): ManagedChannel =
|
private def getChannel(host: String, port: Int): ManagedChannel =
|
||||||
channels.computeIfAbsent(s"$host:$port", _ => ManagedChannelBuilder.forAddress(host, port).usePlaintext().build())
|
channels.computeIfAbsent(
|
||||||
|
s"$host:$port",
|
||||||
|
_ =>
|
||||||
|
log.infof("Opening gRPC channel to %s:%d", host, port)
|
||||||
|
ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(),
|
||||||
|
)
|
||||||
|
|
||||||
private def evictStaleChannel(host: String, port: Int): Unit =
|
private def evictStaleChannel(host: String, port: Int): Unit =
|
||||||
Option(channels.remove(s"$host:$port")).foreach(_.shutdownNow())
|
Option(channels.remove(s"$host:$port")).foreach { ch =>
|
||||||
|
log.infof("Evicting stale gRPC channel to %s:%d", host, port)
|
||||||
|
ch.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
def shutdown(): Unit =
|
def shutdown(): Unit =
|
||||||
@@ -33,7 +41,9 @@ class CoreGrpcClient:
|
|||||||
try
|
try
|
||||||
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
||||||
val request = BatchResubscribeRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
val request = BatchResubscribeRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
||||||
stub.batchResubscribeGames(request).getSubscribedCount
|
val count = stub.batchResubscribeGames(request).getSubscribedCount
|
||||||
|
log.debugf("batchResubscribeGames %s:%d — subscribed %d games", host, port, count)
|
||||||
|
count
|
||||||
catch
|
catch
|
||||||
case ex: Exception =>
|
case ex: Exception =>
|
||||||
log.warnf(ex, "batchResubscribeGames RPC failed for %s:%d", host, port)
|
log.warnf(ex, "batchResubscribeGames RPC failed for %s:%d", host, port)
|
||||||
@@ -44,7 +54,9 @@ class CoreGrpcClient:
|
|||||||
try
|
try
|
||||||
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
||||||
val request = UnsubscribeGamesRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
val request = UnsubscribeGamesRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
||||||
stub.unsubscribeGames(request).getUnsubscribedCount
|
val count = stub.unsubscribeGames(request).getUnsubscribedCount
|
||||||
|
log.debugf("unsubscribeGames %s:%d — unsubscribed %d games", host, port, count)
|
||||||
|
count
|
||||||
catch
|
catch
|
||||||
case ex: Exception =>
|
case ex: Exception =>
|
||||||
log.warnf(ex, "unsubscribeGames RPC failed for %s:%d", host, port)
|
log.warnf(ex, "unsubscribeGames RPC failed for %s:%d", host, port)
|
||||||
@@ -55,7 +67,9 @@ class CoreGrpcClient:
|
|||||||
try
|
try
|
||||||
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
val stub = CoordinatorServiceGrpc.newBlockingStub(getChannel(host, port))
|
||||||
val request = EvictGamesRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
val request = EvictGamesRequest.newBuilder().addAllGameIds(gameIds.asJava).build()
|
||||||
stub.evictGames(request).getEvictedCount
|
val count = stub.evictGames(request).getEvictedCount
|
||||||
|
log.debugf("evictGames %s:%d — evicted %d games", host, port, count)
|
||||||
|
count
|
||||||
catch
|
catch
|
||||||
case ex: Exception =>
|
case ex: Exception =>
|
||||||
log.warnf(ex, "evictGames RPC failed for %s:%d", host, port)
|
log.warnf(ex, "evictGames RPC failed for %s:%d", host, port)
|
||||||
|
|||||||
+14
-3
@@ -41,9 +41,20 @@ class InstanceRegistry:
|
|||||||
.transformToUni { value =>
|
.transformToUni { value =>
|
||||||
try
|
try
|
||||||
val metadata = mapper.readValue(value, classOf[InstanceMetadata])
|
val metadata = mapper.readValue(value, classOf[InstanceMetadata])
|
||||||
|
val isNew = !instances.containsKey(instanceId)
|
||||||
instances.put(instanceId, metadata)
|
instances.put(instanceId, metadata)
|
||||||
|
if isNew then
|
||||||
|
log.infof("Instance %s joined registry (subscriptions=%d)", instanceId, metadata.subscriptionCount)
|
||||||
|
else
|
||||||
|
log.debugf(
|
||||||
|
"Instance %s updated (subscriptions=%d state=%s)",
|
||||||
|
instanceId,
|
||||||
|
metadata.subscriptionCount,
|
||||||
|
metadata.state,
|
||||||
|
)
|
||||||
Uni.createFrom().item(())
|
Uni.createFrom().item(())
|
||||||
catch case ex: Exception =>
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
log.warnf(ex, "Failed to parse instance metadata for %s", instanceId)
|
log.warnf(ex, "Failed to parse instance metadata for %s", instanceId)
|
||||||
Uni.createFrom().item(())
|
Uni.createFrom().item(())
|
||||||
}
|
}
|
||||||
@@ -52,8 +63,8 @@ class InstanceRegistry:
|
|||||||
|
|
||||||
def markInstanceDead(instanceId: String): Unit =
|
def markInstanceDead(instanceId: String): Unit =
|
||||||
instances.computeIfPresent(instanceId, (_, inst) => inst.copy(state = "DEAD"))
|
instances.computeIfPresent(instanceId, (_, inst) => inst.copy(state = "DEAD"))
|
||||||
()
|
log.infof("Instance %s marked dead", instanceId)
|
||||||
|
|
||||||
def removeInstance(instanceId: String): Unit =
|
def removeInstance(instanceId: String): Unit =
|
||||||
instances.remove(instanceId)
|
instances.remove(instanceId)
|
||||||
()
|
log.infof("Instance %s removed from registry", instanceId)
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import scala.compiletime.uninitialized
|
|||||||
import de.nowchess.coordinator.proto.{CoordinatorServiceGrpc, *}
|
import de.nowchess.coordinator.proto.{CoordinatorServiceGrpc, *}
|
||||||
import de.nowchess.chess.redis.GameRedisSubscriberManager
|
import de.nowchess.chess.redis.GameRedisSubscriberManager
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.jboss.logging.Logger
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
@GrpcService
|
@GrpcService
|
||||||
@Singleton
|
@Singleton
|
||||||
class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServiceImplBase:
|
class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServiceImplBase:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[CoordinatorServiceHandler])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject
|
@Inject
|
||||||
private var gameSubscriberManager: GameRedisSubscriberManager = uninitialized
|
private var gameSubscriberManager: GameRedisSubscriberManager = uninitialized
|
||||||
@@ -22,6 +26,7 @@ class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServic
|
|||||||
responseObserver: StreamObserver[BatchResubscribeResponse],
|
responseObserver: StreamObserver[BatchResubscribeResponse],
|
||||||
): Unit =
|
): Unit =
|
||||||
val count = gameSubscriberManager.batchResubscribeGames(request.getGameIdsList)
|
val count = gameSubscriberManager.batchResubscribeGames(request.getGameIdsList)
|
||||||
|
log.infof("Coordinator: batch resubscribe %d games → subscribed %d", request.getGameIdsList.size(), count)
|
||||||
val response = BatchResubscribeResponse
|
val response = BatchResubscribeResponse
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setSubscribedCount(count)
|
.setSubscribedCount(count)
|
||||||
@@ -34,6 +39,7 @@ class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServic
|
|||||||
responseObserver: StreamObserver[UnsubscribeGamesResponse],
|
responseObserver: StreamObserver[UnsubscribeGamesResponse],
|
||||||
): Unit =
|
): Unit =
|
||||||
val count = gameSubscriberManager.unsubscribeGames(request.getGameIdsList)
|
val count = gameSubscriberManager.unsubscribeGames(request.getGameIdsList)
|
||||||
|
log.infof("Coordinator: unsubscribe %d games → unsubscribed %d", request.getGameIdsList.size(), count)
|
||||||
val response = UnsubscribeGamesResponse
|
val response = UnsubscribeGamesResponse
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setUnsubscribedCount(count)
|
.setUnsubscribedCount(count)
|
||||||
@@ -46,6 +52,7 @@ class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServic
|
|||||||
responseObserver: StreamObserver[EvictGamesResponse],
|
responseObserver: StreamObserver[EvictGamesResponse],
|
||||||
): Unit =
|
): Unit =
|
||||||
val count = gameSubscriberManager.evictGames(request.getGameIdsList)
|
val count = gameSubscriberManager.evictGames(request.getGameIdsList)
|
||||||
|
log.infof("Coordinator: evict %d games → evicted %d", request.getGameIdsList.size(), count)
|
||||||
val response = EvictGamesResponse
|
val response = EvictGamesResponse
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setEvictedCount(count)
|
.setEvictedCount(count)
|
||||||
@@ -58,6 +65,7 @@ class CoordinatorServiceHandler extends CoordinatorServiceGrpc.CoordinatorServic
|
|||||||
responseObserver: StreamObserver[DrainInstanceResponse],
|
responseObserver: StreamObserver[DrainInstanceResponse],
|
||||||
): Unit =
|
): Unit =
|
||||||
val migrated = gameSubscriberManager.drainInstance()
|
val migrated = gameSubscriberManager.drainInstance()
|
||||||
|
log.infof("Coordinator: drain instance → migrated %d games", migrated)
|
||||||
val response = DrainInstanceResponse
|
val response = DrainInstanceResponse
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setGamesMigrated(migrated)
|
.setGamesMigrated(migrated)
|
||||||
|
|||||||
@@ -5,29 +5,63 @@ import de.nowchess.chess.client.CombinedExportResponse
|
|||||||
import de.nowchess.core.proto.*
|
import de.nowchess.core.proto.*
|
||||||
import io.quarkus.grpc.GrpcClient
|
import io.quarkus.grpc.GrpcClient
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import org.jboss.logging.Logger
|
||||||
|
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class IoGrpcClientWrapper:
|
class IoGrpcClientWrapper:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[IoGrpcClientWrapper])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@GrpcClient("io-grpc")
|
@GrpcClient("io-grpc")
|
||||||
var stub: IoServiceGrpc.IoServiceBlockingStub = uninitialized
|
var stub: IoServiceGrpc.IoServiceBlockingStub = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
def exportCombined(ctx: GameContext): CombinedExportResponse =
|
def exportCombined(ctx: GameContext): CombinedExportResponse =
|
||||||
|
try
|
||||||
val combined = stub.exportCombined(CoreProtoMapper.toProtoGameContext(ctx))
|
val combined = stub.exportCombined(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
CombinedExportResponse(combined.getFen, combined.getPgn)
|
CombinedExportResponse(combined.getFen, combined.getPgn)
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "IO gRPC exportCombined failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def importFen(fen: String): GameContext =
|
def importFen(fen: String): GameContext =
|
||||||
CoreProtoMapper.fromProtoGameContext(stub.importFen(ProtoImportFenRequest.newBuilder().setFen(fen).build()))
|
try CoreProtoMapper.fromProtoGameContext(stub.importFen(ProtoImportFenRequest.newBuilder().setFen(fen).build()))
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "IO gRPC importFen failed for fen %s", fen)
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def importPgn(pgn: String): GameContext =
|
def importPgn(pgn: String): GameContext =
|
||||||
CoreProtoMapper.fromProtoGameContext(stub.importPgn(ProtoImportPgnRequest.newBuilder().setPgn(pgn).build()))
|
try CoreProtoMapper.fromProtoGameContext(stub.importPgn(ProtoImportPgnRequest.newBuilder().setPgn(pgn).build()))
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "IO gRPC importPgn failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def exportFen(ctx: GameContext): String =
|
def exportFen(ctx: GameContext): String =
|
||||||
stub.exportFen(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
try stub.exportFen(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "IO gRPC exportFen failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def exportPgn(ctx: GameContext): String =
|
def exportPgn(ctx: GameContext): String =
|
||||||
stub.exportPgn(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
try stub.exportPgn(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "IO gRPC exportPgn failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
|
|||||||
import de.nowchess.core.proto.*
|
import de.nowchess.core.proto.*
|
||||||
import io.quarkus.grpc.GrpcClient
|
import io.quarkus.grpc.GrpcClient
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import org.jboss.logging.Logger
|
||||||
|
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
@@ -14,28 +15,59 @@ import scala.jdk.CollectionConverters.*
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class RuleSetGrpcAdapter extends RuleSet:
|
class RuleSetGrpcAdapter extends RuleSet:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[RuleSetGrpcAdapter])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@GrpcClient("rule-grpc")
|
@GrpcClient("rule-grpc")
|
||||||
var stub: RuleServiceGrpc.RuleServiceBlockingStub = uninitialized
|
var stub: RuleServiceGrpc.RuleServiceBlockingStub = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
def candidateMoves(ctx: GameContext)(sq: Square): List[Move] =
|
def candidateMoves(ctx: GameContext)(sq: Square): List[Move] =
|
||||||
|
try
|
||||||
val req =
|
val req =
|
||||||
ProtoSquareRequest.newBuilder().setContext(CoreProtoMapper.toProtoGameContext(ctx)).setSquare(sq.toString).build()
|
ProtoSquareRequest
|
||||||
|
.newBuilder()
|
||||||
|
.setContext(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
|
.setSquare(sq.toString)
|
||||||
|
.build()
|
||||||
stub.candidateMoves(req).getMovesList.asScala.flatMap(CoreProtoMapper.fromProtoMove).toList
|
stub.candidateMoves(req).getMovesList.asScala.flatMap(CoreProtoMapper.fromProtoMove).toList
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Rule gRPC candidateMoves failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def legalMoves(ctx: GameContext)(sq: Square): List[Move] =
|
def legalMoves(ctx: GameContext)(sq: Square): List[Move] =
|
||||||
|
try
|
||||||
val req =
|
val req =
|
||||||
ProtoSquareRequest.newBuilder().setContext(CoreProtoMapper.toProtoGameContext(ctx)).setSquare(sq.toString).build()
|
ProtoSquareRequest
|
||||||
|
.newBuilder()
|
||||||
|
.setContext(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
|
.setSquare(sq.toString)
|
||||||
|
.build()
|
||||||
stub.legalMoves(req).getMovesList.asScala.flatMap(CoreProtoMapper.fromProtoMove).toList
|
stub.legalMoves(req).getMovesList.asScala.flatMap(CoreProtoMapper.fromProtoMove).toList
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Rule gRPC legalMoves failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def allLegalMoves(ctx: GameContext): List[Move] =
|
def allLegalMoves(ctx: GameContext): List[Move] =
|
||||||
|
try
|
||||||
stub
|
stub
|
||||||
.allLegalMoves(CoreProtoMapper.toProtoGameContext(ctx))
|
.allLegalMoves(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
.getMovesList
|
.getMovesList
|
||||||
.asScala
|
.asScala
|
||||||
.flatMap(CoreProtoMapper.fromProtoMove)
|
.flatMap(CoreProtoMapper.fromProtoMove)
|
||||||
.toList
|
.toList
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Rule gRPC allLegalMoves failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
def isCheck(ctx: GameContext): Boolean =
|
def isCheck(ctx: GameContext): Boolean =
|
||||||
stub.isCheck(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
stub.isCheck(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
||||||
@@ -56,14 +88,22 @@ class RuleSetGrpcAdapter extends RuleSet:
|
|||||||
stub.isThreefoldRepetition(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
stub.isThreefoldRepetition(CoreProtoMapper.toProtoGameContext(ctx)).getValue
|
||||||
|
|
||||||
def applyMove(ctx: GameContext)(move: Move): GameContext =
|
def applyMove(ctx: GameContext)(move: Move): GameContext =
|
||||||
|
try
|
||||||
val req = ProtoMoveRequest
|
val req = ProtoMoveRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setContext(CoreProtoMapper.toProtoGameContext(ctx))
|
.setContext(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
.setMove(CoreProtoMapper.toProtoMove(move))
|
.setMove(CoreProtoMapper.toProtoMove(move))
|
||||||
.build()
|
.build()
|
||||||
CoreProtoMapper.fromProtoGameContext(stub.applyMove(req))
|
CoreProtoMapper.fromProtoGameContext(stub.applyMove(req))
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Rule gRPC applyMove failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
override def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
override def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
||||||
|
try
|
||||||
val p = stub.postMoveStatus(CoreProtoMapper.toProtoGameContext(ctx))
|
val p = stub.postMoveStatus(CoreProtoMapper.toProtoGameContext(ctx))
|
||||||
PostMoveStatus(
|
PostMoveStatus(
|
||||||
p.getIsCheckmate,
|
p.getIsCheckmate,
|
||||||
@@ -72,3 +112,9 @@ class RuleSetGrpcAdapter extends RuleSet:
|
|||||||
p.getIsCheck,
|
p.getIsCheck,
|
||||||
p.getIsThreefoldRepetition,
|
p.getIsThreefoldRepetition,
|
||||||
)
|
)
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Rule gRPC postMoveStatus failed")
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
throw ex
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import jakarta.enterprise.context.ApplicationScoped
|
|||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
|
import org.jboss.logging.Logger
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.{MessageDigest, SecureRandom}
|
import java.security.{MessageDigest, SecureRandom}
|
||||||
@@ -35,6 +36,7 @@ class RedisGameRegistry extends GameRegistry:
|
|||||||
@Inject @RestClient var storeClient: StoreServiceClient = uninitialized
|
@Inject @RestClient var storeClient: StoreServiceClient = uninitialized
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[RedisGameRegistry])
|
||||||
private val localEngines = ConcurrentHashMap[String, GameEntry]()
|
private val localEngines = ConcurrentHashMap[String, GameEntry]()
|
||||||
private val rng = new SecureRandom()
|
private val rng = new SecureRandom()
|
||||||
|
|
||||||
@@ -48,6 +50,12 @@ class RedisGameRegistry extends GameRegistry:
|
|||||||
localEngines.put(entry.gameId, entry)
|
localEngines.put(entry.gameId, entry)
|
||||||
val combined = ioClient.exportCombined(entry.engine.context)
|
val combined = ioClient.exportCombined(entry.engine.context)
|
||||||
redis.value(classOf[String]).setex(cacheKey(entry.gameId), 1800L, toJson(entry, combined.fen, combined.pgn))
|
redis.value(classOf[String]).setex(cacheKey(entry.gameId), 1800L, toJson(entry, combined.fen, combined.pgn))
|
||||||
|
log.infof(
|
||||||
|
"Stored game %s in registry (white=%s black=%s)",
|
||||||
|
entry.gameId,
|
||||||
|
entry.white.displayName,
|
||||||
|
entry.black.displayName,
|
||||||
|
)
|
||||||
|
|
||||||
def get(gameId: String): Option[GameEntry] =
|
def get(gameId: String): Option[GameEntry] =
|
||||||
Option(localEngines.get(gameId)) match
|
Option(localEngines.get(gameId)) match
|
||||||
@@ -71,9 +79,15 @@ class RedisGameRegistry extends GameRegistry:
|
|||||||
|
|
||||||
private def fromRedis(gameId: String): Option[GameEntry] =
|
private def fromRedis(gameId: String): Option[GameEntry] =
|
||||||
readRedisDto(gameId)
|
readRedisDto(gameId)
|
||||||
.flatMap(dto => Try(reconstruct(dto)).toOption)
|
.flatMap { dto =>
|
||||||
|
Try(reconstruct(dto)).toOption.orElse {
|
||||||
|
log.warnf("Failed to reconstruct game %s from Redis", gameId)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
.map { entry =>
|
.map { entry =>
|
||||||
localEngines.put(gameId, entry)
|
localEngines.put(gameId, entry)
|
||||||
|
log.infof("Loaded game %s from Redis cache", gameId)
|
||||||
entry
|
entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +116,15 @@ class RedisGameRegistry extends GameRegistry:
|
|||||||
pendingDrawOffer = Option(record.pendingDrawOffer),
|
pendingDrawOffer = Option(record.pendingDrawOffer),
|
||||||
)
|
)
|
||||||
(dto, reconstruct(dto))
|
(dto, reconstruct(dto))
|
||||||
}.toOption
|
} match
|
||||||
.map { case (dto, entry) =>
|
case scala.util.Success((dto, entry)) =>
|
||||||
|
log.infof("Loaded game %s from store service", gameId)
|
||||||
localEngines.put(gameId, entry)
|
localEngines.put(gameId, entry)
|
||||||
redis.value(classOf[String]).setex(cacheKey(gameId), 1800L, objectMapper.writeValueAsString(dto))
|
redis.value(classOf[String]).setex(cacheKey(gameId), 1800L, objectMapper.writeValueAsString(dto))
|
||||||
entry
|
Some(entry)
|
||||||
}
|
case scala.util.Failure(ex) =>
|
||||||
|
log.warnf(ex, "Failed to load game %s from store service", gameId)
|
||||||
|
None
|
||||||
|
|
||||||
private def reconstruct(dto: GameCacheDto): GameEntry =
|
private def reconstruct(dto: GameCacheDto): GameEntry =
|
||||||
val ctx = if dto.pgn.nonEmpty then ioClient.importPgn(dto.pgn) else GameContext.initial
|
val ctx = if dto.pgn.nonEmpty then ioClient.importPgn(dto.pgn) else GameContext.initial
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import jakarta.inject.Inject
|
|||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
import jakarta.ws.rs.core.{MediaType, Response}
|
import jakarta.ws.rs.core.{MediaType, Response}
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken
|
import org.eclipse.microprofile.jwt.JsonWebToken
|
||||||
|
import org.jboss.logging.Logger
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
@@ -38,6 +39,8 @@ import scala.compiletime.uninitialized
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class GameResource:
|
class GameResource:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[GameResource])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject
|
@Inject
|
||||||
var registry: GameRegistry = uninitialized
|
var registry: GameRegistry = uninitialized
|
||||||
@@ -165,7 +168,13 @@ class GameResource:
|
|||||||
val entry = newEntry(GameContext.initial, white, black, tc, mode)
|
val entry = newEntry(GameContext.initial, white, black, tc, mode)
|
||||||
registry.store(entry)
|
registry.store(entry)
|
||||||
subscriberManager.subscribeGame(entry.gameId)
|
subscriberManager.subscribeGame(entry.gameId)
|
||||||
println(s"Created game ${entry.gameId}")
|
log.infof(
|
||||||
|
"Game %s created — white=%s black=%s mode=%s",
|
||||||
|
entry.gameId,
|
||||||
|
white.displayName,
|
||||||
|
black.displayName,
|
||||||
|
mode.toString,
|
||||||
|
)
|
||||||
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -182,6 +191,7 @@ class GameResource:
|
|||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
assertGameNotOver(entry)
|
assertGameNotOver(entry)
|
||||||
val color = colorOf(entry)
|
val color = colorOf(entry)
|
||||||
|
log.infof("Game %s — resign by %s", gameId, color.label)
|
||||||
entry.engine.resign(color)
|
entry.engine.resign(color)
|
||||||
registry.update(entry.copy(resigned = true))
|
registry.update(entry.copy(resigned = true))
|
||||||
ok(OkResponseDto())
|
ok(OkResponseDto())
|
||||||
@@ -194,6 +204,7 @@ class GameResource:
|
|||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
assertGameNotOver(entry)
|
assertGameNotOver(entry)
|
||||||
assertIsCurrentPlayer(entry)
|
assertIsCurrentPlayer(entry)
|
||||||
|
log.debugf("Game %s — move %s by %s", gameId, uci, colorOf(entry).label)
|
||||||
if Parser.parseMove(uci).isEmpty then
|
if Parser.parseMove(uci).isEmpty then
|
||||||
throw BadRequestException("INVALID_UCI", s"Invalid UCI notation: $uci", Some("uci"))
|
throw BadRequestException("INVALID_UCI", s"Invalid UCI notation: $uci", Some("uci"))
|
||||||
applyMoveInput(entry.engine, uci).foreach(err => throw BadRequestException("INVALID_MOVE", err, Some("uci")))
|
applyMoveInput(entry.engine, uci).foreach(err => throw BadRequestException("INVALID_MOVE", err, Some("uci")))
|
||||||
@@ -284,6 +295,7 @@ class GameResource:
|
|||||||
val entry = newEntry(ctx, white, black, tc)
|
val entry = newEntry(ctx, white, black, tc)
|
||||||
registry.store(entry)
|
registry.store(entry)
|
||||||
subscriberManager.subscribeGame(entry.gameId)
|
subscriberManager.subscribeGame(entry.gameId)
|
||||||
|
log.infof("Imported FEN game %s", entry.gameId)
|
||||||
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@@ -295,6 +307,7 @@ class GameResource:
|
|||||||
val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
|
val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
|
||||||
registry.store(entry)
|
registry.store(entry)
|
||||||
subscriberManager.subscribeGame(entry.gameId)
|
subscriberManager.subscribeGame(entry.gameId)
|
||||||
|
log.infof("Imported PGN game %s", entry.gameId)
|
||||||
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
created(GameDtoMapper.toGameFullDto(entry, ioClient))
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
./gradlew test
|
./gradlew test -Dquarkus.profile=test
|
||||||
|
|
||||||
if [ "$#" -eq 0 ]; then
|
if [ "$#" -eq 0 ]; then
|
||||||
PYTHONUTF8=1 python3 jacoco-reporter/test_gaps.py
|
PYTHONUTF8=1 python3 jacoco-reporter/test_gaps.py
|
||||||
|
|||||||
Reference in New Issue
Block a user