mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-03 12:01:58 +00:00
fix(dbmanager): keep the pool alive across errors and restarts
Implements the fixes from audit/pkg/dbmanager.audit.md. - Stop closing the shared *sql.DB to recover from errors. Adapter factories and the health checker no longer call Reconnect; Reconnect is atomic and operator-only. - Postgres uses a custom driver.Connector: Reconnect retires pooled connections by generation without closing the pool, so held Bun/GORM handles keep working. Verified against a live server restart. - Add TCP keepalive, TCP_USER_TIMEOUT, a bounded reuse ping and statement_timeout as a runtime parameter; drop the 2 min timeout floor. - Health check pings without holding the connection lock. - Listener: single goroutine pair, bounded Close without UNLISTEN, and serialised use of the pgx connection (fixes conn busy and a close race). - Fix Connect/Close/Connect/Close panic, idempotent Connect, dial outside the manager lock, clean up on partial failure. - SQLite: pin :memory: to one connection, pragmas via DSN. - Escape credentials in Postgres/MSSQL/Mongo DSNs; sslmode defaults to prefer. Wire retry settings, publish metrics, fix logger calls. - NewConnectionFromDB: Close is a no-op with a warning (caller owns the pool); Reconnect only pings. - Document correct usage in the README; mark the audit with what was done. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
bc8bff7955
commit
da1af1487e
@@ -4,13 +4,8 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
|
||||
)
|
||||
|
||||
func TestNewConnectionFromDB(t *testing.T) {
|
||||
@@ -213,157 +208,3 @@ func TestNewConnectionFromDB_PostgreSQL(t *testing.T) {
|
||||
t.Errorf("Expected type DatabaseTypePostgreSQL, got '%s'", conn.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseNativeAdapterReconnectFactory(t *testing.T) {
|
||||
conn := newSQLConnection("test-native", DatabaseTypeSQLite, ConnectionConfig{
|
||||
Name: "test-native",
|
||||
Type: DatabaseTypeSQLite,
|
||||
FilePath: ":memory:",
|
||||
DefaultORM: string(ORMTypeNative),
|
||||
ConnectTimeout: 2 * time.Second,
|
||||
}, providers.NewSQLiteProvider())
|
||||
|
||||
ctx := context.Background()
|
||||
if err := conn.Connect(ctx); err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
db, err := conn.Database()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get database adapter: %v", err)
|
||||
}
|
||||
|
||||
adapter, ok := db.(*database.PgSQLAdapter)
|
||||
if !ok {
|
||||
t.Fatalf("Expected PgSQLAdapter, got %T", db)
|
||||
}
|
||||
|
||||
underlyingBefore, ok := adapter.GetUnderlyingDB().(*sql.DB)
|
||||
if !ok {
|
||||
t.Fatalf("Expected underlying *sql.DB, got %T", adapter.GetUnderlyingDB())
|
||||
}
|
||||
|
||||
if err := underlyingBefore.Close(); err != nil {
|
||||
t.Fatalf("Failed to close underlying database: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "SELECT 1"); err != nil {
|
||||
t.Fatalf("Expected native adapter to reconnect, got error: %v", err)
|
||||
}
|
||||
|
||||
underlyingAfter, ok := adapter.GetUnderlyingDB().(*sql.DB)
|
||||
if !ok {
|
||||
t.Fatalf("Expected reconnected *sql.DB, got %T", adapter.GetUnderlyingDB())
|
||||
}
|
||||
|
||||
if underlyingAfter == underlyingBefore {
|
||||
t.Fatal("Expected adapter to swap to a fresh *sql.DB after reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseBunAdapterReconnectFactory(t *testing.T) {
|
||||
conn := newSQLConnection("test-bun", DatabaseTypeSQLite, ConnectionConfig{
|
||||
Name: "test-bun",
|
||||
Type: DatabaseTypeSQLite,
|
||||
FilePath: ":memory:",
|
||||
DefaultORM: string(ORMTypeBun),
|
||||
ConnectTimeout: 2 * time.Second,
|
||||
}, providers.NewSQLiteProvider())
|
||||
|
||||
ctx := context.Background()
|
||||
if err := conn.Connect(ctx); err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
db, err := conn.Database()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get database adapter: %v", err)
|
||||
}
|
||||
|
||||
adapter, ok := db.(*database.BunAdapter)
|
||||
if !ok {
|
||||
t.Fatalf("Expected BunAdapter, got %T", db)
|
||||
}
|
||||
|
||||
underlyingBefore, ok := adapter.GetUnderlyingDB().(interface{ Close() error })
|
||||
if !ok {
|
||||
t.Fatalf("Expected underlying Bun DB with Close method, got %T", adapter.GetUnderlyingDB())
|
||||
}
|
||||
|
||||
if err := underlyingBefore.Close(); err != nil {
|
||||
t.Fatalf("Failed to close underlying Bun database: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "SELECT 1"); err != nil {
|
||||
t.Fatalf("Expected Bun adapter to reconnect, got error: %v", err)
|
||||
}
|
||||
|
||||
underlyingAfter := adapter.GetUnderlyingDB()
|
||||
if underlyingAfter == underlyingBefore {
|
||||
t.Fatal("Expected adapter to swap to a fresh Bun DB after reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseGormAdapterReconnectFactory(t *testing.T) {
|
||||
conn := newSQLConnection("test-gorm", DatabaseTypeSQLite, ConnectionConfig{
|
||||
Name: "test-gorm",
|
||||
Type: DatabaseTypeSQLite,
|
||||
FilePath: ":memory:",
|
||||
DefaultORM: string(ORMTypeGORM),
|
||||
ConnectTimeout: 2 * time.Second,
|
||||
}, providers.NewSQLiteProvider())
|
||||
|
||||
ctx := context.Background()
|
||||
if err := conn.Connect(ctx); err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
db, err := conn.Database()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get database adapter: %v", err)
|
||||
}
|
||||
|
||||
adapter, ok := db.(*database.GormAdapter)
|
||||
if !ok {
|
||||
t.Fatalf("Expected GormAdapter, got %T", db)
|
||||
}
|
||||
|
||||
gormBefore, ok := adapter.GetUnderlyingDB().(*gorm.DB)
|
||||
if !ok {
|
||||
t.Fatalf("Expected underlying *gorm.DB, got %T", adapter.GetUnderlyingDB())
|
||||
}
|
||||
|
||||
sqlBefore, err := gormBefore.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get underlying *sql.DB: %v", err)
|
||||
}
|
||||
|
||||
if err := sqlBefore.Close(); err != nil {
|
||||
t.Fatalf("Failed to close underlying database: %v", err)
|
||||
}
|
||||
|
||||
count, err := db.NewSelect().Table("sqlite_master").Count(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GORM query builder to reconnect, got error: %v", err)
|
||||
}
|
||||
if count < 0 {
|
||||
t.Fatalf("Expected non-negative count, got %d", count)
|
||||
}
|
||||
|
||||
gormAfter, ok := adapter.GetUnderlyingDB().(*gorm.DB)
|
||||
if !ok {
|
||||
t.Fatalf("Expected reconnected *gorm.DB, got %T", adapter.GetUnderlyingDB())
|
||||
}
|
||||
|
||||
sqlAfter, err := gormAfter.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get reconnected *sql.DB: %v", err)
|
||||
}
|
||||
|
||||
if sqlAfter == sqlBefore {
|
||||
t.Fatal("Expected GORM adapter to use a fresh *sql.DB after reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user