449 lines
14 KiB
Go
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",
|
|
},
|
|
}
|
|
}
|