Files
descrybe/apps/api/internal/db/db.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

87 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"context"
"fmt"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// PoolOptions tunes pgxpool for API/worker processes.
//
// Sizing guidance (multi-instance): MaxConns ≈ (Postgres max_connections reserved) / instance_count.
// Typical per-process MaxConns is 2050; MinConns ≈ 1030% of MaxConns (warm floor).
// Always set MaxConnLifetimeJitter (~1020% of MaxConnLifetime) to avoid thundering-herd reconnects.
// Defaults match historical NewPool hardcodes, plus idle recycle + statement timeout for multi-tenant churn.
type PoolOptions struct {
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
// MaxConnLifetimeJitter adds random extra lifetime per connection (pgxpool); 0 disables.
MaxConnLifetimeJitter time.Duration
MaxConnIdleTime time.Duration
HealthCheckPeriod time.Duration
// StatementTimeout sets Postgres statement_timeout on each connection (0 disables).
StatementTimeout time.Duration
}
// DefaultPoolOptions returns safe production-oriented defaults used when config omits overrides.
func DefaultPoolOptions() PoolOptions {
return PoolOptions{
MaxConns: 20,
MinConns: 2,
MaxConnLifetime: time.Hour,
MaxConnLifetimeJitter: 6 * time.Minute, // ~10% of lifetime
MaxConnIdleTime: 5 * time.Minute,
HealthCheckPeriod: time.Minute,
StatementTimeout: 30 * time.Second,
}
}
// NewPool opens a pgx pool with sizing/timeouts from opts (see DefaultPoolOptions).
func NewPool(ctx context.Context, databaseURL string, opts PoolOptions) (*pgxpool.Pool, error) {
cfg, err := ParsePoolConfig(databaseURL, opts)
if err != nil {
return nil, err
}
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("connect database: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping database: %w", err)
}
return pool, nil
}
// ParsePoolConfig builds a pgxpool.Config without connecting (unit-testable).
func ParsePoolConfig(databaseURL string, opts PoolOptions) (*pgxpool.Config, error) {
cfg, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("parse database url: %w", err)
}
applyPoolOptions(cfg, opts)
return cfg, nil
}
func applyPoolOptions(cfg *pgxpool.Config, opts PoolOptions) {
cfg.MaxConns = opts.MaxConns
cfg.MinConns = opts.MinConns
cfg.MaxConnLifetime = opts.MaxConnLifetime
cfg.MaxConnLifetimeJitter = opts.MaxConnLifetimeJitter
cfg.MaxConnIdleTime = opts.MaxConnIdleTime
cfg.HealthCheckPeriod = opts.HealthCheckPeriod
if opts.StatementTimeout > 0 {
if cfg.ConnConfig.RuntimeParams == nil {
cfg.ConnConfig.RuntimeParams = map[string]string{}
}
// Postgres accepts integer milliseconds for statement_timeout.
cfg.ConnConfig.RuntimeParams["statement_timeout"] = strconv.FormatInt(opts.StatementTimeout.Milliseconds(), 10)
}
}