Fix public snapshot scheduler job failures

This commit is contained in:
devRaGonSa
2026-06-11 10:22:37 +02:00
parent d6f1739160
commit 915d074b46
8 changed files with 297 additions and 27 deletions

View File

@@ -51,11 +51,13 @@ from .rcon_annual_rankings import (
)
from .rcon_historical_leaderboards import refresh_ranking_snapshots
from .rcon_historical_leaderboards import SNAPSHOT_GENERATOR_SERVER_KEYS
from .rcon_historical_leaderboards import initialize_ranking_snapshot_storage
from .rcon_historical_player_stats import (
refresh_player_period_stats,
refresh_player_search_index,
)
from .rcon_historical_storage import count_rcon_historical_samples_since
from .rcon_historical_storage import initialize_rcon_historical_storage
from .rcon_historical_worker import run_rcon_historical_capture
from .writer_lock import backend_writer_lock, build_writer_lock_holder
@@ -747,10 +749,12 @@ def refresh_public_weekly_ranking_snapshots(
}
)
try:
initialize_ranking_snapshot_storage(ensure_storage=True)
result = refresh_ranking_snapshots(
limit=30,
replace_existing=True,
timeframes=("weekly",),
ensure_storage=False,
now=anchor,
)
except Exception as exc: # noqa: BLE001 - scheduler must keep the runner alive
@@ -803,10 +807,12 @@ def refresh_public_monthly_ranking_snapshots(
}
)
try:
initialize_ranking_snapshot_storage(ensure_storage=True)
result = refresh_ranking_snapshots(
limit=30,
replace_existing=True,
timeframes=("monthly",),
ensure_storage=False,
now=anchor,
)
except Exception as exc: # noqa: BLE001 - scheduler must keep the runner alive
@@ -978,12 +984,14 @@ def refresh_public_weekly_historical_snapshots(
}
)
try:
initialize_rcon_historical_storage(ensure_storage=True)
result = {
"status": "ok",
**generate_and_persist_historical_leaderboard_snapshots(
timeframe="weekly",
server_keys=PREWARM_SNAPSHOT_SERVER_KEYS,
generated_at=anchor,
ensure_storage=False,
),
}
except Exception as exc: # noqa: BLE001 - scheduler must keep the runner alive
@@ -1037,11 +1045,13 @@ def refresh_public_monthly_historical_snapshots(
}
)
try:
initialize_rcon_historical_storage(ensure_storage=True)
result = {
"status": "ok",
**generate_and_persist_historical_monthly_ui_snapshots(
server_keys=PREWARM_SNAPSHOT_SERVER_KEYS,
generated_at=anchor,
ensure_storage=False,
),
}
except Exception as exc: # noqa: BLE001 - scheduler must keep the runner alive
@@ -1340,6 +1350,14 @@ def _to_iso(value: datetime) -> str:
return _as_utc(value).isoformat().replace("+00:00", "Z")
def _json_default(value: object) -> str:
if isinstance(value, datetime):
return _to_iso(value)
if isinstance(value, date):
return value.isoformat()
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
def _resolve_refresh_cycle_status(**results: dict[str, Any]) -> str:
statuses = [
str(result.get("status") or "").strip().lower()
@@ -1658,7 +1676,7 @@ def main() -> None:
raise ValueError("--max-runs must be positive when provided.")
if args.public_job is not None:
payload = run_public_refresh_job_once(args.public_job)
print(json.dumps({"status": "ok", "data": payload}, indent=2))
print(json.dumps({"status": "ok", "data": payload}, indent=2, default=_json_default))
return
run_periodic_historical_refresh(

View File

@@ -398,6 +398,7 @@ def generate_and_persist_historical_leaderboard_snapshots(
server_keys: tuple[str, ...] = PREWARM_SNAPSHOT_SERVER_KEYS,
generated_at: datetime | None = None,
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
ensure_storage: bool = True,
db_path: Path | None = None,
) -> dict[str, object]:
"""Build and persist only the weekly or monthly leaderboard snapshot set."""
@@ -424,6 +425,7 @@ def generate_and_persist_historical_leaderboard_snapshots(
metric,
generated_at_value,
limit=leaderboard_limit,
ensure_storage=ensure_storage if not snapshots else False,
db_path=db_path,
)
)
@@ -440,6 +442,7 @@ def generate_and_persist_historical_leaderboard_snapshots(
metric,
generated_at_value,
limit=leaderboard_limit,
ensure_storage=ensure_storage if not snapshots else False,
db_path=db_path,
)
)
@@ -467,6 +470,7 @@ def generate_and_persist_historical_monthly_ui_snapshots(
server_keys: tuple[str, ...] = PREWARM_SNAPSHOT_SERVER_KEYS,
generated_at: datetime | None = None,
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
ensure_storage: bool = True,
db_path: Path | None = None,
) -> dict[str, object]:
"""Build and persist the monthly historical subset rendered by the public UI."""
@@ -489,6 +493,7 @@ def generate_and_persist_historical_monthly_ui_snapshots(
metric,
generated_at_value,
limit=leaderboard_limit,
ensure_storage=ensure_storage if not snapshots else False,
db_path=db_path,
)
)
@@ -507,6 +512,7 @@ def generate_and_persist_historical_monthly_ui_snapshots(
server_key,
generated_at_value,
limit=leaderboard_limit,
tolerate_missing_player_event_ledger=True,
db_path=db_path,
)
)
@@ -613,6 +619,7 @@ def _build_weekly_leaderboard_snapshot(
generated_at: datetime,
*,
limit: int,
ensure_storage: bool = True,
db_path: Path | None = None,
) -> dict[str, object]:
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
@@ -623,6 +630,7 @@ def _build_weekly_leaderboard_snapshot(
server_key=server_key,
metric=metric,
timeframe="weekly",
ensure_storage=ensure_storage,
db_path=db_path,
now=generated_at,
)
@@ -658,6 +666,7 @@ def _build_monthly_leaderboard_snapshot(
generated_at: datetime,
*,
limit: int,
ensure_storage: bool = True,
db_path: Path | None = None,
) -> dict[str, object]:
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
@@ -668,6 +677,7 @@ def _build_monthly_leaderboard_snapshot(
server_key=server_key,
metric=metric,
timeframe="monthly",
ensure_storage=ensure_storage,
db_path=db_path,
now=generated_at,
)
@@ -829,13 +839,49 @@ def _build_monthly_mvp_v2_snapshot(
generated_at: datetime,
*,
limit: int,
tolerate_missing_player_event_ledger: bool = False,
db_path: Path | None = None,
) -> dict[str, object]:
ranking_result = list_monthly_mvp_v2_ranking(
limit=limit,
server_id=server_key,
db_path=db_path,
)
try:
ranking_result = list_monthly_mvp_v2_ranking(
limit=limit,
server_id=server_key,
db_path=db_path,
)
except sqlite3.OperationalError as error:
if not tolerate_missing_player_event_ledger or "player_event_raw_ledger" not in str(error):
raise
ranking_result = {
"timeframe": "monthly",
"metric": "mvp-v2",
"ranking_version": "v2",
"window_start": None,
"window_end": None,
"window_days": None,
"window_kind": "missing-player-event-ledger",
"window_label": "Sin ledger de eventos",
"uses_fallback": True,
"selection_reason": "player-event-raw-ledger-missing",
"current_month_start": None,
"current_month_closed_matches": 0,
"previous_month_closed_matches": 0,
"sufficient_sample": {
"minimum_closed_matches": 0,
"current_month_closed_matches": 0,
"current_month_has_sufficient_sample": False,
"is_early_month": False,
},
"event_coverage": {
"ready": False,
"reason": "player-event-raw-ledger-missing",
"source_range_start": None,
"source_range_end": None,
},
"eligibility": None,
"eligible_players_count": 0,
"items": [],
"error": str(error),
}
month_key = str(ranking_result.get("window_start") or "")[:7] or None
event_coverage = ranking_result.get("event_coverage")
source_range_start = None

