Skip to content

Commit 8ac2333

Browse files
Start webserver before database init, and improve database init/error handling (#479)
Signed-off-by: Anders Swanson <anders.swanson@oracle.com>
1 parent 325dca0 commit 8ac2333

12 files changed

Lines changed: 297 additions & 42 deletions

File tree

alertlog/alertlog.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ func readLastMatchingLogRecord(logDestination, database string, perDatabaseFiles
129129

130130
// UpdateLog appends newly queried alert log records for a database to the configured log destination.
131131
func UpdateLog(logDestination string, perDatabaseFiles bool, logger *slog.Logger, d *collector.Database) {
132+
if !d.StartupReady() {
133+
return
134+
}
132135
// Do not try to query the alert log if the database configuration is invalid.
133-
if !d.IsValid() {
136+
if d.IsValid() != nil {
134137
return
135138
}
136139
now := time.Now()

alertlog/alertlog_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
"path/filepath"
1010
"testing"
1111
"time"
12+
13+
"io"
14+
"log/slog"
15+
16+
"github.com/oracle/oracle-db-appdev-monitoring/collector"
1217
)
1318

1419
func TestNullStringValue(t *testing.T) {
@@ -156,3 +161,15 @@ func TestRetryTrackerRecordSuccessResetsState(t *testing.T) {
156161
t.Fatalf("expected retry to be allowed after success reset, got retry_after=%v", retryAfter)
157162
}
158163
}
164+
165+
func TestUpdateLogSkipsWhenStartupNotReady(t *testing.T) {
166+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
167+
logPath := filepath.Join(t.TempDir(), "alert.log")
168+
db := &collector.Database{Name: "db1"}
169+
170+
UpdateLog(logPath, false, logger, db)
171+
172+
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
173+
t.Fatalf("expected log file to not be created while startup is in progress, got err=%v", err)
174+
}
175+
}

collector/collector.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ func maskDsn(dsn string) string {
4848
// NewExporter creates a new Exporter instance
4949
func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter {
5050
var databases []*Database
51-
wg := &sync.WaitGroup{}
5251

5352
var allConstLabels []string
5453
// All the metrics of the same name need to have the same set of labels
@@ -64,16 +63,10 @@ func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter {
6463
}
6564

6665
for dbname, dbconfig := range m.Databases {
67-
logger.Info("Initializing database", "database", dbname)
66+
logger.Info("Registering database", "database", dbname)
6867
database := NewDatabase(logger, m.DatabaseLabel(), dbname, dbconfig)
6968
databases = append(databases, database)
70-
wg.Add(1)
71-
go func() {
72-
defer wg.Done()
73-
database.WarmupConnectionPool(logger)
74-
}()
7569
}
76-
wg.Wait()
7770
e := &Exporter{
7871
mu: &sync.Mutex{},
7972
duration: prometheus.NewGauge(prometheus.GaugeOpts{
@@ -116,6 +109,15 @@ func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter {
116109
return e
117110
}
118111

112+
func (e *Exporter) InitializeDatabases() {
113+
for _, database := range e.databases {
114+
e.logger.Info("Starting database connection warmup", "database", database.Name)
115+
if err := database.WarmupConnectionPool(e.logger, e.MetricsConfiguration.ConnectionBackoff()); err != nil {
116+
e.logger.Error("Database startup warmup failed", "error", err, "database", database.Name)
117+
}
118+
}
119+
}
120+
119121
func (e *Exporter) constLabels() map[string]string {
120122
// All the metrics of the same name need to have the same labels
121123
// If a label is set for a particular database, it must be included also
@@ -252,11 +254,16 @@ func (e *Exporter) scrapeDatabase(ch chan<- prometheus.Metric, errChan chan<- er
252254
}()
253255

254256
// If the database configuration is invalid, do not attempt to ping or reestablish the database connection.
255-
if !d.IsValid() {
256-
e.logger.Warn("Invalid database configuration, will not attempt reconnection", "database", d.Name)
257+
if retryAfter := d.IsValid(); retryAfter != nil {
258+
e.logger.Warn("Invalid database configuration", "database", d.Name, "retry_after", retryAfter)
257259
errChan <- fmt.Errorf("database %s is invalid, will not be scraped", d.Name)
258260
return
259261
}
262+
if !d.StartupReady() {
263+
e.logger.Info("Database connection in progress", "database", d.Name)
264+
errChan <- nil
265+
return
266+
}
260267
// If ping fails, we will try again on the next iteration of metrics scraping
261268
if err := d.ping(e.logger, e.MetricsConfiguration.ConnectionBackoff()); err != nil {
262269
e.logger.Error("Error pinging database", "error", err, "database", d.Name)

collector/connect_godror.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *sql.D
7676
// note that this just configures the connection, it does not actually connect until later
7777
// when we call db.Ping()
7878
db := sql.OpenDB(godror.NewConnector(P))
79-
initdb(logger, dbname, dbconfig, db)
8079
return db
8180
}
8281

@@ -91,3 +90,20 @@ func isInvalidCredentialsError(err error) bool {
9190
}
9291
return oraErr.Code() == ora01017code || oraErr.Code() == ora28000code
9392
}
93+
94+
func isTemporaryConnectionError(err error) bool {
95+
err = errors.Unwrap(err)
96+
if err == nil {
97+
return false
98+
}
99+
oraErr, ok := err.(*godror.OraErr)
100+
if !ok {
101+
return false
102+
}
103+
switch oraErr.Code() {
104+
case ora01033code, ora03113code, ora03114code, ora12537code:
105+
return true
106+
default:
107+
return false
108+
}
109+
}

collector/connect_goora.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *sql.D
5151

5252
// Configure connection pool (sql.DB handles pooling)
5353
setConnectionPool(logger, dbname, dbconfig, db)
54-
initdb(logger, dbname, dbconfig, db)
5554
return db
5655
}
5756

@@ -81,3 +80,20 @@ func isInvalidCredentialsError(err error) bool {
8180
}
8281
return oraErr.ErrCode == ora01017code || oraErr.ErrCode == ora28000code
8382
}
83+
84+
func isTemporaryConnectionError(err error) bool {
85+
if err == nil {
86+
return false
87+
}
88+
var oraErr *network.OracleError
89+
ok := errors.As(err, &oraErr)
90+
if !ok {
91+
return false
92+
}
93+
switch oraErr.ErrCode {
94+
case ora01033code, ora03113code, ora03114code, ora12537code:
95+
return true
96+
default:
97+
return false
98+
}
99+
}

collector/database.go

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ package collector
66
import (
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

1618
const (
1719
ora01017code = 1017
20+
ora01033code = 1033
1821
ora28000code = 28000
22+
ora03113code = 3113
23+
ora03114code = 3114
24+
ora12537code = 12537
1925
)
2026

2127
func (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.
5667
func (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.
106154
func (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

133189
func (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+
138202
func 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

Comments
 (0)