mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Cache live Shardok battle reads (#8715)
This commit is contained in:
@@ -653,6 +653,7 @@ scala_library(
|
||||
"HistoryBackendStartupValidation.scala",
|
||||
"PostgresGameSchemaPool.scala",
|
||||
"PostgresHistory.scala",
|
||||
"ShardokLiveBattleCache.scala",
|
||||
"SqliteHistory.scala",
|
||||
],
|
||||
visibility = [
|
||||
|
||||
@@ -64,6 +64,12 @@ trait FullGameHistory extends GameHistory {
|
||||
/** Clear all Shardok results for a battle (used when tutorial battle resets). */
|
||||
def withResetShardokResults(shardokGameId: ShardokGameId): FullGameHistory
|
||||
|
||||
/**
|
||||
* Releases any in-process Shardok read projection after a battle ends. Durable histories may lazily reload it if a
|
||||
* later caller needs completed-battle data; histories without a read cache can use this default no-op.
|
||||
*/
|
||||
def evictShardokReadCache(shardokGameId: ShardokGameId): Unit = ()
|
||||
|
||||
/** Release any held storage resources. In-memory histories can use the default no-op. */
|
||||
def close(): Unit = ()
|
||||
|
||||
|
||||
@@ -22,21 +22,25 @@ class PostgresHistory private[service] (
|
||||
private val gameId: GameId,
|
||||
initialCount: Int,
|
||||
initialShardokResultCounts: Map[ShardokGameId, Int] = Map.empty,
|
||||
initialShardokGameStates: Map[ShardokGameId, com.google.protobuf.ByteString] = Map.empty
|
||||
initialShardokGameStates: Map[ShardokGameId, com.google.protobuf.ByteString] = Map.empty,
|
||||
initialShardokLiveBattleCaches: Map[ShardokGameId, ShardokLiveBattleCache] = Map.empty
|
||||
) extends FullGameHistory {
|
||||
import PostgresHistory.{applier, SnapshotInterval}
|
||||
|
||||
private val schemaName: String = PostgresHistory.schemaNameForGame(gameId)
|
||||
private var cachedCount: Int = initialCount
|
||||
private val dbLock = new Object
|
||||
private val actionCache = mutable.Map.empty[Int, ActionResultT]
|
||||
private val stateCache = mutable.Map.empty[Int, GameState]
|
||||
private val schemaName: String = PostgresHistory.schemaNameForGame(gameId)
|
||||
private var cachedCount: Int = initialCount
|
||||
private val dbLock = new Object
|
||||
private val actionCache = mutable.Map.empty[Int, ActionResultT]
|
||||
private val stateCache = mutable.Map.empty[Int, GameState]
|
||||
// Shardok reconnects need these values while assembling a subscription. Keep them synchronized with committed
|
||||
// writes so reconnect setup never waits for the shared Postgres connection pool.
|
||||
private val shardokResultCounts = mutable.Map.from(initialShardokResultCounts)
|
||||
private val shardokGameStates = mutable.Map.from(initialShardokGameStates)
|
||||
private var activeConnection = Option.empty[Connection]
|
||||
private var closed = false
|
||||
private val shardokResultCounts = mutable.Map.from(initialShardokResultCounts)
|
||||
private val shardokGameStates = mutable.Map.from(initialShardokGameStates)
|
||||
// Player views and current commands are a live read projection. Cache misses hydrate one battle from PostgreSQL;
|
||||
// committed Shardok updates then keep the projection current until battle resolution evicts it.
|
||||
private val shardokLiveBattleCaches = mutable.Map.from(initialShardokLiveBattleCaches)
|
||||
private var activeConnection = Option.empty[Connection]
|
||||
private var closed = false
|
||||
|
||||
private def connection: Connection =
|
||||
activeConnection.getOrElse(
|
||||
@@ -58,12 +62,14 @@ class PostgresHistory private[service] (
|
||||
}
|
||||
}
|
||||
|
||||
private def withDbLock[A](f: => A): A =
|
||||
private def withLock[A](f: => A): A =
|
||||
dbLock.synchronized {
|
||||
if closed then throw new EagleInternalException(s"PostgresHistory for game ${gameId.toHexString} is closed")
|
||||
withConnection(f)
|
||||
f
|
||||
}
|
||||
|
||||
private def withDbLock[A](f: => A): A = withLock(withConnection(f))
|
||||
|
||||
override def count: Int = dbLock.synchronized(cachedCount)
|
||||
|
||||
override def emittedTutorialDialogues(factionId: FactionId): Map[String, RoundId] = withDbLock {
|
||||
@@ -112,6 +118,7 @@ class PostgresHistory private[service] (
|
||||
override def close(): Unit =
|
||||
dbLock.synchronized {
|
||||
closed = true
|
||||
shardokLiveBattleCaches.clear()
|
||||
}
|
||||
|
||||
private[service] def createSqliteSaveExport(
|
||||
@@ -226,6 +233,7 @@ class PostgresHistory private[service] (
|
||||
stateCache.keys.filter(_ > targetActionCount).toVector.foreach(stateCache.remove)
|
||||
shardokResultCounts.keys.filterNot(validBattleIds).toVector.foreach(shardokResultCounts.remove)
|
||||
shardokGameStates.keys.filterNot(validBattleIds).toVector.foreach(shardokGameStates.remove)
|
||||
shardokLiveBattleCaches.keys.filterNot(validBattleIds).toVector.foreach(shardokLiveBattleCaches.remove)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
@@ -362,73 +370,39 @@ class PostgresHistory private[service] (
|
||||
}
|
||||
|
||||
override def shardokCount(shardokGameId: ShardokGameId): Int =
|
||||
dbLock.synchronized {
|
||||
if closed then throw new EagleInternalException(s"PostgresHistory for game ${gameId.toHexString} is closed")
|
||||
withLock {
|
||||
shardokResultCounts.getOrElse(shardokGameId, 0)
|
||||
}
|
||||
|
||||
override def shardokGameState(shardokGameId: ShardokGameId): com.google.protobuf.ByteString =
|
||||
dbLock.synchronized {
|
||||
if closed then throw new EagleInternalException(s"PostgresHistory for game ${gameId.toHexString} is closed")
|
||||
withLock {
|
||||
shardokGameStates.getOrElse(shardokGameId, com.google.protobuf.ByteString.EMPTY)
|
||||
}
|
||||
|
||||
override def shardokPlayerCount(shardokGameId: ShardokGameId, factionId: FactionId): Int = withDbLock {
|
||||
resolveFactionForViews(shardokGameId, factionId).map { fid =>
|
||||
val stmt = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM shardok_player_results WHERE shardok_game_id = ? AND faction_id = ?"
|
||||
)
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
stmt.setInt(2, fid)
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then rs.getInt(1) else 0
|
||||
} finally stmt.close()
|
||||
}
|
||||
.getOrElse(0)
|
||||
}
|
||||
override def shardokPlayerCount(shardokGameId: ShardokGameId, factionId: FactionId): Int =
|
||||
withLock(shardokLiveBattleCache(shardokGameId).playerResultsFor(factionId).size)
|
||||
|
||||
override def shardokPlayerResultsSince(
|
||||
shardokGameId: ShardokGameId,
|
||||
factionId: FactionId,
|
||||
startingCount: Int
|
||||
): Vector[ShardokActionResultView] = withDbLock {
|
||||
resolveFactionForViews(shardokGameId, factionId).map { fid =>
|
||||
val total = shardokPlayerCount(shardokGameId, fid)
|
||||
if total < startingCount then
|
||||
throw new EagleInternalException(
|
||||
s"Wanted $startingCount results but there were only $total for factionId $factionId in game $shardokGameId"
|
||||
)
|
||||
val stmt = connection.prepareStatement(
|
||||
"SELECT payload FROM shardok_player_results WHERE shardok_game_id = ? AND faction_id = ? AND seq >= ? ORDER BY seq"
|
||||
): Vector[ShardokActionResultView] = withLock {
|
||||
val results = shardokLiveBattleCache(shardokGameId).playerResultsFor(factionId)
|
||||
if results.size < startingCount then
|
||||
throw new EagleInternalException(
|
||||
s"Wanted $startingCount results but there were only ${results.size} for factionId $factionId in game $shardokGameId"
|
||||
)
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
stmt.setInt(2, fid)
|
||||
stmt.setInt(3, startingCount)
|
||||
val rs = stmt.executeQuery()
|
||||
val builder = Vector.newBuilder[ShardokActionResultView]
|
||||
while rs.next() do builder += ShardokActionResultView.parseFrom(rs.getBytes("payload"))
|
||||
builder.result()
|
||||
} finally stmt.close()
|
||||
}
|
||||
.getOrElse(Vector.empty)
|
||||
results.drop(startingCount)
|
||||
}
|
||||
|
||||
override def shardokPlayerAvailableCommands(
|
||||
shardokGameId: ShardokGameId,
|
||||
factionId: FactionId
|
||||
): Option[ShardokAvailableCommands] = withDbLock {
|
||||
val stmt = connection.prepareStatement(
|
||||
"SELECT payload FROM shardok_player_commands WHERE shardok_game_id = ? AND faction_id = ?"
|
||||
)
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
stmt.setInt(2, factionId)
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then Option(rs.getBytes("payload")).map(ShardokAvailableCommands.parseFrom)
|
||||
else None
|
||||
} finally stmt.close()
|
||||
): Option[ShardokAvailableCommands] =
|
||||
withLock(shardokLiveBattleCache(shardokGameId).availableCommandsFor(factionId))
|
||||
|
||||
override def evictShardokReadCache(shardokGameId: ShardokGameId): Unit = withLock {
|
||||
shardokLiveBattleCaches.remove(shardokGameId): Unit
|
||||
}
|
||||
|
||||
override def withNewShardokResults(
|
||||
@@ -442,7 +416,8 @@ class PostgresHistory private[service] (
|
||||
val originalAutoCommit = connection.getAutoCommit
|
||||
connection.setAutoCommit(false)
|
||||
try {
|
||||
val existingResultCount = shardokCount(shardokGameId)
|
||||
val existingResultCount = shardokResultCounts.getOrElse(shardokGameId, 0)
|
||||
val existingLiveCache = shardokLiveBattleCache(shardokGameId)
|
||||
if newResults.nonEmpty then {
|
||||
val insertStmt = connection.prepareStatement(
|
||||
"INSERT INTO shardok_results (shardok_game_id, action_seq, payload) VALUES (?, ?, ?)"
|
||||
@@ -459,22 +434,8 @@ class PostgresHistory private[service] (
|
||||
} finally insertStmt.close()
|
||||
}
|
||||
|
||||
val oldFids = {
|
||||
val stmt =
|
||||
connection.prepareStatement("SELECT faction_id FROM shardok_player_commands WHERE shardok_game_id = ?")
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
val rs = stmt.executeQuery()
|
||||
val builder = Set.newBuilder[FactionId]
|
||||
while rs.next() do builder += rs.getInt("faction_id")
|
||||
builder.result()
|
||||
} finally stmt.close()
|
||||
}
|
||||
val allFids = (oldFids ++ newPlayerResults.map(_._1).toSet).toVector
|
||||
val allFids = (existingLiveCache.registeredFactionIds ++ newPlayerResults.map(_._1)).toVector.sorted
|
||||
|
||||
val seqStmt = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM shardok_player_results WHERE shardok_game_id = ? AND faction_id = ?"
|
||||
)
|
||||
val insertViewStmt = connection.prepareStatement(
|
||||
"INSERT INTO shardok_player_results (shardok_game_id, faction_id, seq, payload) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
@@ -485,12 +446,7 @@ class PostgresHistory private[service] (
|
||||
)
|
||||
try {
|
||||
allFids.foreach { fid =>
|
||||
val existingViewCount = {
|
||||
seqStmt.setString(1, shardokGameId)
|
||||
seqStmt.setInt(2, fid)
|
||||
val rs = seqStmt.executeQuery()
|
||||
if rs.next() then rs.getInt(1) else 0
|
||||
}
|
||||
val existingViewCount = existingLiveCache.exactPlayerResults(fid).size
|
||||
val viewsForFid = newPlayerResults.find(_._1 == fid).fold(Vector.empty)(_._2)
|
||||
viewsForFid.zipWithIndex.foreach {
|
||||
case (view, i) =>
|
||||
@@ -511,7 +467,6 @@ class PostgresHistory private[service] (
|
||||
insertViewStmt.executeBatch(): Unit
|
||||
upsertCmdStmt.executeBatch(): Unit
|
||||
} finally {
|
||||
seqStmt.close()
|
||||
insertViewStmt.close()
|
||||
upsertCmdStmt.close()
|
||||
}
|
||||
@@ -531,6 +486,7 @@ class PostgresHistory private[service] (
|
||||
connection.commit()
|
||||
shardokResultCounts(shardokGameId) = existingResultCount + newResults.size
|
||||
shardokGameStates(shardokGameId) = com.google.protobuf.ByteString.copyFrom(currentGameState)
|
||||
shardokLiveBattleCaches(shardokGameId) = existingLiveCache.appended(newPlayerResults, availableCommands)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
@@ -559,6 +515,7 @@ class PostgresHistory private[service] (
|
||||
connection.commit()
|
||||
shardokResultCounts.remove(shardokGameId): Unit
|
||||
shardokGameStates.remove(shardokGameId): Unit
|
||||
shardokLiveBattleCaches.remove(shardokGameId): Unit
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
@@ -583,6 +540,7 @@ class PostgresHistory private[service] (
|
||||
shardokExport.states.foreach { state =>
|
||||
shardokGameStates(state.shardokGameId) = com.google.protobuf.ByteString.copyFrom(state.gameState)
|
||||
}
|
||||
shardokLiveBattleCaches.clear()
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
@@ -663,28 +621,11 @@ class PostgresHistory private[service] (
|
||||
builder.result()
|
||||
}
|
||||
|
||||
private def resolveFactionForViews(shardokGameId: ShardokGameId, factionId: FactionId): Option[FactionId] = {
|
||||
val stmt = connection.prepareStatement(
|
||||
"SELECT 1 FROM shardok_player_commands WHERE shardok_game_id = ? AND faction_id = ? LIMIT 1"
|
||||
private def shardokLiveBattleCache(shardokGameId: ShardokGameId): ShardokLiveBattleCache =
|
||||
shardokLiveBattleCaches.getOrElseUpdate(
|
||||
shardokGameId,
|
||||
withConnection(PostgresHistory.readShardokLiveBattleCache(connection, shardokGameId))
|
||||
)
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
stmt.setInt(2, factionId)
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then Some(factionId)
|
||||
else {
|
||||
val fallback = connection.prepareStatement(
|
||||
"SELECT 1 FROM shardok_player_commands WHERE shardok_game_id = ? AND faction_id = -1 LIMIT 1"
|
||||
)
|
||||
try {
|
||||
fallback.setString(1, shardokGameId)
|
||||
val rs2 = fallback.executeQuery()
|
||||
if rs2.next() then Some(-1) else None
|
||||
} finally fallback.close()
|
||||
}
|
||||
} finally stmt.close()
|
||||
end try
|
||||
}
|
||||
|
||||
private def deleteWhereAtOrAfter(table: String, column: String, targetActionCount: Int): Unit = {
|
||||
val stmt = connection.prepareStatement(s"DELETE FROM $table WHERE $column >= ?")
|
||||
@@ -874,6 +815,43 @@ object PostgresHistory {
|
||||
} finally stmt.close()
|
||||
}
|
||||
|
||||
private[service] def readShardokLiveBattleCache(
|
||||
connection: Connection,
|
||||
shardokGameId: ShardokGameId
|
||||
): ShardokLiveBattleCache = {
|
||||
val sql =
|
||||
"""SELECT 0 AS row_type, faction_id, seq, payload
|
||||
|FROM shardok_player_results
|
||||
|WHERE shardok_game_id = ?
|
||||
|UNION ALL
|
||||
|SELECT 1 AS row_type, faction_id, NULL AS seq, payload
|
||||
|FROM shardok_player_commands
|
||||
|WHERE shardok_game_id = ?
|
||||
|ORDER BY faction_id, row_type, seq""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
try {
|
||||
stmt.setString(1, shardokGameId)
|
||||
stmt.setString(2, shardokGameId)
|
||||
val rs = stmt.executeQuery()
|
||||
val resultsByFaction = mutable.Map.empty[FactionId, Vector[ShardokActionResultView]]
|
||||
val commandsByFaction = Map.newBuilder[FactionId, Option[ShardokAvailableCommands]]
|
||||
while rs.next() do {
|
||||
val factionId = rs.getInt("faction_id")
|
||||
rs.getInt("row_type") match {
|
||||
case 0 =>
|
||||
val result = ShardokActionResultView.parseFrom(rs.getBytes("payload"))
|
||||
resultsByFaction(factionId) = resultsByFaction.getOrElse(factionId, Vector.empty) :+ result
|
||||
case 1 =>
|
||||
commandsByFaction += factionId -> Option(rs.getBytes("payload")).map(ShardokAvailableCommands.parseFrom)
|
||||
case rowType =>
|
||||
throw new EagleInternalException(s"Unexpected Shardok live cache row type $rowType")
|
||||
}
|
||||
}
|
||||
ShardokLiveBattleCache(resultsByFaction.toMap, commandsByFaction.result())
|
||||
} finally stmt.close()
|
||||
end try
|
||||
}
|
||||
|
||||
private[service] def schemaNameForGame(gameId: GameId): String = {
|
||||
val schema = "eagle_game_" + java.lang.Long.toHexString(gameId)
|
||||
if !schema.matches("[a-z][a-z0-9_]*") then
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.eagle0.eagle.service
|
||||
|
||||
import net.eagle0.eagle.FactionId
|
||||
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
|
||||
import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAvailableCommands
|
||||
|
||||
/**
|
||||
* In-process read projection for one active Shardok battle. PostgreSQL remains the durable source of truth, while this
|
||||
* projection avoids rereading player views and current commands on every client update.
|
||||
*
|
||||
* A key in `availableCommandsByFaction` records that the faction participates in this battle even when its value is
|
||||
* `None`. That distinction preserves the persisted `NULL` command row and the `-1` spectator fallback behavior.
|
||||
*/
|
||||
private[service] final case class ShardokLiveBattleCache(
|
||||
playerResultsByFaction: Map[FactionId, Vector[ShardokActionResultView]],
|
||||
availableCommandsByFaction: Map[FactionId, Option[ShardokAvailableCommands]]
|
||||
) {
|
||||
def registeredFactionIds: Set[FactionId] = availableCommandsByFaction.keySet
|
||||
|
||||
def exactPlayerResults(factionId: FactionId): Vector[ShardokActionResultView] =
|
||||
playerResultsByFaction.getOrElse(factionId, Vector.empty)
|
||||
|
||||
def playerResultsFor(factionId: FactionId): Vector[ShardokActionResultView] =
|
||||
resolvedFactionId(factionId).fold(Vector.empty)(exactPlayerResults)
|
||||
|
||||
def availableCommandsFor(factionId: FactionId): Option[ShardokAvailableCommands] =
|
||||
availableCommandsByFaction.get(factionId).flatten
|
||||
|
||||
def appended(
|
||||
newPlayerResults: Vector[(FactionId, Vector[ShardokActionResultView])],
|
||||
availableCommands: Vector[(FactionId, Option[ShardokAvailableCommands])]
|
||||
): ShardokLiveBattleCache = {
|
||||
val allFactionIds = registeredFactionIds ++ newPlayerResults.map(_._1)
|
||||
val updatedResults = allFactionIds.foldLeft(playerResultsByFaction) { (results, factionId) =>
|
||||
val appended = newPlayerResults.find(_._1 == factionId).fold(Vector.empty)(_._2)
|
||||
results.updated(factionId, results.getOrElse(factionId, Vector.empty) ++ appended)
|
||||
}
|
||||
val updatedCommands = allFactionIds.foldLeft(availableCommandsByFaction) { (commands, factionId) =>
|
||||
commands.updated(factionId, availableCommands.find(_._1 == factionId).flatMap(_._2))
|
||||
}
|
||||
copy(
|
||||
playerResultsByFaction = updatedResults,
|
||||
availableCommandsByFaction = updatedCommands
|
||||
)
|
||||
}
|
||||
|
||||
private def resolvedFactionId(factionId: FactionId): Option[FactionId] =
|
||||
if availableCommandsByFaction.contains(factionId) then Some(factionId)
|
||||
else Option.when(availableCommandsByFaction.contains(-1))(-1)
|
||||
}
|
||||
@@ -752,6 +752,9 @@ final case class GameController(
|
||||
.toSet
|
||||
val cleanedProgress = updatedProgress.filter { case (id, _) => remainingBattleIds.contains(id) }
|
||||
|
||||
if !remainingBattleIds.contains(shardokGameId) then
|
||||
result.gameController.fullHistory.evictShardokReadCache(shardokGameId)
|
||||
|
||||
// Reset gated round when all battles have ended so the next battle starts at day 0
|
||||
val cleanedLastRevealedRound =
|
||||
if cleanedProgress.isEmpty then 0
|
||||
|
||||
@@ -38,6 +38,8 @@ scala_test(
|
||||
timeout = "short",
|
||||
srcs = ["PostgresHistorySchemaBatchTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/service:changed_province_table_writer",
|
||||
"//src/main/scala/net/eagle0/eagle/service:sqlite_history",
|
||||
|
||||
@@ -3,6 +3,8 @@ package net.eagle0.eagle.service
|
||||
import java.sql.{Connection, PreparedStatement, ResultSet, Statement}
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
|
||||
import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAvailableCommands
|
||||
import org.scalamock.scalatest.MockFactory
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -231,6 +233,69 @@ class PostgresHistorySchemaBatchTest extends AnyFlatSpec with Matchers with Mock
|
||||
history.shardokGameState("unknown") shouldBe ByteString.EMPTY
|
||||
}
|
||||
|
||||
"Shardok live battle cache" should "serve player results with spectator fallback and exact commands" in {
|
||||
val firstResult = ShardokActionResultView()
|
||||
val secondResult = ShardokActionResultView()
|
||||
val availableCommand = ShardokAvailableCommands()
|
||||
val cache = ShardokLiveBattleCache(
|
||||
playerResultsByFaction = Map(
|
||||
1 -> Vector(firstResult, secondResult),
|
||||
-1 -> Vector(firstResult)
|
||||
),
|
||||
availableCommandsByFaction = Map(
|
||||
1 -> Some(availableCommand),
|
||||
-1 -> None
|
||||
)
|
||||
)
|
||||
|
||||
cache.playerResultsFor(1) should have size 2
|
||||
cache.playerResultsFor(999) should have size 1
|
||||
cache.availableCommandsFor(1) shouldBe Some(availableCommand)
|
||||
cache.availableCommandsFor(999) shouldBe None
|
||||
cache.registeredFactionIds shouldBe Set(-1, 1)
|
||||
}
|
||||
|
||||
it should "append views and replace commands after a committed update" in {
|
||||
val existingResult = ShardokActionResultView()
|
||||
val appendedResult = ShardokActionResultView()
|
||||
val spectatorResult = ShardokActionResultView()
|
||||
val availableCommand = ShardokAvailableCommands()
|
||||
val cache = ShardokLiveBattleCache(
|
||||
playerResultsByFaction = Map(1 -> Vector(existingResult), -1 -> Vector(spectatorResult)),
|
||||
availableCommandsByFaction = Map(1 -> Some(availableCommand), -1 -> None)
|
||||
)
|
||||
|
||||
val updated = cache.appended(
|
||||
newPlayerResults = Vector(1 -> Vector(appendedResult)),
|
||||
availableCommands = Vector(1 -> None)
|
||||
)
|
||||
|
||||
updated.exactPlayerResults(1) shouldBe Vector(existingResult, appendedResult)
|
||||
updated.exactPlayerResults(-1) shouldBe Vector(spectatorResult)
|
||||
updated.availableCommandsFor(1) shouldBe None
|
||||
updated.registeredFactionIds shouldBe Set(-1, 1)
|
||||
}
|
||||
|
||||
it should "let PostgresHistory answer repeated reads without a database connection" in {
|
||||
val result = ShardokActionResultView()
|
||||
val availableCommand = ShardokAvailableCommands()
|
||||
val history = new PostgresHistory(
|
||||
gameId = 0x1234L,
|
||||
initialCount = 1,
|
||||
initialShardokLiveBattleCaches = Map(
|
||||
"battle" -> ShardokLiveBattleCache(
|
||||
playerResultsByFaction = Map(1 -> Vector(result)),
|
||||
availableCommandsByFaction = Map(1 -> Some(availableCommand))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
history.shardokPlayerCount("battle", 1) shouldBe 1
|
||||
history.shardokPlayerResultsSince("battle", 1, 0) shouldBe Vector(result)
|
||||
history.shardokPlayerResultsSince("battle", 1, 1) shouldBe Vector.empty
|
||||
history.shardokPlayerAvailableCommands("battle", 1) shouldBe Some(availableCommand)
|
||||
}
|
||||
|
||||
"readShardokSubscriptionState" should "load counts and states in one query" in {
|
||||
val connection = mock[Connection]
|
||||
val statement = mock[Statement]
|
||||
@@ -256,4 +321,42 @@ class PostgresHistorySchemaBatchTest extends AnyFlatSpec with Matchers with Mock
|
||||
counts shouldBe Map("battle-with-state" -> 3, "battle-without-state" -> 2)
|
||||
states shouldBe Map("battle-with-state" -> ByteString.copyFrom(Array[Byte](1, 2, 3)))
|
||||
}
|
||||
|
||||
"readShardokLiveBattleCache" should "hydrate results and nullable commands in one query" in {
|
||||
val connection = mock[Connection]
|
||||
val statement = mock[PreparedStatement]
|
||||
val resultSet = mock[ResultSet]
|
||||
val result = ShardokActionResultView()
|
||||
val availableCommand = ShardokAvailableCommands()
|
||||
|
||||
(connection.prepareStatement(_: String)).expects(*).returning(statement).once(): Unit
|
||||
statement.setString.expects(1, "battle").once(): Unit
|
||||
statement.setString.expects(2, "battle").once(): Unit
|
||||
(() => statement.executeQuery()).expects().returning(resultSet).once(): Unit
|
||||
inSequence {
|
||||
(() => resultSet.next()).expects().returning(true).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("faction_id").returning(1).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("row_type").returning(0).once(): Unit
|
||||
(resultSet.getBytes(_: String)).expects("payload").returning(result.toByteArray).once(): Unit
|
||||
|
||||
(() => resultSet.next()).expects().returning(true).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("faction_id").returning(-1).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("row_type").returning(1).once(): Unit
|
||||
(resultSet.getBytes(_: String)).expects("payload").returning(null).once(): Unit
|
||||
|
||||
(() => resultSet.next()).expects().returning(true).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("faction_id").returning(1).once(): Unit
|
||||
(resultSet.getInt(_: String)).expects("row_type").returning(1).once(): Unit
|
||||
(resultSet.getBytes(_: String)).expects("payload").returning(availableCommand.toByteArray).once(): Unit
|
||||
|
||||
(() => resultSet.next()).expects().returning(false).once(): Unit
|
||||
}
|
||||
(() => statement.close()).expects().once(): Unit
|
||||
|
||||
val cache = PostgresHistory.readShardokLiveBattleCache(connection, "battle")
|
||||
|
||||
cache.exactPlayerResults(1) shouldBe Vector(result)
|
||||
cache.availableCommandsFor(1) shouldBe Some(availableCommand)
|
||||
cache.registeredFactionIds shouldBe Set(-1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user