package db import ( "context" "fmt" "log/slog" "time" "github.com/jackc/pgx/v5/pgxpool" "visiona-backend/internal/config" ) // Pool 包裝 pgxpool.Pool,提供 visionA-backend 統一的連線池進出點。 // // 設計: // - 薄包裝,不隱藏 *pgxpool.Pool(repository 實作直接拿 .Pool() 用 pgx API)。 // - 持有建池時的 config 供 log / health check 標識連線目標(不含密碼)。 // - Close() 為 graceful shutdown 用,main.go 在收到 SIGTERM 後呼叫。 type Pool struct { pool *pgxpool.Pool cfg config.DatabaseConfig log *slog.Logger } // NewPool 依 DatabaseConfig 建立 pgxpool 連線池,並在啟動時跑一次 ping 確認 DB 可達。 // // fail-fast 語意:ping 失敗即回 error,由 main.go 決定是 fatal(DB 啟用時連不上應該停機)。 // 這避免「DB 設定了卻連不上」卻靜默 fallback in-memory 造成資料不一致的隱患。 // // 逾時:建池與啟動 ping 共用 cfg.ConnTimeout(預設 5s)。 // // 安全:log 只印 SafeTarget(host:port/dbname),DSN 與密碼永遠不入 log。 func NewPool(ctx context.Context, cfg config.DatabaseConfig, log *slog.Logger) (*Pool, error) { if log == nil { log = slog.Default() } dsn := BuildDSN(cfg) poolCfg, err := pgxpool.ParseConfig(dsn) if err != nil { // 不把 err 直接外露 DSN(pgx ParseConfig error 不含密碼,但保守起見只回固定訊息)。 return nil, fmt.Errorf("db: parse pool config failed: %w", err) } // 套用連線池參數(ParseConfig 已從 DSN query 解析 pool_max_conns/pool_min_conns, // 此處再以 config 值覆寫,確保 DSN 與 config 不一致時以 config 為準)。 if cfg.MaxConns > 0 { poolCfg.MaxConns = int32(cfg.MaxConns) } if cfg.MinConns > 0 { poolCfg.MinConns = int32(cfg.MinConns) } if cfg.MaxConnLifetime > 0 { poolCfg.MaxConnLifetime = cfg.MaxConnLifetime } connTimeout := cfg.ConnTimeout if connTimeout <= 0 { connTimeout = 5 * time.Second } buildCtx, cancel := context.WithTimeout(ctx, connTimeout) defer cancel() pgPool, err := pgxpool.NewWithConfig(buildCtx, poolCfg) if err != nil { return nil, fmt.Errorf("db: create pool failed (target=%s): %w", SafeTarget(cfg), err) } // 啟動 ping:確認 DB 真的可達(NewWithConfig 不會立即連線)。 pingCtx, pingCancel := context.WithTimeout(ctx, connTimeout) defer pingCancel() if err := pgPool.Ping(pingCtx); err != nil { pgPool.Close() return nil, fmt.Errorf("db: ping failed (target=%s): %w", SafeTarget(cfg), err) } log.Info("postgres pool initialized", "target", SafeTarget(cfg), "sslmode", cfg.SSLMode, "max_conns", poolCfg.MaxConns, "min_conns", poolCfg.MinConns, "max_conn_lifetime", poolCfg.MaxConnLifetime, ) return &Pool{pool: pgPool, cfg: cfg, log: log}, nil } // Pool 回傳底層 *pgxpool.Pool,供 repository 實作使用 pgx API。 func (p *Pool) Pool() *pgxpool.Pool { return p.pool } // Ping 對 DB 跑一次連線檢查,供 /healthz 等 health check 用。 func (p *Pool) Ping(ctx context.Context) error { return p.pool.Ping(ctx) } // Close 關閉連線池(graceful shutdown)。等待所有 checked-out 連線歸還。 // 重複呼叫安全(pgxpool.Close 內部冪等)。 func (p *Pool) Close() { if p == nil || p.pool == nil { return } p.log.Info("closing postgres pool", "target", SafeTarget(p.cfg)) p.pool.Close() }