mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-09 23:06:27 +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
+39
-15
@@ -1,6 +1,8 @@
|
||||
package dbmanager
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
@@ -34,8 +36,8 @@ var (
|
||||
)
|
||||
|
||||
// connectionWaitCount tracks how many times connections had to wait for availability
|
||||
connectionWaitCount = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
connectionWaitCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "dbmanager_connection_wait_count",
|
||||
Help: "Number of times connections had to wait for availability",
|
||||
},
|
||||
@@ -43,8 +45,8 @@ var (
|
||||
)
|
||||
|
||||
// connectionWaitDuration tracks total time connections spent waiting
|
||||
connectionWaitDuration = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
connectionWaitDuration = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "dbmanager_connection_wait_duration_seconds",
|
||||
Help: "Total time connections spent waiting for availability",
|
||||
},
|
||||
@@ -61,8 +63,8 @@ var (
|
||||
)
|
||||
|
||||
// connectionLifetimeClosed tracks connections closed due to max lifetime
|
||||
connectionLifetimeClosed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
connectionLifetimeClosed = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "dbmanager_connection_lifetime_closed_total",
|
||||
Help: "Total connections closed due to exceeding max lifetime",
|
||||
},
|
||||
@@ -70,8 +72,8 @@ var (
|
||||
)
|
||||
|
||||
// connectionIdleClosed tracks connections closed due to max idle time
|
||||
connectionIdleClosed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
connectionIdleClosed = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "dbmanager_connection_idle_closed_total",
|
||||
Help: "Total connections closed due to exceeding max idle time",
|
||||
},
|
||||
@@ -114,13 +116,13 @@ func (m *connectionManager) PublishMetrics() {
|
||||
connectionPoolSize.WithLabelValues(name, string(connStats.Type), "idle").Set(float64(connStats.Idle))
|
||||
connectionPoolSize.WithLabelValues(name, string(connStats.Type), "in_use").Set(float64(connStats.InUse))
|
||||
|
||||
// Wait stats
|
||||
connectionWaitCount.With(labels).Set(float64(connStats.WaitCount))
|
||||
connectionWaitDuration.With(labels).Set(connStats.WaitDuration.Seconds())
|
||||
|
||||
// Lifetime/idle closed
|
||||
connectionLifetimeClosed.With(labels).Set(float64(connStats.MaxLifetimeClosed))
|
||||
connectionIdleClosed.With(labels).Set(float64(connStats.MaxIdleClosed))
|
||||
// sql.DBStats values are cumulative, so add only the growth since
|
||||
// the last publish to keep these true counters.
|
||||
prev := lastPublished.swap(name, connStats)
|
||||
connectionWaitCount.With(labels).Add(float64(connStats.WaitCount - prev.WaitCount))
|
||||
connectionWaitDuration.With(labels).Add((connStats.WaitDuration - prev.WaitDuration).Seconds())
|
||||
connectionLifetimeClosed.With(labels).Add(float64(connStats.MaxLifetimeClosed - prev.MaxLifetimeClosed))
|
||||
connectionIdleClosed.With(labels).Add(float64(connStats.MaxIdleClosed - prev.MaxIdleClosed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,3 +136,25 @@ func RecordReconnectAttempt(name string, dbType DatabaseType, success bool) {
|
||||
|
||||
reconnectAttempts.WithLabelValues(name, string(dbType), result).Inc()
|
||||
}
|
||||
|
||||
// publishedStats remembers the cumulative pool stats last exported per
|
||||
// connection so counters can be advanced by the delta.
|
||||
type publishedStats struct {
|
||||
mu sync.Mutex
|
||||
last map[string]ConnectionStats
|
||||
}
|
||||
|
||||
var lastPublished = &publishedStats{last: make(map[string]ConnectionStats)}
|
||||
|
||||
// swap stores cur and returns the previous value. A counter reset (a new pool
|
||||
// after Close+Connect) is treated as starting from zero.
|
||||
func (p *publishedStats) swap(name string, cur *ConnectionStats) ConnectionStats {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
prev := p.last[name]
|
||||
if cur.WaitCount < prev.WaitCount || cur.MaxIdleClosed < prev.MaxIdleClosed || cur.MaxLifetimeClosed < prev.MaxLifetimeClosed {
|
||||
prev = ConnectionStats{}
|
||||
}
|
||||
p.last[name] = *cur
|
||||
return prev
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user