From 92aedde121fa526727516048839e44509c499ab9 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 9 Jul 2026 17:18:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(db):=20=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?SQLServer=20=E9=A6=96=E6=AC=A1=E6=9F=A5=E8=AF=A2=E8=80=97?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 SQLServer 连接池保留 1 条空闲连接,减少反复冷连接开销 - 复用 Oracle/OceanBase 的默认空闲连接常量,其他默认数据库池策略保持不变 - 新增连接池行为测试,覆盖 SQLServer 复用与默认池关闭 idle 连接 --- internal/db/sql_pool.go | 9 +++- internal/db/sql_pool_test.go | 102 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 internal/db/sql_pool_test.go diff --git a/internal/db/sql_pool.go b/internal/db/sql_pool.go index 13620c3f..e8f2608c 100644 --- a/internal/db/sql_pool.go +++ b/internal/db/sql_pool.go @@ -8,6 +8,7 @@ import ( const ( defaultSQLMaxOpenConns = 4 + defaultSQLMaxIdleConns = 1 defaultSQLConnMaxLifetime = 30 * time.Minute defaultSQLConnMaxIdleTime = 30 * time.Second ) @@ -21,7 +22,13 @@ func configureSQLConnectionPool(db *sql.DB, dbType string) { return case "oracle", "oceanbase": db.SetMaxOpenConns(defaultSQLMaxOpenConns) - db.SetMaxIdleConns(1) + db.SetMaxIdleConns(defaultSQLMaxIdleConns) + db.SetConnMaxIdleTime(defaultSQLConnMaxIdleTime) + db.SetConnMaxLifetime(defaultSQLConnMaxLifetime) + return + case "sqlserver": + db.SetMaxOpenConns(defaultSQLMaxOpenConns) + db.SetMaxIdleConns(defaultSQLMaxIdleConns) db.SetConnMaxIdleTime(defaultSQLConnMaxIdleTime) db.SetConnMaxLifetime(defaultSQLConnMaxLifetime) return diff --git a/internal/db/sql_pool_test.go b/internal/db/sql_pool_test.go new file mode 100644 index 00000000..45f4c947 --- /dev/null +++ b/internal/db/sql_pool_test.go @@ -0,0 +1,102 @@ +package db + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "sync/atomic" + "testing" +) + +const poolRecordingDriverName = "gonavi_pool_recording" + +var ( + poolRecordingOpenCount atomic.Int64 + poolRecordingCloseCount atomic.Int64 +) + +func init() { + sql.Register(poolRecordingDriverName, poolRecordingDriver{}) +} + +type poolRecordingDriver struct{} + +func (poolRecordingDriver) Open(name string) (driver.Conn, error) { + poolRecordingOpenCount.Add(1) + return poolRecordingConn{}, nil +} + +type poolRecordingConn struct{} + +func (poolRecordingConn) Prepare(query string) (driver.Stmt, error) { + return nil, errors.New("not implemented") +} + +func (poolRecordingConn) Close() error { + poolRecordingCloseCount.Add(1) + return nil +} + +func (poolRecordingConn) Begin() (driver.Tx, error) { + return nil, errors.New("not implemented") +} + +func (poolRecordingConn) Ping(ctx context.Context) error { + return nil +} + +func resetPoolRecordingDriverCounters() { + poolRecordingOpenCount.Store(0) + poolRecordingCloseCount.Store(0) +} + +func openConfiguredPoolForTest(t *testing.T, dbType string) *sql.DB { + t.Helper() + resetPoolRecordingDriverCounters() + dbConn, err := sql.Open(poolRecordingDriverName, t.Name()) + if err != nil { + t.Fatalf("sql.Open failed: %v", err) + } + configureSQLConnectionPool(dbConn, dbType) + t.Cleanup(func() { + _ = dbConn.Close() + }) + return dbConn +} + +func TestConfigureSQLConnectionPoolKeepsOneIdleSQLServerConnection(t *testing.T) { + dbConn := openConfiguredPoolForTest(t, "sqlserver") + + if err := dbConn.PingContext(context.Background()); err != nil { + t.Fatalf("first ping failed: %v", err) + } + if err := dbConn.PingContext(context.Background()); err != nil { + t.Fatalf("second ping failed: %v", err) + } + + if got := poolRecordingOpenCount.Load(); got != 1 { + t.Fatalf("expected SQL Server pool to reuse one idle connection, opened %d connections", got) + } + if got := poolRecordingCloseCount.Load(); got != 0 { + t.Fatalf("expected SQL Server idle connection to remain cached before DB close, closed %d connections", got) + } +} + +func TestConfigureSQLConnectionPoolDefaultDoesNotKeepIdleConnections(t *testing.T) { + dbConn := openConfiguredPoolForTest(t, "mysql") + + if err := dbConn.PingContext(context.Background()); err != nil { + t.Fatalf("first ping failed: %v", err) + } + if err := dbConn.PingContext(context.Background()); err != nil { + t.Fatalf("second ping failed: %v", err) + } + + if got := poolRecordingOpenCount.Load(); got != 2 { + t.Fatalf("expected default pool to reopen without idle cache, opened %d connections", got) + } + if got := poolRecordingCloseCount.Load(); got != 2 { + t.Fatalf("expected default pool to close each returned connection, closed %d connections", got) + } +}