660 lines
23 KiB
Python
660 lines
23 KiB
Python
"""Local development configuration for the HLL Vietnam backend bootstrap."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
DEFAULT_HOST = "127.0.0.1"
|
|
DEFAULT_PORT = 8000
|
|
DEFAULT_STORAGE_FILENAME = "hll_vietnam_dev.sqlite3"
|
|
DEFAULT_REFRESH_INTERVAL_SECONDS = 300
|
|
DEFAULT_LIVE_DATA_SOURCE = "rcon"
|
|
DEFAULT_HISTORICAL_DATA_SOURCE = "rcon"
|
|
DEFAULT_RCON_TIMEOUT_SECONDS = 20.0
|
|
DEFAULT_HISTORICAL_CRCON_PAGE_SIZE = 50
|
|
DEFAULT_HISTORICAL_CRCON_TIMEOUT_SECONDS = 15.0
|
|
DEFAULT_HISTORICAL_CRCON_DETAIL_WORKERS = 8
|
|
DEFAULT_HISTORICAL_CRCON_REQUEST_RETRIES = 3
|
|
DEFAULT_HISTORICAL_CRCON_RETRY_DELAY_SECONDS = 0.5
|
|
DEFAULT_HISTORICAL_REFRESH_INTERVAL_SECONDS = 1800
|
|
DEFAULT_HISTORICAL_REFRESH_OVERLAP_HOURS = 12
|
|
DEFAULT_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS = 900
|
|
DEFAULT_HISTORICAL_REFRESH_MAX_RETRIES = 2
|
|
DEFAULT_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS = 30
|
|
DEFAULT_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS = 4
|
|
DEFAULT_HISTORICAL_ELO_MMR_REBUILD_INTERVAL_MINUTES = 180
|
|
DEFAULT_HISTORICAL_ELO_MMR_MIN_NEW_SAMPLES = 12
|
|
DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES = 3
|
|
DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY = 2
|
|
DEFAULT_PLAYER_EVENT_REFRESH_INTERVAL_SECONDS = 1800
|
|
DEFAULT_PLAYER_EVENT_REFRESH_OVERLAP_HOURS = 12
|
|
DEFAULT_PLAYER_EVENT_REFRESH_MAX_RETRIES = 2
|
|
DEFAULT_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS = 30
|
|
DEFAULT_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS = 600
|
|
DEFAULT_RCON_HISTORICAL_CAPTURE_MAX_RETRIES = 2
|
|
DEFAULT_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS = 15
|
|
DEFAULT_RCON_CURRENT_MATCH_CAPTURE_INTERVAL_SECONDS = 5
|
|
DEFAULT_RCON_CURRENT_MATCH_WRITER_LOCK_TIMEOUT_SECONDS = 4.0
|
|
DEFAULT_RCON_BACKFILL_CHUNK_HOURS = 6
|
|
DEFAULT_RCON_BACKFILL_SLEEP_SECONDS = 1.0
|
|
DEFAULT_RCON_BACKFILL_MAX_DAYS_BACK = 45
|
|
DEFAULT_RECENT_MATCHES_KEEP = 100
|
|
DEFAULT_ADMIN_LOG_NONCRITICAL_RETENTION_DAYS = 30
|
|
DEFAULT_ADMIN_LOG_CRITICAL_RETENTION_DAYS = 90
|
|
DEFAULT_SERVER_SNAPSHOT_RETENTION_DAYS = 14
|
|
DEFAULT_DB_MAINTENANCE_BATCH_SIZE = 5000
|
|
DEFAULT_DB_MAINTENANCE_ENABLED = False
|
|
DEFAULT_DB_MAINTENANCE_INTERVAL_SECONDS = 43200
|
|
DEFAULT_SQLITE_WRITER_TIMEOUT_SECONDS = 30.0
|
|
DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 30000
|
|
DEFAULT_WRITER_LOCK_TIMEOUT_SECONDS = 120.0
|
|
DEFAULT_WRITER_LOCK_POLL_INTERVAL_SECONDS = 1.0
|
|
DEFAULT_ALLOWED_ORIGINS = (
|
|
"null",
|
|
"http://127.0.0.1",
|
|
"http://127.0.0.1:5500",
|
|
"http://127.0.0.1:8080",
|
|
"http://localhost",
|
|
"http://localhost:5500",
|
|
"http://localhost:8080",
|
|
)
|
|
DEFAULT_A2S_TARGETS_ENV_VAR = "HLL_BACKEND_A2S_TARGETS"
|
|
DEFAULT_A2S_SOURCE_NAME = "community-hispana-a2s"
|
|
DEFAULT_RCON_TARGETS_ENV_VAR = "HLL_BACKEND_RCON_TARGETS"
|
|
DEFAULT_RCON_SOURCE_NAME = "community-hispana-rcon"
|
|
|
|
|
|
def get_bind_address() -> tuple[str, int]:
|
|
"""Return the host and port used by the local backend bootstrap."""
|
|
host = os.getenv("HLL_BACKEND_HOST", DEFAULT_HOST)
|
|
port = int(os.getenv("HLL_BACKEND_PORT", str(DEFAULT_PORT)))
|
|
return host, port
|
|
|
|
|
|
def get_allowed_origins() -> tuple[str, ...]:
|
|
"""Return the small allowlist used for local frontend development."""
|
|
raw_origins = os.getenv(
|
|
"HLL_BACKEND_ALLOWED_ORIGINS",
|
|
",".join(DEFAULT_ALLOWED_ORIGINS),
|
|
)
|
|
origins = []
|
|
for origin in raw_origins.split(","):
|
|
normalized_origin = _normalize_origin(origin)
|
|
if normalized_origin:
|
|
origins.append(normalized_origin)
|
|
return tuple(origins) or DEFAULT_ALLOWED_ORIGINS
|
|
|
|
|
|
def _normalize_origin(origin: str) -> str:
|
|
"""Normalize configured origins so env overrides match browser Origin values."""
|
|
return origin.strip().rstrip("/")
|
|
|
|
|
|
def get_storage_path() -> Path:
|
|
"""Return the local SQLite path used for development snapshot persistence."""
|
|
default_path = Path(__file__).resolve().parent.parent / "data" / DEFAULT_STORAGE_FILENAME
|
|
configured_path = os.getenv("HLL_BACKEND_STORAGE_PATH")
|
|
return Path(configured_path) if configured_path else default_path
|
|
|
|
|
|
def get_database_url() -> str | None:
|
|
"""Return the optional PostgreSQL URL for migrated backend storage domains."""
|
|
configured_url = os.getenv("HLL_BACKEND_DATABASE_URL")
|
|
if configured_url is None:
|
|
return None
|
|
normalized_url = configured_url.strip()
|
|
return normalized_url or None
|
|
|
|
|
|
def use_postgres_rcon_storage(*, explicit_sqlite_path: Path | None = None) -> bool:
|
|
"""Return whether phase-1 RCON storage should use PostgreSQL."""
|
|
return explicit_sqlite_path is None and get_database_url() is not None
|
|
|
|
|
|
def get_sqlite_writer_timeout_seconds() -> float:
|
|
"""Return the SQLite connection timeout shared by writer-capable storage layers."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDS",
|
|
str(DEFAULT_SQLITE_WRITER_TIMEOUT_SECONDS),
|
|
)
|
|
timeout_seconds = float(configured_value)
|
|
if timeout_seconds <= 0:
|
|
raise ValueError("HLL_BACKEND_SQLITE_WRITER_TIMEOUT_SECONDS must be positive.")
|
|
return timeout_seconds
|
|
|
|
|
|
def get_sqlite_busy_timeout_ms() -> int:
|
|
"""Return the SQLite busy_timeout shared by writer-capable storage layers."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_SQLITE_BUSY_TIMEOUT_MS",
|
|
str(DEFAULT_SQLITE_BUSY_TIMEOUT_MS),
|
|
)
|
|
busy_timeout_ms = int(configured_value)
|
|
if busy_timeout_ms <= 0:
|
|
raise ValueError("HLL_BACKEND_SQLITE_BUSY_TIMEOUT_MS must be positive.")
|
|
return busy_timeout_ms
|
|
|
|
|
|
def get_writer_lock_timeout_seconds() -> float:
|
|
"""Return how long writer jobs should wait for the shared backend writer lock."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDS",
|
|
str(DEFAULT_WRITER_LOCK_TIMEOUT_SECONDS),
|
|
)
|
|
timeout_seconds = float(configured_value)
|
|
if timeout_seconds < 0:
|
|
raise ValueError("HLL_BACKEND_WRITER_LOCK_TIMEOUT_SECONDS must be zero or positive.")
|
|
return timeout_seconds
|
|
|
|
|
|
def get_writer_lock_poll_interval_seconds() -> float:
|
|
"""Return how often writer jobs should poll the shared backend writer lock."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDS",
|
|
str(DEFAULT_WRITER_LOCK_POLL_INTERVAL_SECONDS),
|
|
)
|
|
poll_interval_seconds = float(configured_value)
|
|
if poll_interval_seconds <= 0:
|
|
raise ValueError(
|
|
"HLL_BACKEND_WRITER_LOCK_POLL_INTERVAL_SECONDS must be positive."
|
|
)
|
|
return poll_interval_seconds
|
|
|
|
|
|
def get_refresh_interval_seconds() -> int:
|
|
"""Return the default interval used by the local refresh loop."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_REFRESH_INTERVAL_SECONDS",
|
|
str(DEFAULT_REFRESH_INTERVAL_SECONDS),
|
|
)
|
|
interval_seconds = int(configured_value)
|
|
if interval_seconds <= 0:
|
|
raise ValueError("HLL_BACKEND_REFRESH_INTERVAL_SECONDS must be positive.")
|
|
|
|
return interval_seconds
|
|
|
|
|
|
def get_historical_crcon_page_size() -> int:
|
|
"""Return the default page size used for CRCON historical ingestion."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_CRCON_PAGE_SIZE",
|
|
str(DEFAULT_HISTORICAL_CRCON_PAGE_SIZE),
|
|
)
|
|
page_size = int(configured_value)
|
|
if page_size <= 0:
|
|
raise ValueError("HLL_HISTORICAL_CRCON_PAGE_SIZE must be positive.")
|
|
|
|
return page_size
|
|
|
|
|
|
def get_historical_crcon_request_timeout_seconds() -> float:
|
|
"""Return the timeout used for CRCON historical JSON requests."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_CRCON_TIMEOUT_SECONDS",
|
|
str(DEFAULT_HISTORICAL_CRCON_TIMEOUT_SECONDS),
|
|
)
|
|
timeout_seconds = float(configured_value)
|
|
if timeout_seconds <= 0:
|
|
raise ValueError("HLL_HISTORICAL_CRCON_TIMEOUT_SECONDS must be positive.")
|
|
|
|
return timeout_seconds
|
|
|
|
|
|
def get_historical_crcon_detail_workers() -> int:
|
|
"""Return the worker count used for CRCON historical detail requests."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_CRCON_DETAIL_WORKERS",
|
|
str(DEFAULT_HISTORICAL_CRCON_DETAIL_WORKERS),
|
|
)
|
|
worker_count = int(configured_value)
|
|
if worker_count <= 0:
|
|
raise ValueError("HLL_HISTORICAL_CRCON_DETAIL_WORKERS must be positive.")
|
|
|
|
return worker_count
|
|
|
|
|
|
def get_historical_crcon_request_retries() -> int:
|
|
"""Return the retry count used for CRCON historical JSON requests."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_CRCON_REQUEST_RETRIES",
|
|
str(DEFAULT_HISTORICAL_CRCON_REQUEST_RETRIES),
|
|
)
|
|
retry_count = int(configured_value)
|
|
if retry_count <= 0:
|
|
raise ValueError("HLL_HISTORICAL_CRCON_REQUEST_RETRIES must be positive.")
|
|
|
|
return retry_count
|
|
|
|
|
|
def get_historical_crcon_retry_delay_seconds() -> float:
|
|
"""Return the base delay used between CRCON request retries."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDS",
|
|
str(DEFAULT_HISTORICAL_CRCON_RETRY_DELAY_SECONDS),
|
|
)
|
|
retry_delay_seconds = float(configured_value)
|
|
if retry_delay_seconds < 0:
|
|
raise ValueError(
|
|
"HLL_HISTORICAL_CRCON_RETRY_DELAY_SECONDS must be zero or positive."
|
|
)
|
|
|
|
return retry_delay_seconds
|
|
|
|
|
|
def get_historical_refresh_interval_seconds() -> int:
|
|
"""Return the default interval used by the historical refresh loop."""
|
|
return _read_int_env(
|
|
"HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS",
|
|
os.getenv(
|
|
"HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS",
|
|
str(DEFAULT_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS),
|
|
),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def _read_int_env(name: str, default_value: str, *, minimum: int) -> int:
|
|
"""Read one integer env var and keep validation errors actionable."""
|
|
configured_value = os.getenv(name, default_value)
|
|
try:
|
|
value = int(configured_value)
|
|
except (TypeError, ValueError) as error:
|
|
raise ValueError(f"{name} must be an integer.") from error
|
|
if value < minimum:
|
|
qualifier = "positive" if minimum == 1 else f"at least {minimum}"
|
|
raise ValueError(f"{name} must be {qualifier}.")
|
|
return value
|
|
|
|
|
|
def _read_float_env(name: str, default_value: str, *, minimum: float) -> float:
|
|
"""Read one float env var and keep validation errors actionable."""
|
|
configured_value = os.getenv(name, default_value)
|
|
try:
|
|
value = float(configured_value)
|
|
except (TypeError, ValueError) as error:
|
|
raise ValueError(f"{name} must be a number.") from error
|
|
if value < minimum:
|
|
qualifier = "zero or positive" if minimum == 0 else f"at least {minimum}"
|
|
raise ValueError(f"{name} must be {qualifier}.")
|
|
return value
|
|
|
|
|
|
def _read_bool_env(name: str, *, default: bool) -> bool:
|
|
"""Read one boolean env var using a small set of explicit truthy/falsey values."""
|
|
configured_value = os.getenv(name)
|
|
if configured_value is None:
|
|
return default
|
|
|
|
normalized = configured_value.strip().lower()
|
|
if normalized in {"1", "true", "yes", "on"}:
|
|
return True
|
|
if normalized in {"0", "false", "no", "off"}:
|
|
return False
|
|
raise ValueError(f"{name} must be a boolean value.")
|
|
|
|
|
|
def get_historical_refresh_overlap_hours() -> int:
|
|
"""Return the overlap window used by incremental historical refreshes."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_REFRESH_OVERLAP_HOURS",
|
|
str(DEFAULT_HISTORICAL_REFRESH_OVERLAP_HOURS),
|
|
)
|
|
overlap_hours = int(configured_value)
|
|
if overlap_hours < 0:
|
|
raise ValueError("HLL_HISTORICAL_REFRESH_OVERLAP_HOURS must be zero or positive.")
|
|
|
|
return overlap_hours
|
|
|
|
|
|
def get_live_data_source_kind() -> str:
|
|
"""Return the live provider kind selected for the current environment."""
|
|
source_kind = os.getenv("HLL_BACKEND_LIVE_DATA_SOURCE", DEFAULT_LIVE_DATA_SOURCE).strip()
|
|
if source_kind not in {"a2s", "rcon"}:
|
|
raise ValueError("HLL_BACKEND_LIVE_DATA_SOURCE must be 'a2s' or 'rcon'.")
|
|
return source_kind
|
|
|
|
|
|
def get_historical_data_source_kind() -> str:
|
|
"""Return the historical provider kind selected for the current environment."""
|
|
source_kind = os.getenv(
|
|
"HLL_BACKEND_HISTORICAL_DATA_SOURCE",
|
|
DEFAULT_HISTORICAL_DATA_SOURCE,
|
|
).strip()
|
|
if source_kind not in {"public-scoreboard", "rcon"}:
|
|
raise ValueError(
|
|
"HLL_BACKEND_HISTORICAL_DATA_SOURCE must be 'public-scoreboard' or 'rcon'."
|
|
)
|
|
return source_kind
|
|
|
|
|
|
def get_rcon_request_timeout_seconds() -> float:
|
|
"""Return the timeout used for HLL RCON TCP requests."""
|
|
configured_value = os.getenv(
|
|
"HLL_BACKEND_RCON_TIMEOUT_SECONDS",
|
|
str(DEFAULT_RCON_TIMEOUT_SECONDS),
|
|
)
|
|
timeout_seconds = float(configured_value)
|
|
if timeout_seconds <= 0:
|
|
raise ValueError("HLL_BACKEND_RCON_TIMEOUT_SECONDS must be positive.")
|
|
return timeout_seconds
|
|
|
|
|
|
def get_historical_refresh_max_retries() -> int:
|
|
"""Return the retry count used by the historical refresh loop."""
|
|
return _read_int_env(
|
|
"HLL_HISTORICAL_REFRESH_MAX_RETRIES",
|
|
str(DEFAULT_HISTORICAL_REFRESH_MAX_RETRIES),
|
|
minimum=0,
|
|
)
|
|
|
|
|
|
def get_historical_refresh_retry_delay_seconds() -> float:
|
|
"""Return the wait time between historical refresh retries."""
|
|
return _read_float_env(
|
|
"HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS",
|
|
str(DEFAULT_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS),
|
|
minimum=0,
|
|
)
|
|
|
|
|
|
def get_historical_full_snapshot_every_runs() -> int:
|
|
"""Return how often the runner should rebuild the full snapshot matrix."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS",
|
|
str(DEFAULT_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS),
|
|
)
|
|
run_count = int(configured_value)
|
|
if run_count <= 0:
|
|
raise ValueError("HLL_HISTORICAL_FULL_SNAPSHOT_EVERY_RUNS must be positive.")
|
|
|
|
return run_count
|
|
|
|
|
|
def get_historical_elo_mmr_rebuild_interval_minutes() -> int:
|
|
"""Return the minimum minutes between automatic Elo/MMR rebuilds."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_ELO_MMR_REBUILD_INTERVAL_MINUTES",
|
|
str(DEFAULT_HISTORICAL_ELO_MMR_REBUILD_INTERVAL_MINUTES),
|
|
)
|
|
interval_minutes = int(configured_value)
|
|
if interval_minutes <= 0:
|
|
raise ValueError("HLL_HISTORICAL_ELO_MMR_REBUILD_INTERVAL_MINUTES must be positive.")
|
|
return interval_minutes
|
|
|
|
|
|
def get_historical_elo_mmr_min_new_samples() -> int:
|
|
"""Return the minimum new RCON samples required for an automatic Elo/MMR rebuild."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_ELO_MMR_MIN_NEW_SAMPLES",
|
|
str(DEFAULT_HISTORICAL_ELO_MMR_MIN_NEW_SAMPLES),
|
|
)
|
|
min_samples = int(configured_value)
|
|
if min_samples <= 0:
|
|
raise ValueError("HLL_HISTORICAL_ELO_MMR_MIN_NEW_SAMPLES must be positive.")
|
|
return min_samples
|
|
|
|
|
|
def get_historical_weekly_fallback_min_matches() -> int:
|
|
"""Return the minimum closed matches required to trust the current week."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES",
|
|
str(DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES),
|
|
)
|
|
min_matches = int(configured_value)
|
|
if min_matches <= 0:
|
|
raise ValueError("HLL_HISTORICAL_WEEKLY_FALLBACK_MIN_MATCHES must be positive.")
|
|
|
|
return min_matches
|
|
|
|
|
|
def get_historical_weekly_fallback_max_weekday() -> int:
|
|
"""Return the last weekday index where weekly fallback may still apply."""
|
|
configured_value = os.getenv(
|
|
"HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY",
|
|
str(DEFAULT_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY),
|
|
)
|
|
max_weekday = int(configured_value)
|
|
if max_weekday < 0 or max_weekday > 6:
|
|
raise ValueError("HLL_HISTORICAL_WEEKLY_FALLBACK_MAX_WEEKDAY must be between 0 and 6.")
|
|
|
|
return max_weekday
|
|
|
|
|
|
def get_player_event_refresh_interval_seconds() -> int:
|
|
"""Return the default interval used by the player event refresh loop."""
|
|
configured_value = os.getenv(
|
|
"HLL_PLAYER_EVENT_REFRESH_INTERVAL_SECONDS",
|
|
str(DEFAULT_PLAYER_EVENT_REFRESH_INTERVAL_SECONDS),
|
|
)
|
|
interval_seconds = int(configured_value)
|
|
if interval_seconds <= 0:
|
|
raise ValueError("HLL_PLAYER_EVENT_REFRESH_INTERVAL_SECONDS must be positive.")
|
|
return interval_seconds
|
|
|
|
|
|
def get_player_event_refresh_overlap_hours() -> int:
|
|
"""Return the overlap window used by player event refresh runs."""
|
|
configured_value = os.getenv(
|
|
"HLL_PLAYER_EVENT_REFRESH_OVERLAP_HOURS",
|
|
str(DEFAULT_PLAYER_EVENT_REFRESH_OVERLAP_HOURS),
|
|
)
|
|
overlap_hours = int(configured_value)
|
|
if overlap_hours < 0:
|
|
raise ValueError("HLL_PLAYER_EVENT_REFRESH_OVERLAP_HOURS must be zero or positive.")
|
|
return overlap_hours
|
|
|
|
|
|
def get_player_event_refresh_max_retries() -> int:
|
|
"""Return the retry count used by the player event refresh loop."""
|
|
configured_value = os.getenv(
|
|
"HLL_PLAYER_EVENT_REFRESH_MAX_RETRIES",
|
|
str(DEFAULT_PLAYER_EVENT_REFRESH_MAX_RETRIES),
|
|
)
|
|
max_retries = int(configured_value)
|
|
if max_retries < 0:
|
|
raise ValueError("HLL_PLAYER_EVENT_REFRESH_MAX_RETRIES must be zero or positive.")
|
|
return max_retries
|
|
|
|
|
|
def get_player_event_refresh_retry_delay_seconds() -> int:
|
|
"""Return the wait time between player event refresh retries."""
|
|
configured_value = os.getenv(
|
|
"HLL_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS",
|
|
str(DEFAULT_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS),
|
|
)
|
|
retry_delay_seconds = int(configured_value)
|
|
if retry_delay_seconds < 0:
|
|
raise ValueError(
|
|
"HLL_PLAYER_EVENT_REFRESH_RETRY_DELAY_SECONDS must be zero or positive."
|
|
)
|
|
return retry_delay_seconds
|
|
|
|
|
|
def get_rcon_historical_capture_interval_seconds() -> int:
|
|
"""Return the default interval used by the prospective RCON capture loop."""
|
|
configured_value = os.getenv(
|
|
"HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS",
|
|
str(DEFAULT_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS),
|
|
)
|
|
interval_seconds = int(configured_value)
|
|
if interval_seconds <= 0:
|
|
raise ValueError("HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS must be positive.")
|
|
return interval_seconds
|
|
|
|
|
|
def get_rcon_historical_capture_max_retries() -> int:
|
|
"""Return the retry count used by the prospective RCON capture loop."""
|
|
configured_value = os.getenv(
|
|
"HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES",
|
|
str(DEFAULT_RCON_HISTORICAL_CAPTURE_MAX_RETRIES),
|
|
)
|
|
max_retries = int(configured_value)
|
|
if max_retries < 0:
|
|
raise ValueError("HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES must be zero or positive.")
|
|
return max_retries
|
|
|
|
|
|
def get_rcon_historical_capture_retry_delay_seconds() -> int:
|
|
"""Return the wait time between failed prospective RCON capture attempts."""
|
|
configured_value = os.getenv(
|
|
"HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS",
|
|
str(DEFAULT_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS),
|
|
)
|
|
retry_delay_seconds = int(configured_value)
|
|
if retry_delay_seconds < 0:
|
|
raise ValueError(
|
|
"HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS must be zero or positive."
|
|
)
|
|
return retry_delay_seconds
|
|
|
|
|
|
def get_rcon_capture_mode() -> str:
|
|
"""Return whether the worker runs the normal historical pipeline or live capture only."""
|
|
configured_mode = os.getenv("HLL_RCON_CAPTURE_MODE")
|
|
if configured_mode is not None:
|
|
normalized_mode = configured_mode.strip().lower()
|
|
if normalized_mode in {"historical", "current-live"}:
|
|
return normalized_mode
|
|
raise ValueError("HLL_RCON_CAPTURE_MODE must be 'historical' or 'current-live'.")
|
|
|
|
if _read_bool_env("HLL_RCON_CURRENT_MATCH_MODE", default=False):
|
|
return "current-live"
|
|
if _read_bool_env("HLL_RCON_SKIP_HISTORICAL_MATERIALIZATION", default=False):
|
|
return "current-live"
|
|
return "historical"
|
|
|
|
|
|
def get_rcon_skip_historical_materialization() -> bool:
|
|
"""Return whether the worker must skip heavy historical AdminLog materialization."""
|
|
return get_rcon_capture_mode() == "current-live"
|
|
|
|
|
|
def get_rcon_current_match_capture_interval_seconds() -> int:
|
|
"""Return the preferred live current-match capture interval."""
|
|
return _read_int_env(
|
|
"HLL_RCON_CURRENT_MATCH_CAPTURE_INTERVAL_SECONDS",
|
|
str(DEFAULT_RCON_CURRENT_MATCH_CAPTURE_INTERVAL_SECONDS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_rcon_current_match_writer_lock_timeout_seconds() -> float:
|
|
"""Return the lock wait budget for the lightweight current-match worker."""
|
|
return _read_float_env(
|
|
"HLL_RCON_CURRENT_MATCH_WRITER_LOCK_TIMEOUT_SECONDS",
|
|
str(DEFAULT_RCON_CURRENT_MATCH_WRITER_LOCK_TIMEOUT_SECONDS),
|
|
minimum=0,
|
|
)
|
|
|
|
|
|
def get_rcon_backfill_chunk_hours() -> int:
|
|
"""Return the AdminLog backfill chunk size in hours."""
|
|
return _read_int_env(
|
|
"HLL_RCON_BACKFILL_CHUNK_HOURS",
|
|
str(DEFAULT_RCON_BACKFILL_CHUNK_HOURS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_rcon_backfill_sleep_seconds() -> float:
|
|
"""Return the delay between AdminLog backfill RCON requests."""
|
|
return _read_float_env(
|
|
"HLL_RCON_BACKFILL_SLEEP_SECONDS",
|
|
str(DEFAULT_RCON_BACKFILL_SLEEP_SECONDS),
|
|
minimum=0,
|
|
)
|
|
|
|
|
|
def get_rcon_backfill_max_days_back() -> int:
|
|
"""Return the maximum AdminLog backfill lookback horizon in days."""
|
|
return _read_int_env(
|
|
"HLL_RCON_BACKFILL_MAX_DAYS_BACK",
|
|
str(DEFAULT_RCON_BACKFILL_MAX_DAYS_BACK),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_recent_matches_keep() -> int:
|
|
"""Return how many recent closed materialized matches maintenance must protect."""
|
|
return _read_int_env(
|
|
"HLL_RECENT_MATCHES_KEEP",
|
|
str(DEFAULT_RECENT_MATCHES_KEEP),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_admin_log_noncritical_retention_days() -> int:
|
|
"""Return retention days for non-critical AdminLog events."""
|
|
return _read_int_env(
|
|
"HLL_ADMIN_LOG_NONCRITICAL_RETENTION_DAYS",
|
|
str(DEFAULT_ADMIN_LOG_NONCRITICAL_RETENTION_DAYS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_admin_log_critical_retention_days() -> int:
|
|
"""Return retention days for critical AdminLog events."""
|
|
return _read_int_env(
|
|
"HLL_ADMIN_LOG_CRITICAL_RETENTION_DAYS",
|
|
str(DEFAULT_ADMIN_LOG_CRITICAL_RETENTION_DAYS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_server_snapshot_retention_days() -> int:
|
|
"""Return retention days for live server snapshots."""
|
|
return _read_int_env(
|
|
"HLL_SERVER_SNAPSHOT_RETENTION_DAYS",
|
|
str(DEFAULT_SERVER_SNAPSHOT_RETENTION_DAYS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_db_maintenance_batch_size() -> int:
|
|
"""Return the delete batch size used by database maintenance."""
|
|
return _read_int_env(
|
|
"HLL_DB_MAINTENANCE_BATCH_SIZE",
|
|
str(DEFAULT_DB_MAINTENANCE_BATCH_SIZE),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_db_maintenance_enabled() -> bool:
|
|
"""Return whether scheduled database maintenance is enabled."""
|
|
normalized = os.getenv(
|
|
"HLL_DB_MAINTENANCE_ENABLED",
|
|
"true" if DEFAULT_DB_MAINTENANCE_ENABLED else "false",
|
|
).strip().lower()
|
|
return normalized in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def get_db_maintenance_interval_seconds() -> int:
|
|
"""Return the scheduled database maintenance interval in seconds."""
|
|
return _read_int_env(
|
|
"HLL_DB_MAINTENANCE_INTERVAL_SECONDS",
|
|
str(DEFAULT_DB_MAINTENANCE_INTERVAL_SECONDS),
|
|
minimum=1,
|
|
)
|
|
|
|
|
|
def get_a2s_targets_payload() -> str | None:
|
|
"""Return the optional JSON payload that overrides local A2S targets."""
|
|
raw_payload = os.getenv(DEFAULT_A2S_TARGETS_ENV_VAR)
|
|
if raw_payload is None:
|
|
return None
|
|
|
|
normalized = raw_payload.strip()
|
|
return normalized or None
|
|
|
|
|
|
def get_rcon_targets_payload() -> str | None:
|
|
"""Return the optional JSON payload that defines live RCON targets."""
|
|
raw_payload = os.getenv(DEFAULT_RCON_TARGETS_ENV_VAR)
|
|
if raw_payload is None:
|
|
return None
|
|
|
|
normalized = raw_payload.strip()
|
|
return normalized or None
|