// 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", }, } }