feat: use postgres
This commit is contained in:
@@ -3,16 +3,20 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"galaxy/postgres"
|
||||
"galaxy/redisconn"
|
||||
"galaxy/user/internal/adapters/local"
|
||||
"galaxy/user/internal/adapters/postgres/migrations"
|
||||
pguserstore "galaxy/user/internal/adapters/postgres/userstore"
|
||||
"galaxy/user/internal/adapters/redis/domainevents"
|
||||
"galaxy/user/internal/adapters/redis/lifecycleevents"
|
||||
"galaxy/user/internal/adapters/redis/userstore"
|
||||
"galaxy/user/internal/adminapi"
|
||||
"galaxy/user/internal/api/internalhttp"
|
||||
"galaxy/user/internal/config"
|
||||
@@ -25,16 +29,14 @@ import (
|
||||
"galaxy/user/internal/service/policysvc"
|
||||
"galaxy/user/internal/service/selfservice"
|
||||
"galaxy/user/internal/telemetry"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type pinger interface {
|
||||
Ping(context.Context) error
|
||||
}
|
||||
|
||||
type closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Runtime owns the runnable user-service process plus the cleanup functions
|
||||
// that release runtime resources after shutdown.
|
||||
type Runtime struct {
|
||||
@@ -93,61 +95,75 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
return telemetryRuntime.Shutdown(shutdownCtx)
|
||||
})
|
||||
|
||||
store, err := userstore.New(userstore.Config{
|
||||
Addr: cfg.Redis.Addr,
|
||||
Username: cfg.Redis.Username,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
TLSEnabled: cfg.Redis.TLSEnabled,
|
||||
KeyspacePrefix: cfg.Redis.KeyspacePrefix,
|
||||
OperationTimeout: cfg.Redis.OperationTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: redis user store: %w", err))
|
||||
// Open the shared Redis master client for both stream publishers. The
|
||||
// client is owned by the runtime; publishers borrow it through their
|
||||
// New(client, cfg) constructors.
|
||||
redisClient := redisconn.NewMasterClient(cfg.Redis.Conn)
|
||||
if err := redisconn.Instrument(redisClient,
|
||||
redisconn.WithTracerProvider(telemetryRuntime.TracerProvider()),
|
||||
redisconn.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: instrument redis client: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, store.Close)
|
||||
|
||||
if err := pingDependency(ctx, "redis user store", store); err != nil {
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, redisClient.Close)
|
||||
if err := pingRedisClient(ctx, redisClient, cfg.Redis.Conn); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: %w", err))
|
||||
}
|
||||
|
||||
domainEventPublisher, err := domainevents.New(domainevents.Config{
|
||||
Addr: cfg.Redis.Addr,
|
||||
Username: cfg.Redis.Username,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
TLSEnabled: cfg.Redis.TLSEnabled,
|
||||
// Open the PostgreSQL pool, attach instrumentation, ping it, and apply
|
||||
// embedded migrations strictly before any HTTP listener opens. A failure
|
||||
// at any of these steps is fatal: the service exits with non-zero status.
|
||||
pgPool, err := postgres.OpenPrimary(ctx, cfg.Postgres.Conn,
|
||||
postgres.WithTracerProvider(telemetryRuntime.TracerProvider()),
|
||||
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: open postgres primary: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, pgPool.Close)
|
||||
unregisterDBStats, err := postgres.InstrumentDBStats(pgPool,
|
||||
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: instrument postgres db stats: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, unregisterDBStats)
|
||||
if err := postgres.Ping(ctx, pgPool, cfg.Postgres.Conn.OperationTimeout); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: %w", err))
|
||||
}
|
||||
migrationsFS := migrations.FS()
|
||||
if err := postgres.RunMigrations(ctx, pgPool, migrationsFS, "."); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: run postgres migrations: %w", err))
|
||||
}
|
||||
|
||||
store, err := pguserstore.New(pguserstore.Config{
|
||||
DB: pgPool,
|
||||
OperationTimeout: cfg.Postgres.Conn.OperationTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: postgres user store: %w", err))
|
||||
}
|
||||
if err := pingDependency(ctx, "postgres user store", store); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: %w", err))
|
||||
}
|
||||
|
||||
domainEventPublisher, err := domainevents.New(redisClient, domainevents.Config{
|
||||
Stream: cfg.Redis.DomainEventsStream,
|
||||
StreamMaxLen: cfg.Redis.DomainEventsStreamMaxLen,
|
||||
OperationTimeout: cfg.Redis.OperationTimeout,
|
||||
OperationTimeout: cfg.Redis.Conn.OperationTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: redis domain-event publisher: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, domainEventPublisher.Close)
|
||||
|
||||
if err := pingDependency(ctx, "redis domain-event publisher", domainEventPublisher); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: %w", err))
|
||||
}
|
||||
|
||||
lifecycleEventPublisher, err := lifecycleevents.New(lifecycleevents.Config{
|
||||
Addr: cfg.Redis.Addr,
|
||||
Username: cfg.Redis.Username,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
TLSEnabled: cfg.Redis.TLSEnabled,
|
||||
lifecycleEventPublisher, err := lifecycleevents.New(redisClient, lifecycleevents.Config{
|
||||
Stream: cfg.Redis.LifecycleEventsStream,
|
||||
StreamMaxLen: cfg.Redis.LifecycleEventsStreamMaxLen,
|
||||
OperationTimeout: cfg.Redis.OperationTimeout,
|
||||
OperationTimeout: cfg.Redis.Conn.OperationTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: redis lifecycle-event publisher: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, lifecycleEventPublisher.Close)
|
||||
|
||||
if err := pingDependency(ctx, "redis lifecycle-event publisher", lifecycleEventPublisher); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new user-service runtime: %w", err))
|
||||
}
|
||||
|
||||
clock := local.Clock{}
|
||||
idGenerator := local.IDGenerator{}
|
||||
@@ -517,4 +533,24 @@ func pingDependency(ctx context.Context, name string, dependency pinger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ closer = (*userstore.Store)(nil)
|
||||
func pingRedisClient(ctx context.Context, client *goredis.Client, cfg redisconn.Config) error {
|
||||
pingCtx, cancel := context.WithTimeout(ctx, cfg.OperationTimeout)
|
||||
defer cancel()
|
||||
if err := client.Ping(pingCtx).Err(); err != nil {
|
||||
return fmt.Errorf("ping redis master: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time guard that the postgres-backed user store implements the
|
||||
// closer pattern relied on by cleanupFns. Close is a no-op on the postgres
|
||||
// store; the underlying *sql.DB is closed via cleanupFns appended above.
|
||||
var _ interface{ Close() error } = (*pguserstore.Store)(nil)
|
||||
|
||||
// Compile-time guard that the postgres-backed user store also satisfies the
|
||||
// pinger contract used by pingDependency.
|
||||
var _ pinger = (*pguserstore.Store)(nil)
|
||||
|
||||
// Compile-time guard kept from the previous implementation so future readers
|
||||
// can trust the *sql.DB life cycle remains consistent with cleanupFns.
|
||||
var _ *sql.DB = (*sql.DB)(nil)
|
||||
|
||||
Reference in New Issue
Block a user