Pool Postgres connections behind provider (#8372)

* Pool Postgres connections behind provider

* Use smaller production Postgres pool default
This commit is contained in:
2026-07-09 10:47:54 -07:00
committed by GitHub
parent 09a2f0bf30
commit 6274ba4195
9 changed files with 200 additions and 79 deletions
+1
View File
@@ -365,6 +365,7 @@ maven.install(
"org.xerial:sqlite-jdbc:3.53.2.0",
# Postgres for production history benchmarking and migration work
"com.zaxxer:HikariCP:6.3.3",
"org.postgresql:postgresql:42.7.11",
],
duplicate_version_warning = "error",
+14
View File
@@ -39,6 +39,13 @@ services:
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}"
EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}"
EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}"
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-60000}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
# Auth token for Shardok on Hetzner (required)
@@ -95,6 +102,13 @@ services:
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}"
EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}"
EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}"
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-60000}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
+22
View File
@@ -29,6 +29,7 @@
"com.thesamet.scalapb:scalapb-json4s_3": -772830803,
"com.thesamet.scalapb:scalapb-runtime-grpc_3": -2026114331,
"com.thesamet.scalapb:scalapb-runtime_3": -1281968352,
"com.zaxxer:HikariCP": 1858991878,
"io.netty:netty-buffer": 732437843,
"io.netty:netty-codec": -1199307927,
"io.netty:netty-codec-http": -1456497900,
@@ -117,6 +118,7 @@
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 97373115,
"com.thesamet.scalapb:scalapb-runtime_3": 818778125,
"com.typesafe:config": -1791448454,
"com.zaxxer:HikariCP": -704610647,
"commons-codec:commons-codec": 1377992676,
"commons-io:commons-io": 669567296,
"dev.dirs:directories": 1716880629,
@@ -550,6 +552,12 @@
},
"version": "1.4.6"
},
"com.zaxxer:HikariCP": {
"shasums": {
"jar": "709f378c05756280939ce50fc1b1f1a53bb8e1899dc1b249f21f12703640b48b"
},
"version": "6.3.3"
},
"commons-codec:commons-codec": {
"shasums": {
"jar": "f700de80ac270d0344fdea7468201d8b9c805e5c648331c3619f2ee067ccfc59"
@@ -1581,6 +1589,9 @@
"org.scala-lang.modules:scala-collection-compat_3",
"org.scala-lang:scala3-library_3"
],
"com.zaxxer:HikariCP": [
"org.slf4j:slf4j-api"
],
"io.get-coursier:coursier-cache_2.13": [
"io.get-coursier.jniutils:windows-jni-utils",
"io.get-coursier:cache-util",
@@ -2557,6 +2568,16 @@
"com.typesafe.config.impl",
"com.typesafe.config.parser"
],
"com.zaxxer:HikariCP": [
"com.zaxxer.hikari",
"com.zaxxer.hikari.hibernate",
"com.zaxxer.hikari.metrics",
"com.zaxxer.hikari.metrics.dropwizard",
"com.zaxxer.hikari.metrics.micrometer",
"com.zaxxer.hikari.metrics.prometheus",
"com.zaxxer.hikari.pool",
"com.zaxxer.hikari.util"
],
"commons-codec:commons-codec": [
"org.apache.commons.codec",
"org.apache.commons.codec.binary",
@@ -4100,6 +4121,7 @@
"com.thesamet.scalapb:scalapb-runtime-grpc_3",
"com.thesamet.scalapb:scalapb-runtime_3",
"com.typesafe:config",
"com.zaxxer:HikariCP",
"commons-codec:commons-codec",
"commons-io:commons-io",
"dev.dirs:directories",
@@ -107,6 +107,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:generated_text_request_converter",
"//src/main/scala/net/eagle0/eagle/postgres:postgres_connection_provider",
],
)
@@ -1,6 +1,6 @@
package net.eagle0.eagle.client_text
import java.sql.{Connection, DriverManager, ResultSet}
import java.sql.{Connection, ResultSet}
import java.util.concurrent.atomic.AtomicInteger
import scala.util.Try
@@ -11,6 +11,7 @@ import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
import net.eagle0.eagle.model.proto_converters.GeneratedTextRequestConverter
import net.eagle0.eagle.postgres.PostgresConnectionProvider
class PostgresClientTextStore private[client_text] (
initialConnection: Connection,
@@ -945,15 +946,14 @@ object PostgresClientTextStore {
loaded(gameId, pregenerated).get
private def openConnection(): Connection = {
val cfg = config.getOrElse(
val provider = PostgresConnectionProvider.shared.getOrElse(
throw new EagleInternalException(
"PostgresClientTextStore requires EAGLE_POSTGRES_HOST, DATABASE, USER, and PASSWORD"
)
)
Class.forName("org.postgresql.Driver")
val connection = DriverManager.getConnection(cfg.jdbcUrl, cfg.user, cfg.password)
val connection = provider.getConnection("PostgresClientTextStore")
val count = openConnections.incrementAndGet()
SimpleTimedLogger.printLogger.logLine(s"PostgresClientTextStore opened connection; openTextConnections=$count")
SimpleTimedLogger.printLogger.logLine(s"PostgresClientTextStore borrowed connection; openTextConnections=$count")
connection
}
@@ -967,28 +967,10 @@ object PostgresClientTextStore {
private def recordClosed(description: String): Unit = {
val count = openConnections.decrementAndGet()
SimpleTimedLogger.printLogger.logLine(
s"PostgresClientTextStore closed connection for $description; openTextConnections=$count"
s"PostgresClientTextStore returned connection for $description; openTextConnections=$count"
)
}
private def config: Option[PostgresConfig] =
for
host <- envNonEmpty("EAGLE_POSTGRES_HOST")
database <- envNonEmpty("EAGLE_POSTGRES_DATABASE")
user <- envNonEmpty("EAGLE_POSTGRES_USER")
password <- envNonEmpty("EAGLE_POSTGRES_PASSWORD")
yield PostgresConfig(
host = host,
port = sys.env.get("EAGLE_POSTGRES_PORT").flatMap(_.toIntOption).getOrElse(5432),
database = database,
user = user,
password = password,
sslmode = sys.env.get("EAGLE_POSTGRES_SSLMODE").filter(_.nonEmpty).getOrElse("require")
)
private def envNonEmpty(name: String): Option[String] =
sys.env.get(name).map(_.trim).filter(_.nonEmpty)
private def schemaNameForGame(gameId: GameId): String = {
val schema = "eagle_game_" + java.lang.Long.toHexString(gameId)
if !schema.matches("[a-z][a-z0-9_]*") then
@@ -1099,15 +1081,4 @@ object PostgresClientTextStore {
|)""".stripMargin
)
private case class PostgresConfig(
host: String,
port: Int,
database: String,
user: String,
password: String,
sslmode: String
) {
def jdbcUrl: String =
s"jdbc:postgresql://$host:$port/$database?sslmode=$sslmode"
}
}
@@ -0,0 +1,19 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "postgres_connection_provider",
srcs = ["PostgresConnectionProvider.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
runtime_deps = [
"@maven//:com_zaxxer_HikariCP",
"@maven//:org_postgresql_postgresql",
],
deps = [
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"@maven//:com_zaxxer_HikariCP",
],
)
@@ -0,0 +1,126 @@
package net.eagle0.eagle.postgres
import java.sql.Connection
import com.zaxxer.hikari.{HikariConfig, HikariDataSource}
import net.eagle0.common.SimpleTimedLogger
import net.eagle0.eagle.library.EagleInternalException
trait PostgresConnectionProvider extends AutoCloseable {
def getConnection(description: String): Connection
def close(): Unit
}
object PostgresConnectionProvider {
lazy val shared: Option[PostgresConnectionProvider] =
PostgresConfig.fromEnvironment.map(config => new HikariPostgresConnectionProvider(config))
def requireConfigured(): Unit =
if shared.isEmpty then
throw new EagleInternalException(
"Postgres requires EAGLE_POSTGRES_HOST, EAGLE_POSTGRES_DATABASE, " +
"EAGLE_POSTGRES_USER, and EAGLE_POSTGRES_PASSWORD"
)
def envNonEmpty(name: String): Option[String] =
sys.env.get(name).map(_.trim).filter(_.nonEmpty)
def envLong(name: String, defaultValue: Long): Long =
envNonEmpty(name).flatMap(_.toLongOption).getOrElse(defaultValue)
def envInt(name: String, defaultValue: Int): Int =
envNonEmpty(name).flatMap(_.toIntOption).getOrElse(defaultValue)
}
case class PostgresConfig(
host: String,
port: Int,
database: String,
user: String,
password: String,
sslmode: String,
maxPoolSize: Int,
minimumIdle: Int,
connectionTimeoutMillis: Long,
idleTimeoutMillis: Long,
maxLifetimeMillis: Long,
keepaliveTimeMillis: Long,
leakDetectionThresholdMillis: Long
) {
def jdbcUrl: String =
s"jdbc:postgresql://$host:$port/$database?sslmode=$sslmode"
}
object PostgresConfig {
def fromEnvironment: Option[PostgresConfig] =
for
host <- PostgresConnectionProvider.envNonEmpty("EAGLE_POSTGRES_HOST")
database <- PostgresConnectionProvider.envNonEmpty("EAGLE_POSTGRES_DATABASE")
user <- PostgresConnectionProvider.envNonEmpty("EAGLE_POSTGRES_USER")
password <- PostgresConnectionProvider.envNonEmpty("EAGLE_POSTGRES_PASSWORD")
yield PostgresConfig(
host = host,
port = PostgresConnectionProvider.envInt("EAGLE_POSTGRES_PORT", 5432),
database = database,
user = user,
password = password,
sslmode = PostgresConnectionProvider.envNonEmpty("EAGLE_POSTGRES_SSLMODE").getOrElse("require"),
maxPoolSize = PostgresConnectionProvider.envInt("EAGLE_POSTGRES_MAX_POOL_SIZE", 10),
minimumIdle = PostgresConnectionProvider.envInt("EAGLE_POSTGRES_MIN_IDLE", 0),
connectionTimeoutMillis = PostgresConnectionProvider.envLong("EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS", 10000L),
idleTimeoutMillis = PostgresConnectionProvider.envLong("EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS", 120000L),
maxLifetimeMillis = PostgresConnectionProvider.envLong("EAGLE_POSTGRES_MAX_LIFETIME_MILLIS", 1500000L),
keepaliveTimeMillis = PostgresConnectionProvider.envLong("EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS", 300000L),
leakDetectionThresholdMillis =
PostgresConnectionProvider.envLong("EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS", 60000L)
)
}
private class HikariPostgresConnectionProvider(config: PostgresConfig) extends PostgresConnectionProvider {
private val hikariConfig = {
val cfg = new HikariConfig()
cfg.setJdbcUrl(config.jdbcUrl)
cfg.setUsername(config.user)
cfg.setPassword(config.password)
cfg.setPoolName("eagle-postgres")
cfg.setMaximumPoolSize(config.maxPoolSize)
cfg.setMinimumIdle(config.minimumIdle)
cfg.setConnectionTimeout(config.connectionTimeoutMillis)
cfg.setIdleTimeout(config.idleTimeoutMillis)
cfg.setMaxLifetime(config.maxLifetimeMillis)
cfg.setKeepaliveTime(config.keepaliveTimeMillis)
cfg.setLeakDetectionThreshold(config.leakDetectionThresholdMillis)
cfg
}
private val dataSource = new HikariDataSource(hikariConfig)
Runtime.getRuntime.addShutdownHook(new Thread(() => close()))
SimpleTimedLogger.printLogger.logLine(
s"Initialized Hikari Postgres connection provider: " +
s"maxPoolSize=${config.maxPoolSize}, minimumIdle=${config.minimumIdle}, " +
s"connectionTimeoutMillis=${config.connectionTimeoutMillis}, idleTimeoutMillis=${config.idleTimeoutMillis}, " +
s"maxLifetimeMillis=${config.maxLifetimeMillis}, keepaliveTimeMillis=${config.keepaliveTimeMillis}, " +
s"leakDetectionThresholdMillis=${config.leakDetectionThresholdMillis}"
)
override def getConnection(description: String): Connection = {
val start = System.currentTimeMillis()
val connection = dataSource.getConnection()
val duration = System.currentTimeMillis() - start
if duration > 500 then
SimpleTimedLogger.printLogger.logLine(
s"Waited ${duration}ms for Postgres connection: $description; " +
s"active=${dataSource.getHikariPoolMXBean.getActiveConnections}, " +
s"idle=${dataSource.getHikariPoolMXBean.getIdleConnections}, " +
s"total=${dataSource.getHikariPoolMXBean.getTotalConnections}, " +
s"waiting=${dataSource.getHikariPoolMXBean.getThreadsAwaitingConnection}"
)
connection
}
override def close(): Unit =
SimpleTimedLogger.printLogger.logLine("Closing Hikari Postgres connection provider")
dataSource.close()
}
@@ -692,6 +692,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
"//src/main/scala/net/eagle0/eagle/postgres:postgres_connection_provider",
"//src/main/scala/net/eagle0/eagle/service/persistence:persister",
"//src/main/scala/net/eagle0/eagle/service/persistence:save_directory",
],
@@ -1,6 +1,6 @@
package net.eagle0.eagle.service
import java.sql.{Connection, DriverManager, Types}
import java.sql.{Connection, Types}
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable
@@ -14,6 +14,7 @@ import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.postgres.PostgresConnectionProvider
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAvailableCommands
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
@@ -676,7 +677,7 @@ object PostgresHistory {
}
def loaded(gameId: GameId): Option[PostgresHistory] =
config.map { _ =>
PostgresConnectionProvider.shared.map { _ =>
val connection = openConnection()
try {
val schemaName = schemaNameForGame(gameId)
@@ -705,37 +706,17 @@ object PostgresHistory {
}.getOrElse(None)
def requireConfigured(): Unit =
if config.isEmpty then
throw new EagleInternalException(
"HistoryBackend=postgres requires EAGLE_POSTGRES_HOST, EAGLE_POSTGRES_DATABASE, " +
"EAGLE_POSTGRES_USER, and EAGLE_POSTGRES_PASSWORD"
)
private def config: Option[PostgresConfig] =
for
host <- envNonEmpty("EAGLE_POSTGRES_HOST")
database <- envNonEmpty("EAGLE_POSTGRES_DATABASE")
user <- envNonEmpty("EAGLE_POSTGRES_USER")
password <- envNonEmpty("EAGLE_POSTGRES_PASSWORD")
yield PostgresConfig(
host = host,
port = sys.env.get("EAGLE_POSTGRES_PORT").flatMap(_.toIntOption).getOrElse(5432),
database = database,
user = user,
password = password,
sslmode = sys.env.get("EAGLE_POSTGRES_SSLMODE").filter(_.nonEmpty).getOrElse("require")
)
PostgresConnectionProvider.requireConfigured()
private def openConnection(): Connection = {
val cfg = config.getOrElse(
val provider = PostgresConnectionProvider.shared.getOrElse(
throw new EagleInternalException(
"EAGLE_HISTORY_BACKEND=postgres requires EAGLE_POSTGRES_HOST, DATABASE, USER, and PASSWORD"
)
)
Class.forName("org.postgresql.Driver")
val connection = DriverManager.getConnection(cfg.jdbcUrl, cfg.user, cfg.password)
val connection = provider.getConnection("PostgresHistory")
val count = openConnections.incrementAndGet()
SimpleTimedLogger.printLogger.logLine(s"PostgresHistory opened connection; openHistoryConnections=$count")
SimpleTimedLogger.printLogger.logLine(s"PostgresHistory borrowed connection; openHistoryConnections=$count")
connection
}
@@ -746,13 +727,10 @@ object PostgresHistory {
private def recordClosed(description: String): Unit = {
val count = openConnections.decrementAndGet()
SimpleTimedLogger.printLogger.logLine(
s"PostgresHistory closed connection for $description; openHistoryConnections=$count"
s"PostgresHistory returned connection for $description; openHistoryConnections=$count"
)
}
private def envNonEmpty(name: String): Option[String] =
sys.env.get(name).map(_.trim).filter(_.nonEmpty)
private def schemaNameForGame(gameId: GameId): String = {
val schema = "eagle_game_" + java.lang.Long.toHexString(gameId)
if !schema.matches("[a-z][a-z0-9_]*") then
@@ -761,7 +739,7 @@ object PostgresHistory {
}
private[service] def dropGameSchema(gameId: GameId): Unit =
config.foreach { _ =>
PostgresConnectionProvider.shared.foreach { _ =>
val connection = openConnection()
val schemaName = schemaNameForGame(gameId)
try {
@@ -772,7 +750,7 @@ object PostgresHistory {
}
private[service] def exists(gameId: GameId): Boolean =
config.exists { _ =>
PostgresConnectionProvider.shared.exists { _ =>
val connection = openConnection()
try schemaExists(connection, schemaNameForGame(gameId))
finally closeOpenedConnection(connection, s"exists check for game ${gameId.toHexString}")
@@ -1073,18 +1051,6 @@ object PostgresHistory {
} finally stmt.close()
}
private case class PostgresConfig(
host: String,
port: Int,
database: String,
user: String,
password: String,
sslmode: String
) {
def jdbcUrl: String =
s"jdbc:postgresql://$host:$port/$database?sslmode=$sslmode"
}
extension (history: PostgresHistory)
private def writeStartingSnapshot(startingState: GameState): Unit = {
val stmt = history.connection.prepareStatement(