feat: add configuration, logging, metrics, scheduler and storage foundation
- config: defaults -> JSON file -> NOEMA_* env with validation and redaction - observability: slog with secret-redacting handler; Prometheus registry with the metric set from the plan (events, workspace, memory, LLM, tools, queues) - scheduler: supervised Group and bounded Pool so no goroutine is unowned - storage: pgx pool with statement timeout, embedded migration runner (advisory lock, checksums, up/down/status), per-test schema helper - migrations/0001: append-only guard trigger, users, sessions, api tokens, recovery codes, audit log, settings - pkg/ids: UUIDv7 with monotonic counter
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// Package storage opens the PostgreSQL pool, applies embedded migrations and
|
||||
// provides small helpers shared by repositories. All SQL lives in the domain
|
||||
// packages; this package knows nothing about minds or events.
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Open connects to PostgreSQL and verifies the connection.
|
||||
func Open(ctx context.Context, url string) (*pgxpool.Pool, error) {
|
||||
cfg, err := pgxpool.ParseConfig(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: parse url: %w", err)
|
||||
}
|
||||
if cfg.MaxConns < 8 {
|
||||
cfg.MaxConns = 8
|
||||
}
|
||||
cfg.MaxConnLifetime = time.Hour
|
||||
cfg.HealthCheckPeriod = 30 * time.Second
|
||||
if cfg.ConnConfig.RuntimeParams == nil {
|
||||
cfg.ConnConfig.RuntimeParams = map[string]string{}
|
||||
}
|
||||
cfg.ConnConfig.RuntimeParams["application_name"] = "noema"
|
||||
// A statement timeout bounds runaway queries from any subsystem (T18).
|
||||
if _, set := cfg.ConnConfig.RuntimeParams["statement_timeout"]; !set {
|
||||
cfg.ConnConfig.RuntimeParams["statement_timeout"] = "30000"
|
||||
}
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: connect: %w", err)
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
if err := pool.Ping(pingCtx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("storage: ping: %w", err)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// ErrNotFound is returned by repositories when a row does not exist.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// ErrConflict is returned when a uniqueness constraint is violated.
|
||||
var ErrConflict = errors.New("conflict")
|
||||
|
||||
// Translate maps driver errors to storage sentinels so callers can use errors.Is.
|
||||
func Translate(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return fmt.Errorf("%w: %s", ErrConflict, pgErr.ConstraintName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Tx runs fn in a transaction, committing on nil error and rolling back otherwise.
|
||||
func Tx(ctx context.Context, pool *pgxpool.Pool, fn func(tx pgx.Tx) error) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx) //nolint:errcheck // rollback after commit is a no-op
|
||||
if err := fn(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// Querier is implemented by both *pgxpool.Pool and pgx.Tx so repositories can
|
||||
// run inside or outside a transaction.
|
||||
type Querier interface {
|
||||
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
||||
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||||
}
|
||||
Reference in New Issue
Block a user