@@ -6,16 +6,22 @@ package collector
66import (
77 "context"
88 "database/sql"
9+ "errors"
910 "fmt"
1011 "github.com/prometheus/client_golang/prometheus"
1112 "log/slog"
1213 "strings"
14+ "sync"
1315 "time"
1416)
1517
1618const (
1719 ora01017code = 1017
20+ ora01033code = 1033
1821 ora28000code = 28000
22+ ora03113code = 3113
23+ ora03114code = 3114
24+ ora12537code = 12537
1925)
2026
2127func (d * Database ) UpMetric (exporterLabels map [string ]string ) prometheus.Metric {
@@ -49,9 +55,14 @@ func NewDatabase(logger *slog.Logger, dblabel, dbname string, dbconfig DatabaseC
4955 Session : db ,
5056 Config : dbconfig ,
5157 DatabaseLabel : dblabel ,
58+ reconnectMU : sync.Mutex {},
5259 }
5360}
5461
62+ func (d * Database ) StartupReady () bool {
63+ return d .startupReady .Load ()
64+ }
65+
5566// initCache resets the metrics cached. Used on startup and when metrics are reloaded.
5667func (d * Database ) initCache (metrics map [string ]* Metric ) {
5768 d .MetricsCache = NewMetricsCache (metrics )
@@ -60,7 +71,18 @@ func (d *Database) initCache(metrics map[string]*Metric) {
6071// WarmupConnectionPool serially acquires connections to "warm up" the connection pool.
6172// This is a workaround for a perceived bug in ODPI_C where rapid acquisition of connections
6273// results in a SIGABRT.
63- func (d * Database ) WarmupConnectionPool (logger * slog.Logger ) {
74+ func (d * Database ) WarmupConnectionPool (logger * slog.Logger , backoff time.Duration ) error {
75+ defer d .startupReady .Store (true )
76+ return d .warmupSession (logger , backoff , d .Session )
77+ }
78+
79+ func (d * Database ) warmupSession (logger * slog.Logger , backoff time.Duration , session * sql.DB ) error {
80+ if session == nil {
81+ d .Up = 0
82+ d .invalidate (backoff )
83+ return errors .New ("database session is not initialized" )
84+ }
85+
6486 var connections []* sql.Conn
6587 poolSize := d .Config .GetMaxOpenConns ()
6688 if poolSize < 1 {
@@ -74,67 +96,109 @@ func (d *Database) WarmupConnectionPool(logger *slog.Logger) {
7496 ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
7597 defer cancel ()
7698
77- conn , err := d . Session .Conn (ctx )
99+ conn , err := session .Conn (ctx )
78100 if err != nil {
79101 return err
80102 }
81103 connections = append (connections , conn )
82104 return nil
83105 }
84106
85- func () {
86- for i := 0 ; i < poolSize ; i ++ {
87- // short circuit warmup for inaccessible databases
88- if err := warmup (i + 1 ); err != nil {
89- d .Up = 0
90- logger .Error ("Failed warmup database connection pool" , "conn" , i , "error" , err , "database" , d .Name )
91- return
92- }
107+ initdb (logger , d .Name , d .Config , session )
108+
109+ for i := 0 ; i < poolSize ; i ++ {
110+ // short circuit warmup for inaccessible databases
111+ if err := warmup (i + 1 ); err != nil {
112+ d .Up = 0
113+ d .invalidate (backoff )
114+ logger .Debug ("Failed warmup database connection pool" , "conn" , i , "error" , err , "database" , d .Name )
115+ return err
93116 }
94- }()
117+ }
95118
96119 logger .Debug ("Warmed connection pool" , "total" , len (connections ), "database" , d .Name )
97120 for i , conn := range connections {
98121 if err := conn .Close (); err != nil {
99122 logger .Debug ("Failed to return database connection to pool on warmup" , "conn" , i + 1 , "error" , err , "database" , d .Name )
100123 }
101124 }
125+ d .Up = 1
126+ d .clearInvalid ()
127+ return nil
128+ }
129+
130+ func (d * Database ) reconnect (logger * slog.Logger , backoff time.Duration ) error {
131+ d .reconnectMU .Lock ()
132+ defer d .reconnectMU .Unlock ()
133+
134+ logger .Info ("Reconnecting database session" , "database" , d .Name )
135+
136+ session := connect (logger , d .Name , d .Config )
137+ if err := d .warmupSession (logger , backoff , session ); err != nil {
138+ if session != nil {
139+ _ = session .Close ()
140+ }
141+ return err
142+ }
143+
144+ oldSession := d .Session
145+ d .Session = session
146+ if oldSession != nil && oldSession != session {
147+ _ = oldSession .Close ()
148+ }
149+ return nil
102150}
103151
104152// ping the database. If the database is disconnected, try to reconnect.
105153// If the database type is unknown, try to reload it.
106154func (d * Database ) ping (logger * slog.Logger , backoff time.Duration ) error {
155+ if d .Session == nil {
156+ return d .reconnect (logger , backoff )
157+ }
107158 ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
108159 defer cancel ()
109160 err := d .Session .PingContext (ctx )
110161 if err != nil {
111162 d .Up = 0
112- if isInvalidCredentialsError (err ) {
163+ if isInvalidCredentialsError (err ) || isTemporaryConnectionError ( err ) {
113164 d .invalidate (backoff )
114165 return err
115166 }
116- // If database is closed, try to reconnect
117- if strings . Contains (err . Error (), "sql: database is closed" ) {
118- d . Session = connect (logger , d . Name , d . Config )
167+ // If database is closed, rebuild the handle and rerun init/warmup.
168+ if isClosedDatabaseError (err ) {
169+ return d . reconnect (logger , backoff )
119170 }
120171 return err
121172 }
122173 d .Up = 1
174+ d .clearInvalid ()
123175 return nil
124176}
125177
126- func (d * Database ) IsValid () bool {
178+ func (d * Database ) IsValid () * time. Duration {
127179 if d .invalidUntil == nil {
128- return true
180+ return nil
181+ }
182+ retryAfter := time .Until (* d .invalidUntil )
183+ if retryAfter <= 0 {
184+ return nil
129185 }
130- return time . Now (). After ( * d . invalidUntil )
186+ return & retryAfter
131187}
132188
133189func (d * Database ) invalidate (backoff time.Duration ) {
134190 until := time .Now ().Add (backoff )
135191 d .invalidUntil = & until
136192}
137193
194+ func (d * Database ) clearInvalid () {
195+ d .invalidUntil = nil
196+ }
197+
198+ func isClosedDatabaseError (err error ) bool {
199+ return errors .Is (err , sql .ErrConnDone ) || strings .Contains (err .Error (), "sql: database is closed" )
200+ }
201+
138202func initdb (logger * slog.Logger , dbname string , dbconfig DatabaseConfig , db * sql.DB ) {
139203 logger .Debug (fmt .Sprintf ("set max idle connections to %d" , dbconfig .MaxIdleConns ), "database" , dbname )
140204 db .SetMaxIdleConns (dbconfig .GetMaxIdleConns ())
0 commit comments