Files
galaxy-game/gamemaster/internal/config/config.go
T
2026-05-03 07:59:03 +02:00

449 lines
14 KiB
Go

// Package config loads the Game Master process configuration from
// environment variables.
package config
import (
"fmt"
"strings"
"time"
"galaxy/postgres"
"galaxy/redisconn"
"galaxy/gamemaster/internal/telemetry"
)
const (
envPrefix = "GAMEMASTER"
shutdownTimeoutEnvVar = "GAMEMASTER_SHUTDOWN_TIMEOUT"
logLevelEnvVar = "GAMEMASTER_LOG_LEVEL"
internalHTTPAddrEnvVar = "GAMEMASTER_INTERNAL_HTTP_ADDR"
internalHTTPReadHeaderTimeoutEnvVar = "GAMEMASTER_INTERNAL_HTTP_READ_HEADER_TIMEOUT"
internalHTTPReadTimeoutEnvVar = "GAMEMASTER_INTERNAL_HTTP_READ_TIMEOUT"
internalHTTPWriteTimeoutEnvVar = "GAMEMASTER_INTERNAL_HTTP_WRITE_TIMEOUT"
internalHTTPIdleTimeoutEnvVar = "GAMEMASTER_INTERNAL_HTTP_IDLE_TIMEOUT"
lobbyEventsStreamEnvVar = "GAMEMASTER_REDIS_LOBBY_EVENTS_STREAM"
healthEventsStreamEnvVar = "GAMEMASTER_REDIS_HEALTH_EVENTS_STREAM"
notificationIntentsStreamEnvVar = "GAMEMASTER_REDIS_NOTIFICATION_INTENTS_STREAM"
streamBlockTimeoutEnvVar = "GAMEMASTER_STREAM_BLOCK_TIMEOUT"
engineCallTimeoutEnvVar = "GAMEMASTER_ENGINE_CALL_TIMEOUT"
engineProbeTimeoutEnvVar = "GAMEMASTER_ENGINE_PROBE_TIMEOUT"
lobbyInternalBaseURLEnvVar = "GAMEMASTER_LOBBY_INTERNAL_BASE_URL"
lobbyInternalTimeoutEnvVar = "GAMEMASTER_LOBBY_INTERNAL_TIMEOUT"
rtmInternalBaseURLEnvVar = "GAMEMASTER_RTM_INTERNAL_BASE_URL"
rtmInternalTimeoutEnvVar = "GAMEMASTER_RTM_INTERNAL_TIMEOUT"
schedulerTickIntervalEnvVar = "GAMEMASTER_SCHEDULER_TICK_INTERVAL"
turnGenerationTimeoutEnvVar = "GAMEMASTER_TURN_GENERATION_TIMEOUT"
membershipCacheTTLEnvVar = "GAMEMASTER_MEMBERSHIP_CACHE_TTL"
membershipCacheMaxGamesEnvVar = "GAMEMASTER_MEMBERSHIP_CACHE_MAX_GAMES"
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
otelMetricsExporterEnvVar = "OTEL_METRICS_EXPORTER"
otelExporterOTLPProtocolEnvVar = "OTEL_EXPORTER_OTLP_PROTOCOL"
otelExporterOTLPTracesProtocolEnvVar = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
otelExporterOTLPMetricsProtocolEnvVar = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
otelStdoutTracesEnabledEnvVar = "GAMEMASTER_OTEL_STDOUT_TRACES_ENABLED"
otelStdoutMetricsEnabledEnvVar = "GAMEMASTER_OTEL_STDOUT_METRICS_ENABLED"
defaultShutdownTimeout = 30 * time.Second
defaultLogLevel = "info"
defaultInternalHTTPAddr = ":8097"
defaultReadHeaderTimeout = 2 * time.Second
defaultReadTimeout = 5 * time.Second
defaultWriteTimeout = 30 * time.Second
defaultIdleTimeout = 60 * time.Second
defaultLobbyEventsStream = "gm:lobby_events"
defaultHealthEventsStream = "runtime:health_events"
defaultNotificationIntentsStream = "notification:intents"
defaultStreamBlockTimeout = 5 * time.Second
defaultEngineCallTimeout = 30 * time.Second
defaultEngineProbeTimeout = 5 * time.Second
defaultLobbyInternalTimeout = 2 * time.Second
defaultRTMInternalTimeout = 5 * time.Second
defaultSchedulerTickInterval = time.Second
defaultTurnGenerationTimeout = 60 * time.Second
defaultMembershipCacheTTL = 30 * time.Second
defaultMembershipCacheMaxGames = 4096
defaultOTelServiceName = "galaxy-gamemaster"
)
// Config stores the full Game Master process configuration.
type Config struct {
// ShutdownTimeout bounds graceful shutdown of every long-lived
// component.
ShutdownTimeout time.Duration
// Logging configures the process-wide structured logger.
Logging LoggingConfig
// InternalHTTP configures the trusted internal HTTP listener.
InternalHTTP InternalHTTPConfig
// Postgres configures the PostgreSQL-backed durable store consumed
// via `pkg/postgres`.
Postgres PostgresConfig
// Redis configures the shared Redis connection topology consumed via
// `pkg/redisconn`.
Redis RedisConfig
// Streams stores the stable Redis Stream names GM reads from and
// writes to.
Streams StreamsConfig
// EngineClient configures per-call timeouts of the engine HTTP
// client.
EngineClient EngineClientConfig
// Lobby configures the synchronous Lobby internal REST client.
Lobby LobbyClientConfig
// RTM configures the synchronous Runtime Manager internal REST
// client.
RTM RTMClientConfig
// Scheduler configures the scheduler ticker worker and the per-turn
// generation deadline.
Scheduler SchedulerConfig
// MembershipCache configures the in-process membership cache.
MembershipCache MembershipCacheConfig
// Telemetry configures the process-wide OpenTelemetry runtime.
Telemetry TelemetryConfig
}
// LoggingConfig configures the process-wide structured logger.
type LoggingConfig struct {
// Level stores the process log level accepted by log/slog.
Level string
}
// InternalHTTPConfig configures the trusted internal HTTP listener.
type InternalHTTPConfig struct {
// Addr stores the TCP listen address.
Addr string
// ReadHeaderTimeout bounds request-header reading.
ReadHeaderTimeout time.Duration
// ReadTimeout bounds reading one request.
ReadTimeout time.Duration
// WriteTimeout bounds writing one response.
WriteTimeout time.Duration
// IdleTimeout bounds how long keep-alive connections stay open.
IdleTimeout time.Duration
}
// Validate reports whether cfg stores a usable internal HTTP listener
// configuration.
func (cfg InternalHTTPConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.Addr) == "":
return fmt.Errorf("internal HTTP addr must not be empty")
case !isTCPAddr(cfg.Addr):
return fmt.Errorf("internal HTTP addr %q must use host:port form", cfg.Addr)
case cfg.ReadHeaderTimeout <= 0:
return fmt.Errorf("internal HTTP read header timeout must be positive")
case cfg.ReadTimeout <= 0:
return fmt.Errorf("internal HTTP read timeout must be positive")
case cfg.WriteTimeout <= 0:
return fmt.Errorf("internal HTTP write timeout must be positive")
case cfg.IdleTimeout <= 0:
return fmt.Errorf("internal HTTP idle timeout must be positive")
default:
return nil
}
}
// PostgresConfig configures the PostgreSQL-backed durable store consumed
// via `pkg/postgres`.
type PostgresConfig struct {
// Conn carries the primary plus replica DSN topology and pool tuning.
Conn postgres.Config
}
// Validate reports whether cfg stores a usable PostgreSQL configuration.
func (cfg PostgresConfig) Validate() error {
return cfg.Conn.Validate()
}
// RedisConfig configures the Game Master Redis connection topology.
type RedisConfig struct {
// Conn carries the connection topology (master, replicas, password,
// db, per-call timeout).
Conn redisconn.Config
}
// Validate reports whether cfg stores a usable Redis configuration.
func (cfg RedisConfig) Validate() error {
return cfg.Conn.Validate()
}
// StreamsConfig stores the stable Redis Stream names used by Game Master.
type StreamsConfig struct {
// LobbyEvents stores the Redis Streams key GM publishes runtime
// snapshot updates and game-finished events to.
LobbyEvents string
// HealthEvents stores the Redis Streams key GM consumes runtime
// health events from.
HealthEvents string
// NotificationIntents stores the Redis Streams key GM publishes
// notification intents to.
NotificationIntents string
// BlockTimeout bounds the maximum blocking read window for stream
// consumers.
BlockTimeout time.Duration
}
// Validate reports whether cfg stores usable stream names.
func (cfg StreamsConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.LobbyEvents) == "":
return fmt.Errorf("redis lobby events stream must not be empty")
case strings.TrimSpace(cfg.HealthEvents) == "":
return fmt.Errorf("redis health events stream must not be empty")
case strings.TrimSpace(cfg.NotificationIntents) == "":
return fmt.Errorf("redis notification intents stream must not be empty")
case cfg.BlockTimeout <= 0:
return fmt.Errorf("redis stream block timeout must be positive")
default:
return nil
}
}
// EngineClientConfig configures per-call timeouts of the engine HTTP
// client.
type EngineClientConfig struct {
// CallTimeout bounds one full engine call (including turn generation
// for large games).
CallTimeout time.Duration
// ProbeTimeout bounds inspect-style reads against the engine.
ProbeTimeout time.Duration
}
// Validate reports whether cfg stores usable engine client timeouts.
func (cfg EngineClientConfig) Validate() error {
switch {
case cfg.CallTimeout <= 0:
return fmt.Errorf("engine call timeout must be positive")
case cfg.ProbeTimeout <= 0:
return fmt.Errorf("engine probe timeout must be positive")
default:
return nil
}
}
// LobbyClientConfig configures the synchronous Lobby internal REST
// client.
type LobbyClientConfig struct {
// BaseURL stores the trusted Lobby internal listener base URL.
BaseURL string
// Timeout bounds one Lobby internal request.
Timeout time.Duration
}
// Validate reports whether cfg stores a usable Lobby client
// configuration.
func (cfg LobbyClientConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.BaseURL) == "":
return fmt.Errorf("lobby internal base url must not be empty")
case !isHTTPURL(cfg.BaseURL):
return fmt.Errorf("lobby internal base url %q must be an absolute http(s) URL", cfg.BaseURL)
case cfg.Timeout <= 0:
return fmt.Errorf("lobby internal timeout must be positive")
default:
return nil
}
}
// RTMClientConfig configures the synchronous Runtime Manager internal
// REST client.
type RTMClientConfig struct {
// BaseURL stores the trusted Runtime Manager internal listener base
// URL.
BaseURL string
// Timeout bounds one Runtime Manager internal request.
Timeout time.Duration
}
// Validate reports whether cfg stores a usable Runtime Manager client
// configuration.
func (cfg RTMClientConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.BaseURL) == "":
return fmt.Errorf("rtm internal base url must not be empty")
case !isHTTPURL(cfg.BaseURL):
return fmt.Errorf("rtm internal base url %q must be an absolute http(s) URL", cfg.BaseURL)
case cfg.Timeout <= 0:
return fmt.Errorf("rtm internal timeout must be positive")
default:
return nil
}
}
// SchedulerConfig configures the scheduler ticker worker and the
// per-turn generation deadline.
type SchedulerConfig struct {
// TickInterval is the period between two scheduler scans for due
// runtime records.
TickInterval time.Duration
// TurnGenerationTimeout bounds one engine `/admin/turn` call from
// the scheduler's perspective.
TurnGenerationTimeout time.Duration
}
// Validate reports whether cfg stores usable scheduler timings.
func (cfg SchedulerConfig) Validate() error {
switch {
case cfg.TickInterval <= 0:
return fmt.Errorf("scheduler tick interval must be positive")
case cfg.TurnGenerationTimeout <= 0:
return fmt.Errorf("turn generation timeout must be positive")
default:
return nil
}
}
// MembershipCacheConfig configures the in-process membership cache.
type MembershipCacheConfig struct {
// TTL bounds how long an unobserved membership entry stays cached
// before a forced reload from Lobby.
TTL time.Duration
// MaxGames bounds how many games can populate the cache before
// LRU eviction kicks in.
MaxGames int
}
// Validate reports whether cfg stores usable membership cache settings.
func (cfg MembershipCacheConfig) Validate() error {
switch {
case cfg.TTL <= 0:
return fmt.Errorf("membership cache ttl must be positive")
case cfg.MaxGames <= 0:
return fmt.Errorf("membership cache max games must be positive")
default:
return nil
}
}
// TelemetryConfig configures the Game Master OpenTelemetry runtime.
type TelemetryConfig struct {
// ServiceName overrides the default OpenTelemetry service name.
ServiceName string
// TracesExporter selects the external traces exporter. Supported
// values are `none` and `otlp`.
TracesExporter string
// MetricsExporter selects the external metrics exporter. Supported
// values are `none` and `otlp`.
MetricsExporter string
// TracesProtocol selects the OTLP traces protocol when
// TracesExporter is `otlp`.
TracesProtocol string
// MetricsProtocol selects the OTLP metrics protocol when
// MetricsExporter is `otlp`.
MetricsProtocol string
// StdoutTracesEnabled enables the additional stdout trace exporter
// used for local development and debugging.
StdoutTracesEnabled bool
// StdoutMetricsEnabled enables the additional stdout metric
// exporter used for local development and debugging.
StdoutMetricsEnabled bool
}
// Validate reports whether cfg contains a supported OpenTelemetry
// configuration.
func (cfg TelemetryConfig) Validate() error {
return telemetry.ProcessConfig{
ServiceName: cfg.ServiceName,
TracesExporter: cfg.TracesExporter,
MetricsExporter: cfg.MetricsExporter,
TracesProtocol: cfg.TracesProtocol,
MetricsProtocol: cfg.MetricsProtocol,
StdoutTracesEnabled: cfg.StdoutTracesEnabled,
StdoutMetricsEnabled: cfg.StdoutMetricsEnabled,
}.Validate()
}
// DefaultConfig returns the default Game Master process configuration.
func DefaultConfig() Config {
return Config{
ShutdownTimeout: defaultShutdownTimeout,
Logging: LoggingConfig{
Level: defaultLogLevel,
},
InternalHTTP: InternalHTTPConfig{
Addr: defaultInternalHTTPAddr,
ReadHeaderTimeout: defaultReadHeaderTimeout,
ReadTimeout: defaultReadTimeout,
WriteTimeout: defaultWriteTimeout,
IdleTimeout: defaultIdleTimeout,
},
Postgres: PostgresConfig{
Conn: postgres.DefaultConfig(),
},
Redis: RedisConfig{
Conn: redisconn.DefaultConfig(),
},
Streams: StreamsConfig{
LobbyEvents: defaultLobbyEventsStream,
HealthEvents: defaultHealthEventsStream,
NotificationIntents: defaultNotificationIntentsStream,
BlockTimeout: defaultStreamBlockTimeout,
},
EngineClient: EngineClientConfig{
CallTimeout: defaultEngineCallTimeout,
ProbeTimeout: defaultEngineProbeTimeout,
},
Lobby: LobbyClientConfig{
Timeout: defaultLobbyInternalTimeout,
},
RTM: RTMClientConfig{
Timeout: defaultRTMInternalTimeout,
},
Scheduler: SchedulerConfig{
TickInterval: defaultSchedulerTickInterval,
TurnGenerationTimeout: defaultTurnGenerationTimeout,
},
MembershipCache: MembershipCacheConfig{
TTL: defaultMembershipCacheTTL,
MaxGames: defaultMembershipCacheMaxGames,
},
Telemetry: TelemetryConfig{
ServiceName: defaultOTelServiceName,
TracesExporter: "none",
MetricsExporter: "none",
},
}
}