View File

@@ -1761,11 +1761,22 @@ def list_monthly_mvp_v2_ranking(
window_start = monthly_window["window_start"]
window_end = monthly_window["window_end"]
month_key = window_start.strftime("%Y-%m")
event_coverage = _get_monthly_player_event_coverage(
server_id=server_id,
month_key=month_key,
db_path=resolved_path,
)
try:
event_coverage = _get_monthly_player_event_coverage(
server_id=server_id,
month_key=month_key,
db_path=resolved_path,
)
except sqlite3.OperationalError as error:
if "player_event_raw_ledger" not in str(error):
raise
event_coverage = {
"ready": False,
"reason": "player-event-raw-ledger-missing",
"source_range_start": None,
"source_range_end": None,
"error": str(error),
}
window_days = _calculate_window_days(window_start=window_start, window_end=window_end)
empty_result = {

View File

@@ -101,13 +101,21 @@ ON ranking_snapshot_items(snapshot_id, player_id);
"""
def initialize_ranking_snapshot_storage(*, db_path: Path | None = None) -> Path:
def initialize_ranking_snapshot_storage(
*,
db_path: Path | None = None,
ensure_storage: bool = True,
) -> Path:
"""Create ranking snapshot tables used by weekly/monthly public ranking reads."""
resolved_path = initialize_rcon_materialized_storage(db_path=db_path)
resolved_path = initialize_rcon_materialized_storage(
db_path=db_path,
ensure_storage=ensure_storage,
)
if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
from .postgres_rcon_storage import initialize_postgres_rcon_storage
if ensure_storage:
from .postgres_rcon_storage import initialize_postgres_rcon_storage
initialize_postgres_rcon_storage()
initialize_postgres_rcon_storage()
return resolved_path
with closing(connect_sqlite_writer(resolved_path)) as connection:
@@ -132,6 +140,7 @@ def generate_ranking_snapshot(
metric: str,
limit: int,
replace_existing: bool = True,
ensure_storage: bool = True,
now: datetime | None = None,
db_path: Path | None = None,
) -> dict[str, object]:
@@ -142,8 +151,15 @@ def generate_ranking_snapshot(
normalized_limit = _normalize_limit(limit)
anchor = _as_utc(now or datetime.now(timezone.utc))
resolved_path = initialize_ranking_snapshot_storage(db_path=db_path)
connection_scope = _connect_write_scope(resolved_path, db_path=db_path)
resolved_path = initialize_ranking_snapshot_storage(
db_path=db_path,
ensure_storage=ensure_storage,
)
connection_scope = _connect_write_scope(
resolved_path,
db_path=db_path,
initialize=ensure_storage,
)
with connection_scope as connection:
window = select_leaderboard_window(
connection=connection,
@@ -231,6 +247,7 @@ def refresh_ranking_snapshots(
limit: int = DEFAULT_RANKING_SNAPSHOT_REFRESH_LIMIT,
replace_existing: bool = True,
timeframes: tuple[str, ...] | None = None,
ensure_storage: bool = True,
now: datetime | None = None,
db_path: Path | None = None,
) -> dict[str, object]:
@@ -260,6 +277,7 @@ def refresh_ranking_snapshots(
metric=metric,
limit=normalized_limit,
replace_existing=replace_existing,
ensure_storage=ensure_storage if not results else False,
now=anchor,
db_path=db_path,
)
@@ -532,6 +550,7 @@ def list_rcon_materialized_leaderboard(
timeframe: str = "weekly",
metric: str = "kills",
limit: int = 10,
ensure_storage: bool = True,
db_path: Path | None = None,
now: datetime | None = None,
) -> dict[str, object]:
@@ -547,8 +566,15 @@ def list_rcon_materialized_leaderboard(
normalized_limit = max(1, int(limit or 10))
anchor = _as_utc(now or datetime.now(timezone.utc))
resolved_path = initialize_rcon_materialized_storage(db_path=db_path)
connection_scope = _connect_scope(resolved_path, db_path=db_path)
resolved_path = initialize_rcon_materialized_storage(
db_path=db_path,
ensure_storage=ensure_storage,
)
connection_scope = _connect_scope(
resolved_path,
db_path=db_path,
initialize=ensure_storage,
)
with connection_scope as connection:
window = select_leaderboard_window(
connection=connection,
@@ -809,7 +835,8 @@ def _insert_snapshot_items(
rows: list[dict[str, object]],
limit: int,
) -> None:
for index, row in enumerate(rows[:limit], start=1):
deduplicated_rows = _dedupe_snapshot_rows(rows)
for index, row in enumerate(deduplicated_rows[:limit], start=1):
item = _build_item(row, index=index)
connection.execute(
"""
@@ -843,6 +870,19 @@ def _insert_snapshot_items(
)
def _dedupe_snapshot_rows(rows: list[dict[str, object]]) -> list[dict[str, object]]:
"""Keep the first ranked row for each player id to preserve deterministic snapshot order."""
deduplicated_rows: list[dict[str, object]] = []
seen_player_ids: set[str] = set()
for row in rows:
player_id = str(row.get("player_id") or "").strip()
if not player_id or player_id in seen_player_ids:
continue
seen_player_ids.add(player_id)
deduplicated_rows.append(row)
return deduplicated_rows
def _fetch_leaderboard_rows(
connection: object,
*,
@@ -1124,19 +1164,24 @@ def _normalize_snapshot_server_key(server_key: str | None) -> str:
return normalized
def _connect_scope(resolved_path: Path, *, db_path: Path | None):
def _connect_scope(resolved_path: Path, *, db_path: Path | None, initialize: bool = True):
if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
from .postgres_rcon_storage import connect_postgres_compat
return connect_postgres_compat()
return connect_postgres_compat(initialize=initialize)
return closing(connect_sqlite_readonly(resolved_path))
def _connect_write_scope(resolved_path: Path, *, db_path: Path | None):
def _connect_write_scope(
resolved_path: Path,
*,
db_path: Path | None,
initialize: bool = True,
):
if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
from .postgres_rcon_storage import connect_postgres_compat
return connect_postgres_compat()
return connect_postgres_compat(initialize=initialize)
return connect_sqlite_writer(resolved_path)

View File

@@ -20,12 +20,17 @@ COMPETITIVE_MODE_APPROXIMATE = "approximate"
COMPETITIVE_MODE_EXACT = "exact"
def initialize_rcon_historical_storage(*, db_path: Path | None = None) -> Path:
def initialize_rcon_historical_storage(
*,
db_path: Path | None = None,
ensure_storage: bool = True,
) -> Path:
"""Create the SQLite structures used by prospective RCON capture."""
if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
from .postgres_rcon_storage import initialize_postgres_rcon_storage
if ensure_storage:
from .postgres_rcon_storage import initialize_postgres_rcon_storage
initialize_postgres_rcon_storage()
initialize_postgres_rcon_storage()
return get_storage_path()
resolved_path = db_path or get_storage_path()