mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-09-30 20:11:59 +00:00
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>
125 lines
3.2 KiB
Go
125 lines
3.2 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
)
|
|
|
|
// ExistingDBProvider wraps an existing *sql.DB connection
|
|
// This allows using dbmanager features with a database connection
|
|
// that was opened outside of the dbmanager package
|
|
type ExistingDBProvider struct {
|
|
db *sql.DB
|
|
name string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewExistingDBProvider creates a new provider wrapping an existing *sql.DB
|
|
func NewExistingDBProvider(db *sql.DB, name string) *ExistingDBProvider {
|
|
return &ExistingDBProvider{
|
|
db: db,
|
|
name: name,
|
|
}
|
|
}
|
|
|
|
// Connect verifies the existing database connection is valid
|
|
// It does NOT create a new connection, but ensures the existing one works
|
|
func (p *ExistingDBProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.db == nil {
|
|
return fmt.Errorf("database connection is nil")
|
|
}
|
|
|
|
// Verify the connection works
|
|
if err := p.db.PingContext(ctx); err != nil {
|
|
return fmt.Errorf("failed to ping existing database: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Refresh verifies the wrapped database is still reachable. The pool belongs to
|
|
// the caller and cannot be re-dialed here, so it is never closed to "reconnect".
|
|
func (p *ExistingDBProvider) Refresh(ctx context.Context) error {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
if p.db == nil {
|
|
return fmt.Errorf("database connection is nil")
|
|
}
|
|
return p.db.PingContext(ctx)
|
|
}
|
|
|
|
// OwnsDB reports whether Close releases the wrapped database. It never does:
|
|
// the *sql.DB was opened by the caller, who is responsible for closing it.
|
|
func (p *ExistingDBProvider) OwnsDB() bool { return false }
|
|
|
|
// Close is a no-op for the wrapped database. The pool belongs to the caller, so
|
|
// closing it here would break the caller's other users of it.
|
|
func (p *ExistingDBProvider) Close() error {
|
|
logger.Warn("Not closing externally provided database: name=%s; the caller owns this *sql.DB and must close it", p.name)
|
|
return nil
|
|
}
|
|
|
|
// HealthCheck verifies the connection is alive
|
|
func (p *ExistingDBProvider) HealthCheck(ctx context.Context) error {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
if p.db == nil {
|
|
return fmt.Errorf("database connection is nil")
|
|
}
|
|
|
|
return p.db.PingContext(ctx)
|
|
}
|
|
|
|
// GetNative returns the wrapped *sql.DB
|
|
func (p *ExistingDBProvider) GetNative() (*sql.DB, error) {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
if p.db == nil {
|
|
return nil, fmt.Errorf("database connection is nil")
|
|
}
|
|
|
|
return p.db, nil
|
|
}
|
|
|
|
// GetMongo returns an error since this is a SQL database
|
|
func (p *ExistingDBProvider) GetMongo() (*mongo.Client, error) {
|
|
return nil, ErrNotMongoDB
|
|
}
|
|
|
|
// Stats returns connection statistics
|
|
func (p *ExistingDBProvider) Stats() *ConnectionStats {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
stats := &ConnectionStats{
|
|
Name: p.name,
|
|
Type: "sql", // Generic since we don't know the specific type
|
|
Connected: p.db != nil,
|
|
}
|
|
|
|
if p.db != nil {
|
|
dbStats := p.db.Stats()
|
|
stats.OpenConnections = dbStats.OpenConnections
|
|
stats.InUse = dbStats.InUse
|
|
stats.Idle = dbStats.Idle
|
|
stats.WaitCount = dbStats.WaitCount
|
|
stats.WaitDuration = dbStats.WaitDuration
|
|
stats.MaxIdleClosed = dbStats.MaxIdleClosed
|
|
stats.MaxLifetimeClosed = dbStats.MaxLifetimeClosed
|
|
}
|
|
|
|
return stats
|
|
}
|