feat: use postgres
This commit is contained in:
@@ -3,16 +3,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/postgres"
|
||||
"galaxy/redisconn"
|
||||
)
|
||||
|
||||
const (
|
||||
envPrefix = "USERSERVICE"
|
||||
|
||||
shutdownTimeoutEnvVar = "USERSERVICE_SHUTDOWN_TIMEOUT"
|
||||
logLevelEnvVar = "USERSERVICE_LOG_LEVEL"
|
||||
|
||||
@@ -27,13 +31,6 @@ const (
|
||||
adminHTTPReadTimeoutEnvVar = "USERSERVICE_ADMIN_HTTP_READ_TIMEOUT"
|
||||
adminHTTPIdleTimeoutEnvVar = "USERSERVICE_ADMIN_HTTP_IDLE_TIMEOUT"
|
||||
|
||||
redisAddrEnvVar = "USERSERVICE_REDIS_ADDR"
|
||||
redisUsernameEnvVar = "USERSERVICE_REDIS_USERNAME"
|
||||
redisPasswordEnvVar = "USERSERVICE_REDIS_PASSWORD"
|
||||
redisDBEnvVar = "USERSERVICE_REDIS_DB"
|
||||
redisTLSEnabledEnvVar = "USERSERVICE_REDIS_TLS_ENABLED"
|
||||
redisOperationTimeoutEnvVar = "USERSERVICE_REDIS_OPERATION_TIMEOUT"
|
||||
redisKeyspacePrefixEnvVar = "USERSERVICE_REDIS_KEYSPACE_PREFIX"
|
||||
redisDomainEventsStreamEnvVar = "USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM"
|
||||
redisDomainEventsStreamMaxLenEnvVar = "USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM_MAX_LEN"
|
||||
redisLifecycleEventsStreamEnvVar = "USERSERVICE_REDIS_LIFECYCLE_EVENTS_STREAM"
|
||||
@@ -48,26 +45,23 @@ const (
|
||||
otelStdoutTracesEnabledEnvVar = "USERSERVICE_OTEL_STDOUT_TRACES_ENABLED"
|
||||
otelStdoutMetricsEnabledEnvVar = "USERSERVICE_OTEL_STDOUT_METRICS_ENABLED"
|
||||
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8091"
|
||||
defaultAdminHTTPAddr = ""
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
defaultRequestTimeout = 3 * time.Second
|
||||
defaultRedisDB = 0
|
||||
defaultRedisOperationTimeout = 250 * time.Millisecond
|
||||
defaultRedisKeyspacePrefix = "user:"
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8091"
|
||||
defaultAdminHTTPAddr = ""
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
defaultRequestTimeout = 3 * time.Second
|
||||
defaultDomainEventsStream = "user:domain_events"
|
||||
defaultDomainEventsStreamMaxLen = 1024
|
||||
defaultLifecycleEventsStream = "user:lifecycle_events"
|
||||
defaultLifecycleEventsStreamMaxLen = 1024
|
||||
defaultOTelServiceName = "galaxy-user"
|
||||
otelExporterNone = "none"
|
||||
otelExporterOTLP = "otlp"
|
||||
otelProtocolHTTPProtobuf = "http/protobuf"
|
||||
otelProtocolGRPC = "grpc"
|
||||
defaultOTelServiceName = "galaxy-user"
|
||||
otelExporterNone = "none"
|
||||
otelExporterOTLP = "otlp"
|
||||
otelProtocolHTTPProtobuf = "http/protobuf"
|
||||
otelProtocolGRPC = "grpc"
|
||||
)
|
||||
|
||||
// Config stores the full user-service process configuration.
|
||||
@@ -85,9 +79,14 @@ type Config struct {
|
||||
// AdminHTTP configures the optional private admin HTTP listener.
|
||||
AdminHTTP AdminHTTPConfig
|
||||
|
||||
// Redis configures the Redis-backed user store and domain-event publisher.
|
||||
// Redis configures the Redis-backed event publishers (domain + lifecycle
|
||||
// streams) plus the connection topology consumed via `pkg/redisconn`.
|
||||
Redis RedisConfig
|
||||
|
||||
// Postgres configures the PostgreSQL-backed durable store consumed via
|
||||
// `pkg/postgres`.
|
||||
Postgres PostgresConfig
|
||||
|
||||
// Telemetry configures the process-wide OpenTelemetry runtime.
|
||||
Telemetry TelemetryConfig
|
||||
}
|
||||
@@ -171,28 +170,12 @@ func (cfg AdminHTTPConfig) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// RedisConfig configures the Redis-backed store and domain-event publisher.
|
||||
// RedisConfig configures the Redis-backed event publishers and the connection
|
||||
// topology shared with `pkg/redisconn`.
|
||||
type RedisConfig struct {
|
||||
// Addr stores the Redis network address.
|
||||
Addr string
|
||||
|
||||
// Username stores the optional Redis ACL username.
|
||||
Username string
|
||||
|
||||
// Password stores the optional Redis ACL password.
|
||||
Password string
|
||||
|
||||
// DB stores the Redis logical database index.
|
||||
DB int
|
||||
|
||||
// TLSEnabled reports whether TLS must be used for Redis connections.
|
||||
TLSEnabled bool
|
||||
|
||||
// OperationTimeout bounds one Redis round trip.
|
||||
OperationTimeout time.Duration
|
||||
|
||||
// KeyspacePrefix stores the root prefix of the service-owned Redis keyspace.
|
||||
KeyspacePrefix string
|
||||
// Conn carries the connection topology (master, replicas, password, db,
|
||||
// per-call timeout). Loaded via redisconn.LoadFromEnv("USERSERVICE").
|
||||
Conn redisconn.Config
|
||||
|
||||
// DomainEventsStream stores the Redis Stream key used for auxiliary
|
||||
// post-commit domain events.
|
||||
@@ -203,8 +186,8 @@ type RedisConfig struct {
|
||||
DomainEventsStreamMaxLen int64
|
||||
|
||||
// LifecycleEventsStream stores the Redis Stream key used for trusted
|
||||
// user-lifecycle events (permanent_block, delete) consumed by
|
||||
// `Game Lobby` for Race Name Directory cascade release.
|
||||
// user-lifecycle events (permanent_block, delete) consumed by `Game
|
||||
// Lobby` for Race Name Directory cascade release.
|
||||
LifecycleEventsStream string
|
||||
|
||||
// LifecycleEventsStreamMaxLen bounds the lifecycle-events Redis Stream
|
||||
@@ -212,27 +195,12 @@ type RedisConfig struct {
|
||||
LifecycleEventsStreamMaxLen int64
|
||||
}
|
||||
|
||||
// TLSConfig returns the conservative TLS configuration used by Redis adapters
|
||||
// when TLSEnabled is true.
|
||||
func (cfg RedisConfig) TLSConfig() *tls.Config {
|
||||
if !cfg.TLSEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores a usable Redis configuration.
|
||||
func (cfg RedisConfig) Validate() error {
|
||||
if err := cfg.Conn.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case strings.TrimSpace(cfg.Addr) == "":
|
||||
return fmt.Errorf("redis addr must not be empty")
|
||||
case cfg.DB < 0:
|
||||
return fmt.Errorf("redis db must not be negative")
|
||||
case cfg.OperationTimeout <= 0:
|
||||
return fmt.Errorf("redis operation timeout must be positive")
|
||||
case strings.TrimSpace(cfg.KeyspacePrefix) == "":
|
||||
return fmt.Errorf("redis keyspace prefix must not be empty")
|
||||
case strings.TrimSpace(cfg.DomainEventsStream) == "":
|
||||
return fmt.Errorf("redis domain events stream must not be empty")
|
||||
case cfg.DomainEventsStreamMaxLen <= 0:
|
||||
@@ -246,6 +214,20 @@ func (cfg RedisConfig) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// PostgresConfig configures the PostgreSQL-backed durable store. It wraps
|
||||
// the shared `pkg/postgres.Config` so callers receive the same struct shape
|
||||
// across services.
|
||||
type PostgresConfig struct {
|
||||
// Conn stores the primary plus replica DSN topology and pool tuning.
|
||||
// Loaded via postgres.LoadFromEnv("USERSERVICE").
|
||||
Conn postgres.Config
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores a usable PostgreSQL configuration.
|
||||
func (cfg PostgresConfig) Validate() error {
|
||||
return cfg.Conn.Validate()
|
||||
}
|
||||
|
||||
// TelemetryConfig configures the user-service OpenTelemetry runtime.
|
||||
type TelemetryConfig struct {
|
||||
// ServiceName overrides the default OpenTelemetry service name.
|
||||
@@ -313,7 +295,9 @@ func DefaultAdminHTTPConfig() AdminHTTPConfig {
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default process configuration with all optional
|
||||
// values filled.
|
||||
// values filled. Required connection coordinates (Redis master/password,
|
||||
// Postgres primary DSN) remain zero-valued and must be supplied via
|
||||
// LoadFromEnv.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
ShutdownTimeout: defaultShutdownTimeout,
|
||||
@@ -329,14 +313,15 @@ func DefaultConfig() Config {
|
||||
},
|
||||
AdminHTTP: DefaultAdminHTTPConfig(),
|
||||
Redis: RedisConfig{
|
||||
DB: defaultRedisDB,
|
||||
OperationTimeout: defaultRedisOperationTimeout,
|
||||
KeyspacePrefix: defaultRedisKeyspacePrefix,
|
||||
Conn: redisconn.DefaultConfig(),
|
||||
DomainEventsStream: defaultDomainEventsStream,
|
||||
DomainEventsStreamMaxLen: defaultDomainEventsStreamMaxLen,
|
||||
LifecycleEventsStream: defaultLifecycleEventsStream,
|
||||
LifecycleEventsStreamMaxLen: defaultLifecycleEventsStreamMaxLen,
|
||||
},
|
||||
Postgres: PostgresConfig{
|
||||
Conn: postgres.DefaultConfig(),
|
||||
},
|
||||
Telemetry: TelemetryConfig{
|
||||
ServiceName: defaultOTelServiceName,
|
||||
TracesExporter: otelExporterNone,
|
||||
@@ -360,6 +345,9 @@ func (cfg Config) Validate() error {
|
||||
if err := cfg.Redis.Validate(); err != nil {
|
||||
return fmt.Errorf("redis config: %w", err)
|
||||
}
|
||||
if err := cfg.Postgres.Validate(); err != nil {
|
||||
return fmt.Errorf("postgres config: %w", err)
|
||||
}
|
||||
if _, err := parseLogLevel(cfg.Logging.Level); err != nil {
|
||||
return fmt.Errorf("logging config: %w", err)
|
||||
}
|
||||
@@ -370,7 +358,11 @@ func (cfg Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromEnv loads Config from the process environment.
|
||||
// LoadFromEnv loads Config from the process environment. Connection topology
|
||||
// for Redis and PostgreSQL is delegated to the shared `pkg/redisconn` and
|
||||
// `pkg/postgres` LoadFromEnv helpers, which enforce the architectural rules
|
||||
// (mandatory Redis password, deprecated TLS/USERNAME variables hard-fail,
|
||||
// required Postgres primary DSN).
|
||||
func LoadFromEnv() (Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
@@ -413,22 +405,11 @@ func LoadFromEnv() (Config, error) {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg.Redis.Addr = loadString(redisAddrEnvVar, cfg.Redis.Addr)
|
||||
cfg.Redis.Username = loadString(redisUsernameEnvVar, cfg.Redis.Username)
|
||||
cfg.Redis.Password = loadString(redisPasswordEnvVar, cfg.Redis.Password)
|
||||
cfg.Redis.DB, err = loadInt(redisDBEnvVar, cfg.Redis.DB)
|
||||
redisConn, err := redisconn.LoadFromEnv(envPrefix)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Redis.TLSEnabled, err = loadBool(redisTLSEnabledEnvVar, cfg.Redis.TLSEnabled)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Redis.OperationTimeout, err = loadDuration(redisOperationTimeoutEnvVar, cfg.Redis.OperationTimeout)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Redis.KeyspacePrefix = loadString(redisKeyspacePrefixEnvVar, cfg.Redis.KeyspacePrefix)
|
||||
cfg.Redis.Conn = redisConn
|
||||
cfg.Redis.DomainEventsStream = loadString(redisDomainEventsStreamEnvVar, cfg.Redis.DomainEventsStream)
|
||||
cfg.Redis.DomainEventsStreamMaxLen, err = loadInt64(redisDomainEventsStreamMaxLenEnvVar, cfg.Redis.DomainEventsStreamMaxLen)
|
||||
if err != nil {
|
||||
@@ -440,6 +421,12 @@ func LoadFromEnv() (Config, error) {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
pgConn, err := postgres.LoadFromEnv(envPrefix)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Postgres.Conn = pgConn
|
||||
|
||||
cfg.Telemetry.ServiceName = loadString(otelServiceNameEnvVar, cfg.Telemetry.ServiceName)
|
||||
cfg.Telemetry.TracesExporter = normalizeExporterValue(loadString(otelTracesExporterEnvVar, cfg.Telemetry.TracesExporter))
|
||||
cfg.Telemetry.MetricsExporter = normalizeExporterValue(loadString(otelMetricsExporterEnvVar, cfg.Telemetry.MetricsExporter))
|
||||
@@ -492,20 +479,6 @@ func loadDuration(envName string, defaultValue time.Duration) (time.Duration, er
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
func loadInt(envName string, defaultValue int) (int, error) {
|
||||
value, ok := os.LookupEnv(envName)
|
||||
if !ok {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
parsedValue, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: parse int: %w", envName, err)
|
||||
}
|
||||
|
||||
return parsedValue, nil
|
||||
}
|
||||
|
||||
func loadInt64(envName string, defaultValue int64) (int64, error) {
|
||||
value, ok := os.LookupEnv(envName)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user