123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
// Package db owns the PostgreSQL connection pool and the database health check.
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/krow/krow-backend/go-api/internal/config"
|
|
)
|
|
|
|
// DB wraps the pgx pool together with the schema the application is pinned to.
|
|
type DB struct {
|
|
Pool *pgxpool.Pool
|
|
Schema string
|
|
}
|
|
|
|
// Open builds the pool and verifies it can actually reach the database.
|
|
//
|
|
// pgxpool.New is lazy — it returns a usable pool without having connected —
|
|
// so a bad host or a wrong password would otherwise not surface until the
|
|
// first request. Acquiring and pinging once here turns a misconfiguration into
|
|
// a startup failure instead of a runtime surprise.
|
|
func Open(ctx context.Context, cfg config.DBConfig) (*DB, error) {
|
|
poolCfg, err := pgxpool.ParseConfig(cfg.DSN())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse database config: %w", err)
|
|
}
|
|
poolCfg.MaxConns = cfg.MaxOpenConns
|
|
poolCfg.MinIdleConns = cfg.MinIdleConns
|
|
poolCfg.MaxConnLifetime = cfg.ConnMaxLifetime
|
|
|
|
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create connection pool: %w", err)
|
|
}
|
|
|
|
pingCtx, cancel := context.WithTimeout(ctx, cfg.ConnectTimeout)
|
|
defer cancel()
|
|
if err := pool.Ping(pingCtx); err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("connect to %s: %w", cfg.Redacted(), err)
|
|
}
|
|
|
|
return &DB{Pool: pool, Schema: cfg.Schema}, nil
|
|
}
|
|
|
|
// Close releases every pooled connection.
|
|
func (d *DB) Close() {
|
|
if d != nil && d.Pool != nil {
|
|
d.Pool.Close()
|
|
}
|
|
}
|
|
|
|
// Health is what the /health endpoint reports about the database.
|
|
type Health struct {
|
|
Reachable bool `json:"reachable"`
|
|
Error string `json:"error,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
Database string `json:"database,omitempty"`
|
|
Schema string `json:"schema,omitempty"`
|
|
SchemaPresent bool `json:"schema_present"`
|
|
AppliedMigration *int64 `json:"applied_migration,omitempty"`
|
|
MigrationDirty bool `json:"migration_dirty"`
|
|
TableCount int `json:"table_count"`
|
|
LatencyMS int64 `json:"latency_ms"`
|
|
}
|
|
|
|
// Check answers "can the API serve requests against this database right now".
|
|
//
|
|
// It reports more than a ping because a reachable database with no schema in it
|
|
// is a different failure from an unreachable one, and both are worth telling
|
|
// apart at a glance during Phase 1. Reads are confined to the configured schema
|
|
// via to_regclass and a count over information_schema, which is the standard
|
|
// SQL view rather than a pg_catalog table.
|
|
func (d *DB) Check(ctx context.Context) Health {
|
|
started := time.Now()
|
|
h := Health{Schema: d.Schema}
|
|
|
|
conn, err := d.Pool.Acquire(ctx)
|
|
if err != nil {
|
|
h.Error = err.Error()
|
|
h.LatencyMS = time.Since(started).Milliseconds()
|
|
return h
|
|
}
|
|
defer conn.Release()
|
|
|
|
if err := conn.QueryRow(ctx,
|
|
`SELECT current_database(), current_setting('server_version')`,
|
|
).Scan(&h.Database, &h.Version); err != nil {
|
|
h.Error = err.Error()
|
|
h.LatencyMS = time.Since(started).Milliseconds()
|
|
return h
|
|
}
|
|
h.Reachable = true
|
|
|
|
if err := conn.QueryRow(ctx,
|
|
`SELECT count(*)::int FROM information_schema.tables
|
|
WHERE table_schema = $1 AND table_type = 'BASE TABLE'`,
|
|
d.Schema,
|
|
).Scan(&h.TableCount); err != nil {
|
|
h.Error = err.Error()
|
|
h.LatencyMS = time.Since(started).Milliseconds()
|
|
return h
|
|
}
|
|
h.SchemaPresent = h.TableCount > 0
|
|
|
|
// golang-migrate's bookkeeping table. Absent before the first migration,
|
|
// which is a legitimate state and not an error.
|
|
var version int64
|
|
var dirty bool
|
|
err = conn.QueryRow(ctx, `SELECT version, dirty FROM schema_migrations LIMIT 1`).Scan(&version, &dirty)
|
|
if err == nil {
|
|
h.AppliedMigration = &version
|
|
h.MigrationDirty = dirty
|
|
}
|
|
|
|
h.LatencyMS = time.Since(started).Milliseconds()
|
|
return h
|
|
}
|