perf(postgres): 回收突发连接并等待验收副作用
失败 Run 的未提交任务已取消,但退款 outbox 尚未完成时下一轮会继承 API 连接高水位和 canceled acquire 增量,污染容量验收。 新增 AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS,生产配置 30 秒回收突发空闲连接;验收中止后等待旧任务、退款、回调和数据库连接全部收敛后才允许新负载启动。 验证:Go 全量测试、gofmt、bash -n、ShellCheck、kubectl kustomize 和 git diff --check 通过。
This commit is contained in:
@@ -40,6 +40,7 @@ func main() {
|
||||
db, err := store.ConnectWithPoolOptions(ctx, cfg.DatabaseURL, store.PostgresPoolOptions{
|
||||
MaxConns: cfg.DatabaseMaxConns,
|
||||
MinIdleConns: cfg.DatabaseMinIdleConns,
|
||||
MaxConnIdleTime: time.Duration(cfg.DatabaseMaxConnIdleSeconds) * time.Second,
|
||||
IdleInTransactionTimeout: time.Duration(cfg.DatabaseIdleInTransactionTimeoutSeconds) * time.Second,
|
||||
LockTimeout: time.Duration(cfg.DatabaseLockTimeoutSeconds) * time.Second,
|
||||
})
|
||||
|
||||
@@ -60,6 +60,7 @@ type Config struct {
|
||||
ProcessRole string
|
||||
DatabaseMaxConns int
|
||||
DatabaseMinIdleConns int
|
||||
DatabaseMaxConnIdleSeconds int
|
||||
DatabaseIdleInTransactionTimeoutSeconds int
|
||||
DatabaseLockTimeoutSeconds int
|
||||
MediaRequestConcurrency int
|
||||
@@ -124,6 +125,7 @@ func Load() Config {
|
||||
ProcessRole: strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))),
|
||||
DatabaseMaxConns: envInt("AI_GATEWAY_DATABASE_MAX_CONNS", 0),
|
||||
DatabaseMinIdleConns: envOptionalIntValidated("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", 0),
|
||||
DatabaseMaxConnIdleSeconds: envOptionalIntValidated("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS", 0),
|
||||
|
||||
DatabaseIdleInTransactionTimeoutSeconds: envOptionalIntValidated(
|
||||
"AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS",
|
||||
@@ -153,6 +155,9 @@ func (c Config) Validate() error {
|
||||
(c.DatabaseMaxConns > 0 && c.DatabaseMinIdleConns > c.DatabaseMaxConns) {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must be between 0 and AI_GATEWAY_DATABASE_MAX_CONNS")
|
||||
}
|
||||
if c.DatabaseMaxConnIdleSeconds < 0 || c.DatabaseMaxConnIdleSeconds > 3600 {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS must be between 0 and 3600")
|
||||
}
|
||||
if c.DatabaseIdleInTransactionTimeoutSeconds < 0 || c.DatabaseIdleInTransactionTimeoutSeconds > 3600 {
|
||||
return errors.New("AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS must be between 0 and 3600")
|
||||
}
|
||||
|
||||
@@ -145,11 +145,13 @@ func TestProcessRolePrecedenceAndCompatibility(t *testing.T) {
|
||||
func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
|
||||
cfg := Load()
|
||||
if cfg.DatabaseMinIdleConns != 0 ||
|
||||
cfg.DatabaseMaxConnIdleSeconds != 0 ||
|
||||
cfg.DatabaseIdleInTransactionTimeoutSeconds != 60 ||
|
||||
cfg.DatabaseLockTimeoutSeconds != 30 {
|
||||
t.Fatalf(
|
||||
"database pool minimum/transaction timeouts = %d/%d/%d, want 0/60/30",
|
||||
"database pool minimum/max-idle/transaction timeouts = %d/%d/%d/%d, want 0/0/60/30",
|
||||
cfg.DatabaseMinIdleConns,
|
||||
cfg.DatabaseMaxConnIdleSeconds,
|
||||
cfg.DatabaseIdleInTransactionTimeoutSeconds,
|
||||
cfg.DatabaseLockTimeoutSeconds,
|
||||
)
|
||||
@@ -174,6 +176,11 @@ func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
|
||||
t.Fatalf("Validate() error = %v, want invalid non-integer database min idle conns", err)
|
||||
}
|
||||
t.Setenv("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", "4")
|
||||
cfg.DatabaseMaxConnIdleSeconds = 3601
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "DATABASE_MAX_CONN_IDLE_SECONDS") {
|
||||
t.Fatalf("Validate() error = %v, want invalid database max connection idle seconds", err)
|
||||
}
|
||||
cfg.DatabaseMaxConnIdleSeconds = 30
|
||||
cfg.DatabaseIdleInTransactionTimeoutSeconds = 3601
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "IDLE_IN_TRANSACTION") {
|
||||
t.Fatalf("Validate() error = %v, want invalid idle transaction timeout", err)
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
type PostgresPoolOptions struct {
|
||||
MaxConns int
|
||||
MinIdleConns int
|
||||
MaxConnIdleTime time.Duration
|
||||
IdleInTransactionTimeout time.Duration
|
||||
LockTimeout time.Duration
|
||||
}
|
||||
@@ -132,6 +133,9 @@ func postgresPoolConfigWithOptions(databaseURL string, options PostgresPoolOptio
|
||||
config.MaxConns = int32(options.MaxConns)
|
||||
}
|
||||
config.MinIdleConns = int32(min(max(options.MinIdleConns, 0), int(config.MaxConns)))
|
||||
if options.MaxConnIdleTime > 0 {
|
||||
config.MaxConnIdleTime = options.MaxConnIdleTime
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ func TestPostgresPoolConfigSetsBoundedTransactionTimeouts(t *testing.T) {
|
||||
PostgresPoolOptions{
|
||||
MaxConns: 32,
|
||||
MinIdleConns: 4,
|
||||
MaxConnIdleTime: 30 * time.Second,
|
||||
IdleInTransactionTimeout: 60 * time.Second,
|
||||
LockTimeout: 30 * time.Second,
|
||||
},
|
||||
@@ -77,6 +78,9 @@ func TestPostgresPoolConfigSetsBoundedTransactionTimeouts(t *testing.T) {
|
||||
if config.MaxConns != 32 || config.MinIdleConns != 4 {
|
||||
t.Fatalf("pool bounds max=%d minIdle=%d, want 32/4", config.MaxConns, config.MinIdleConns)
|
||||
}
|
||||
if config.MaxConnIdleTime != 30*time.Second {
|
||||
t.Fatalf("pool max idle time=%s, want 30s", config.MaxConnIdleTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPostgresUnavailableClassifiesConnectivityFailures(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user