feat: gamemaster
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
// 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",
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user