Merge pull request #2711 from wucm667/fix/db-pool-enforce-conn-lifetime-floor
fix(db-pool): 强制连接生命期下限,缓解 lib/pq watchCancel 协程泄漏
This commit is contained in:
commit
57bae985a7
@ -1,12 +1,26 @@
|
|||||||
|
// Package repository contains persistence infrastructure helpers.
|
||||||
|
//
|
||||||
|
// DB pool lifetimes are clamped here because lib/pq starts watchCancel
|
||||||
|
// goroutines for context-aware queries. If a cloud proxy silently drops idle
|
||||||
|
// TCP without RST/FIN, those goroutines can block in Read until database/sql
|
||||||
|
// retires the connection. This is a short-term mitigation; the long-term
|
||||||
|
// follow-up is migrating PostgreSQL access to jackc/pgx/v5/stdlib.
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultConnMaxLifetime = 30 * time.Minute
|
||||||
|
defaultConnMaxIdleTime = 5 * time.Minute
|
||||||
|
maxConfiguredConnAge = 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
type dbPoolSettings struct {
|
type dbPoolSettings struct {
|
||||||
MaxOpenConns int
|
MaxOpenConns int
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
@ -14,19 +28,41 @@ type dbPoolSettings struct {
|
|||||||
ConnMaxIdleTime time.Duration
|
ConnMaxIdleTime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDBPoolSettings(cfg *config.Config) dbPoolSettings {
|
func clampDBPoolSettings(cfg *config.Config) dbPoolSettings {
|
||||||
return dbPoolSettings{
|
return dbPoolSettings{
|
||||||
MaxOpenConns: cfg.Database.MaxOpenConns,
|
MaxOpenConns: cfg.Database.MaxOpenConns,
|
||||||
MaxIdleConns: cfg.Database.MaxIdleConns,
|
MaxIdleConns: cfg.Database.MaxIdleConns,
|
||||||
ConnMaxLifetime: time.Duration(cfg.Database.ConnMaxLifetimeMinutes) * time.Minute,
|
ConnMaxLifetime: clampDBPoolDuration("database.conn_max_lifetime_minutes", cfg.Database.ConnMaxLifetimeMinutes, defaultConnMaxLifetime),
|
||||||
ConnMaxIdleTime: time.Duration(cfg.Database.ConnMaxIdleTimeMinutes) * time.Minute,
|
ConnMaxIdleTime: clampDBPoolDuration("database.conn_max_idle_time_minutes", cfg.Database.ConnMaxIdleTimeMinutes, defaultConnMaxIdleTime),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clampDBPoolDuration(key string, minutes int, fallback time.Duration) time.Duration {
|
||||||
|
if minutes <= 0 || minutes > int(maxConfiguredConnAge/time.Minute) {
|
||||||
|
slog.Warn("database connection pool duration clamped",
|
||||||
|
"key", key,
|
||||||
|
"before", minutes,
|
||||||
|
"after", int(fallback/time.Minute),
|
||||||
|
)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(minutes) * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
func applyDBPoolSettings(db *sql.DB, cfg *config.Config) {
|
func applyDBPoolSettings(db *sql.DB, cfg *config.Config) {
|
||||||
settings := buildDBPoolSettings(cfg)
|
settings := clampDBPoolSettings(cfg)
|
||||||
db.SetMaxOpenConns(settings.MaxOpenConns)
|
db.SetMaxOpenConns(settings.MaxOpenConns)
|
||||||
db.SetMaxIdleConns(settings.MaxIdleConns)
|
db.SetMaxIdleConns(settings.MaxIdleConns)
|
||||||
db.SetConnMaxLifetime(settings.ConnMaxLifetime)
|
db.SetConnMaxLifetime(settings.ConnMaxLifetime)
|
||||||
db.SetConnMaxIdleTime(settings.ConnMaxIdleTime)
|
db.SetConnMaxIdleTime(settings.ConnMaxIdleTime)
|
||||||
|
|
||||||
|
slog.Info("database connection pool configured",
|
||||||
|
slog.Group("effective",
|
||||||
|
slog.Int("max_open", settings.MaxOpenConns),
|
||||||
|
slog.Int("max_idle", settings.MaxIdleConns),
|
||||||
|
slog.Duration("max_lifetime", settings.ConnMaxLifetime),
|
||||||
|
slog.Duration("max_idle_time", settings.ConnMaxIdleTime),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,21 +11,62 @@ import (
|
|||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildDBPoolSettings(t *testing.T) {
|
func TestClampDBPoolSettings(t *testing.T) {
|
||||||
cfg := &config.Config{
|
tests := []struct {
|
||||||
Database: config.DatabaseConfig{
|
name string
|
||||||
MaxOpenConns: 50,
|
connMaxLifetime int
|
||||||
MaxIdleConns: 10,
|
connMaxIdleTime int
|
||||||
ConnMaxLifetimeMinutes: 30,
|
wantMaxLifetime time.Duration
|
||||||
ConnMaxIdleTimeMinutes: 5,
|
wantConnMaxIdleTime time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero values fall back to safe defaults",
|
||||||
|
connMaxLifetime: 0,
|
||||||
|
connMaxIdleTime: 0,
|
||||||
|
wantMaxLifetime: 30 * time.Minute,
|
||||||
|
wantConnMaxIdleTime: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative values fall back to safe defaults",
|
||||||
|
connMaxLifetime: -1,
|
||||||
|
connMaxIdleTime: -5,
|
||||||
|
wantMaxLifetime: 30 * time.Minute,
|
||||||
|
wantConnMaxIdleTime: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reasonable values pass through",
|
||||||
|
connMaxLifetime: 15,
|
||||||
|
connMaxIdleTime: 3,
|
||||||
|
wantMaxLifetime: 15 * time.Minute,
|
||||||
|
wantConnMaxIdleTime: 3 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "values over twenty four hours fall back to safe defaults",
|
||||||
|
connMaxLifetime: 24*60 + 1,
|
||||||
|
connMaxIdleTime: 24*60 + 1,
|
||||||
|
wantMaxLifetime: 30 * time.Minute,
|
||||||
|
wantConnMaxIdleTime: 5 * time.Minute,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := buildDBPoolSettings(cfg)
|
for _, tt := range tests {
|
||||||
require.Equal(t, 50, settings.MaxOpenConns)
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
require.Equal(t, 10, settings.MaxIdleConns)
|
cfg := &config.Config{
|
||||||
require.Equal(t, 30*time.Minute, settings.ConnMaxLifetime)
|
Database: config.DatabaseConfig{
|
||||||
require.Equal(t, 5*time.Minute, settings.ConnMaxIdleTime)
|
MaxOpenConns: 50,
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
ConnMaxLifetimeMinutes: tt.connMaxLifetime,
|
||||||
|
ConnMaxIdleTimeMinutes: tt.connMaxIdleTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := clampDBPoolSettings(cfg)
|
||||||
|
require.Equal(t, 50, settings.MaxOpenConns)
|
||||||
|
require.Equal(t, 10, settings.MaxIdleConns)
|
||||||
|
require.Equal(t, tt.wantMaxLifetime, settings.ConnMaxLifetime)
|
||||||
|
require.Equal(t, tt.wantConnMaxIdleTime, settings.ConnMaxIdleTime)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyDBPoolSettings(t *testing.T) {
|
func TestApplyDBPoolSettings(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user