diff --git a/collector/collector.go b/collector/collector.go index 3f7daca1..cb16f3de 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -49,9 +49,18 @@ func maskDsn(dsn string) string { // NewExporter creates a new Exporter instance func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter { var databases []*Database + wg := &sync.WaitGroup{} for dbname, dbconfig := range m.Databases { - databases = append(databases, NewDatabase(logger, dbname, dbconfig)) + logger.Info("Initializing database", "database", dbname) + database := NewDatabase(logger, dbname, dbconfig) + databases = append(databases, database) + wg.Add(1) + go func() { + defer wg.Done() + database.WarmupConnectionPool(logger) + }() } + wg.Wait() e := &Exporter{ mu: &sync.Mutex{}, duration: prometheus.NewGauge(prometheus.GaugeOpts{ diff --git a/collector/database.go b/collector/database.go index 54f36f31..76c4882c 100644 --- a/collector/database.go +++ b/collector/database.go @@ -4,6 +4,7 @@ package collector import ( + "context" "database/sql" "fmt" "github.com/godror/godror" @@ -64,7 +65,6 @@ func (d *Database) constLabels() map[string]string { func NewDatabase(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *Database { db, dbtype := connect(logger, dbname, dbconfig) - return &Database{ Name: dbname, Up: 0, @@ -74,6 +74,42 @@ func NewDatabase(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *D } } +// WarmupConnectionPool serially acquires connections to "warm up" the connection pool. +// This is a workaround for a perceived bug in ODPI_C where rapid acquisition of connections +// results in a SIGABRT. +func (d *Database) WarmupConnectionPool(logger *slog.Logger) { + var connections []*sql.Conn + poolSize := d.Config.GetMaxOpenConns() + if poolSize < 1 { + poolSize = d.Config.GetPoolMaxConnections() + } + if poolSize > 100 { // defensively cap poolsize + poolSize = 100 + } + warmup := func(i int) { + time.Sleep(100 * time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + conn, err := d.Session.Conn(ctx) + if err != nil { + logger.Debug("Failed to open database connection on warmup", "conn", i, "error", err, "database", d.Name) + return + } + connections = append(connections, conn) + } + for i := 0; i < poolSize; i++ { + warmup(i + 1) + } + + logger.Debug("Warmed connection pool", "total", len(connections), "database", d.Name) + for i, conn := range connections { + if err := conn.Close(); err != nil { + logger.Debug("Failed to return database connection to pool on warmup", "conn", i+1, "error", err, "database", d.Name) + } + } +} + func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) (*sql.DB, float64) { logger.Debug("Launching connection to "+maskDsn(dbconfig.URL), "database", dbname) diff --git a/main.go b/main.go index 9600f3f9..61f17511 100644 --- a/main.go +++ b/main.go @@ -110,14 +110,7 @@ func main() { logger.Error("unable to load metrics configuration", "error", err) return } - - for dbname, db := range m.Databases { - if db.GetMaxOpenConns() > 0 { - logger.Info(dbname + " database max idle connections is greater than 0, so will use go-sql connection pool and pooling settings will be ignored") - } else { - logger.Info(dbname + " database max idle connections is 0, so will use Oracle connection pool. Tune with database pooling settings") - } - } + exporter := collector.NewExporter(logger, m) if exporter.ScrapeInterval() != 0 { ctx, cancel := context.WithCancel(context.Background())