Defer schema pool refill after game creation (#8533)

This commit is contained in:
2026-07-14 06:37:21 -07:00
committed by GitHub
parent f52d54b7c2
commit 15d7d30ccf
3 changed files with 109 additions and 11 deletions
@@ -2111,6 +2111,21 @@ class GamesManager(
gameId: GameId,
humanPlayers: Vector[WaitingGamePlayerInfo],
aiPlayerCount: Int
): GameControllerWithPostResults =
PostgresGameSchemaPool.withRefillDeferred {
synchronizedCreateGameWithRefillDeferred(
expandedGameParameters = expandedGameParameters,
gameId = gameId,
humanPlayers = humanPlayers,
aiPlayerCount = aiPlayerCount
)
}
private def synchronizedCreateGameWithRefillDeferred(
expandedGameParameters: ExpandedGameParameters,
gameId: GameId,
humanPlayers: Vector[WaitingGamePlayerInfo],
aiPlayerCount: Int
): GameControllerWithPostResults = JfrEvents.createGamePhase(gameId.toHexString, "synchronizedCreateGame.total") { _ =>
this.synchronized {
val logger = SimpleTimedLogger.printLogger
@@ -1,8 +1,8 @@
package net.eagle0.eagle.service
import java.sql.Connection
import java.util.concurrent.{Executors, TimeUnit}
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.Executors
import scala.util.control.NonFatal
@@ -11,18 +11,47 @@ import net.eagle0.eagle.client_text.PostgresClientTextStore
import net.eagle0.eagle.postgres.PostgresConnectionProvider
import net.eagle0.eagle.GameId
private[service] final class DeferredRefillCoordinator(triggerRefill: () => Unit) {
private val deferralDepth = ThreadLocal.withInitial[Integer](() => 0)
private val refillDeferred = ThreadLocal.withInitial[java.lang.Boolean](() => false)
def requestRefill(): Unit =
if deferralDepth.get() > 0 then refillDeferred.set(true)
else triggerRefill()
def withRefillDeferred[A](work: => A): A = {
val isOutermost = deferralDepth.get() == 0
deferralDepth.set(deferralDepth.get() + 1)
try work
finally {
deferralDepth.set(deferralDepth.get() - 1)
if isOutermost then {
val shouldRefill = refillDeferred.get()
deferralDepth.remove()
refillDeferred.remove()
if shouldRefill then triggerRefill()
}
}
}
}
private[service] object PostgresGameSchemaPool {
private val PoolSize = PostgresConnectionProvider.envInt("EAGLE_POSTGRES_GAME_SCHEMA_POOL_SIZE", 3).max(0)
private val PoolLockName = "eagle0_game_schema_pool"
private val PoolPrefix =
private val PoolSize =
PostgresConnectionProvider.envInt("EAGLE_POSTGRES_GAME_SCHEMA_POOL_SIZE", 3).max(0)
private val PoolLockName = "eagle0_game_schema_pool"
private val PostCreationRefillDelayMillis = 1000L
private val PoolPrefix =
s"eagle_pool_h${PostgresHistory.SchemaVersion}_t${PostgresClientTextStore.SchemaVersion}_"
private val started = new AtomicBoolean(false)
private val refillMonitor = new Object
private var refillPending = false
private var refillRunning = false
private val started = new AtomicBoolean(false)
private val delayedRefillScheduled = new AtomicBoolean(false)
private val refillMonitor = new Object
private var refillPending = false
private var refillRunning = false
private val refillExecutor = Executors.newSingleThreadExecutor { runnable =>
private val deferredRefillCoordinator = new DeferredRefillCoordinator(() => scheduleRefillAfterCreation())
private val refillExecutor = Executors.newSingleThreadScheduledExecutor { runnable =>
val thread = new Thread(runnable, "postgres-game-schema-pool")
thread.setDaemon(true)
thread
@@ -35,7 +64,7 @@ private[service] object PostgresGameSchemaPool {
case exception: Exception =>
log(s"Could not synchronously prepare the first schema: ${exception.getMessage}")
}
requestRefill()
requestRefillNow()
}
def claimForGame(connection: Connection, gameId: GameId): Boolean =
@@ -74,7 +103,26 @@ private[service] object PostgresGameSchemaPool {
case None => false
}
private def requestRefill(): Unit = refillMonitor.synchronized {
private def requestRefill(): Unit =
deferredRefillCoordinator.requestRefill()
private[service] def withRefillDeferred[A](work: => A): A =
deferredRefillCoordinator.withRefillDeferred(work)
private def scheduleRefillAfterCreation(): Unit =
if delayedRefillScheduled.compareAndSet(false, true) then
refillExecutor.schedule(
new Runnable {
override def run(): Unit = {
delayedRefillScheduled.set(false)
requestRefillNow()
}
},
PostCreationRefillDelayMillis,
TimeUnit.MILLISECONDS
): Unit
private def requestRefillNow(): Unit = refillMonitor.synchronized {
refillPending = true
if !refillRunning then {
refillRunning = true
@@ -7,6 +7,41 @@ import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class PostgresGameSchemaPoolTest extends AnyFlatSpec with Matchers with MockFactory {
"DeferredRefillCoordinator" should "trigger refill requests made outside creation" in {
var refillCount = 0
val coordinator = new DeferredRefillCoordinator(() => refillCount += 1)
coordinator.requestRefill()
refillCount shouldBe 1
}
it should "coalesce refill requests until creation finishes" in {
val events = scala.collection.mutable.ArrayBuffer.empty[String]
val coordinator = new DeferredRefillCoordinator(() => events += "refill")
coordinator.withRefillDeferred {
events += "creation"
coordinator.requestRefill()
coordinator.requestRefill()
events shouldBe Vector("creation")
}
events shouldBe Vector("creation", "refill")
}
it should "release a deferred refill when creation fails" in {
val events = scala.collection.mutable.ArrayBuffer.empty[String]
val coordinator = new DeferredRefillCoordinator(() => events += "refill")
an[IllegalStateException] should be thrownBy coordinator.withRefillDeferred {
coordinator.requestRefill()
throw new IllegalStateException("creation failed")
}
events shouldBe Vector("refill")
}
"poolSchemaNames" should "include both schema versions" in {
PostgresGameSchemaPool.poolSchemaNames(2) shouldBe Vector(
"eagle_pool_h1_t2_0",