// Package db — подключение к PostgreSQL и рантайм-раннер миграций // (embed SQL-файлов + checksum применённых версий). package db import ( "context" "fmt" "os" "strconv" "time" "github.com/jackc/pgx/v5/pgxpool" ) // Connect создаёт пул и проверяет соединение пингом: ошибку лучше // поймать здесь, чем на первом запросе хендлера. // // Пул настраивается по env (DB_*), и только если переменная задана: // без них работают дефолты pgxpool — dev и CI не меняются, а в проде // потолок соединений задаётся явно (см. .env.prod.example). Без // потолка MaxConns = max(4, 4*CPU) — на большой машине это лишние // соединения к PostgreSQL. func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, err } if err := applyPoolEnv(cfg); err != nil { return nil, err } pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, err } if err := pool.Ping(ctx); err != nil { // Пинг упал — пул уже создал соединения, закрываем их: // утечка на старте незаметна, но копится с каждым рестартом. pool.Close() return nil, err } return pool, nil } // applyPoolEnv читает DB_* настройки пула. Пустая строка = «не задано», // дефолты остаются от pgxpool. Ошибка здесь — фейл-фаст на старте: // молча работать с чужим потолком нельзя. func applyPoolEnv(cfg *pgxpool.Config) error { if v := os.Getenv("DB_MAX_CONNS"); v != "" { n, err := strconv.Atoi(v) if err != nil || n <= 0 { return fmt.Errorf("DB_MAX_CONNS must be a positive number, got %q", v) } cfg.MaxConns = int32(n) } if v := os.Getenv("DB_MIN_CONNS"); v != "" { n, err := strconv.Atoi(v) if err != nil || n < 0 { return fmt.Errorf("DB_MIN_CONNS must be a non-negative number, got %q", v) } cfg.MinConns = int32(n) } if v := os.Getenv("DB_MAX_CONN_LIFETIME"); v != "" { d, err := time.ParseDuration(v) if err != nil || d <= 0 { return fmt.Errorf("DB_MAX_CONN_LIFETIME must be a positive duration, e.g. 1h, got %q", v) } cfg.MaxConnLifetime = d } if v := os.Getenv("DB_MAX_CONN_IDLE_TIME"); v != "" { d, err := time.ParseDuration(v) if err != nil || d <= 0 { return fmt.Errorf("DB_MAX_CONN_IDLE_TIME must be a positive duration, e.g. 30m, got %q", v) } cfg.MaxConnIdleTime = d } return nil }