"""Server configuration, read from environment variables.""" from __future__ import annotations import os from dataclasses import dataclass def _float(name: str, default: float) -> float: return float(os.environ.get(name, default)) def _int(name: str, default: int) -> int: return int(os.environ.get(name, default)) @dataclass(frozen=True) class Settings: # Shared secret the AI service has to present on /internal/ai. # Empty disables authentication (development only). ai_shared_secret: str = os.environ.get("AI_SHARED_SECRET", "") # Seconds to wait for the AI service before falling back to a random move. ai_decision_timeout: float = _float("AI_DECISION_TIMEOUT", 5.0) # Minimum "thinking" time of AI players so humans can follow the game. ai_move_delay: float = _float("AI_MOVE_DELAY", 0.8) # Pause after a completed trick before AI players continue. trick_pause: float = _float("TRICK_PAUSE", 1.8) # Sessions without a connection are dropped after this many seconds. session_ttl: float = _float("SESSION_TTL", 1800.0) # Maximum number of simultaneously existing lobbies. max_lobbies: int = _int("MAX_LOBBIES", 1000) # Comma separated list of allowed websocket origins; empty allows all. allowed_origins: tuple[str, ...] = tuple( origin.strip() for origin in os.environ.get("ALLOWED_ORIGINS", "").split(",") if origin.strip() ) log_level: str = os.environ.get("LOG_LEVEL", "INFO") # --- Abuse limits ------------------------------------------------------- # Open websocket connections in total and per client IP. max_connections: int = _int("MAX_CONNECTIONS", 500) max_connections_per_ip: int = _int("MAX_CONNECTIONS_PER_IP", 10) # Messages per second a single connection may send (token bucket). message_rate: float = _float("MESSAGE_RATE", 10.0) message_burst: int = _int("MESSAGE_BURST", 30) # Dropped messages after which a flooding connection is closed. max_rate_violations: int = _int("MAX_RATE_VIOLATIONS", 50) # Lobbies a single IP may create per minute. lobby_create_rate: int = _int("LOBBY_CREATE_RATE", 10) # --- Load control ------------------------------------------------------- # Games running at the same time; further game starts are refused. max_active_games: int = _int("MAX_ACTIVE_GAMES", 50) load_sample_interval: float = _float("LOAD_SAMPLE_INTERVAL", 1.0) # Event loop lag (seconds) / CPU share of the available quota (0..1). load_lag_busy: float = _float("LOAD_LAG_BUSY", 0.1) load_lag_overload: float = _float("LOAD_LAG_OVERLOAD", 0.25) # A single sample with this much lag means overload right away: when the # server is that slow, even the load sampler runs rarely. load_lag_severe: float = _float("LOAD_LAG_SEVERE", 1.0) load_cpu_busy: float = _float("LOAD_CPU_BUSY", 0.7) load_cpu_overload: float = _float("LOAD_CPU_OVERLOAD", 0.9) # Share of CPU periods in which the container was throttled (cgroup v2). load_throttle_overload: float = _float("LOAD_THROTTLE_OVERLOAD", 0.5) # Consecutive overloaded samples to enter / good samples to leave overload. load_overload_samples: int = _int("LOAD_OVERLOAD_SAMPLES", 3) load_recover_samples: int = _int("LOAD_RECOVER_SAMPLES", 10) # "normal", "busy" or "overloaded" to pin the level (testing only). load_force_level: str = os.environ.get("LOAD_FORCE_LEVEL", "") # --- AI service protection ---------------------------------------------- # Unanswered decide requests per AI connection before falling back. # Each running game has at most one AI deciding at a time, so keep this at # least MAX_ACTIVE_GAMES; 64 matches the AI service's AI_MAX_QUEUE. ai_max_inflight: int = _int("AI_MAX_INFLIGHT", 64) # Consecutive AI failures that pause AI requests for ai_breaker_cooldown seconds. ai_breaker_failures: int = _int("AI_BREAKER_FAILURES", 5) ai_breaker_cooldown: float = _float("AI_BREAKER_COOLDOWN", 30.0) settings = Settings()