Automate ranking snapshot refresh
This commit is contained in:
@@ -29,6 +29,7 @@ from .historical_snapshots import (
|
||||
generate_and_persist_historical_snapshots,
|
||||
generate_and_persist_priority_historical_snapshots,
|
||||
)
|
||||
from .rcon_historical_leaderboards import refresh_ranking_snapshots
|
||||
from .rcon_historical_storage import count_rcon_historical_samples_since
|
||||
from .rcon_historical_worker import run_rcon_historical_capture
|
||||
from .writer_lock import backend_writer_lock, build_writer_lock_holder
|
||||
@@ -176,6 +177,10 @@ def _run_refresh_with_retries(
|
||||
rcon_capture_result=rcon_capture_result
|
||||
),
|
||||
}
|
||||
ranking_snapshot_result = refresh_periodic_ranking_snapshots(
|
||||
server_slug=server_slug,
|
||||
run_number=run_number,
|
||||
)
|
||||
maintenance_result = _maybe_run_database_maintenance()
|
||||
return {
|
||||
"status": "ok",
|
||||
@@ -186,6 +191,7 @@ def _run_refresh_with_retries(
|
||||
"classic_fallback_reason": classic_fallback_reason,
|
||||
"refresh_result": refresh_result,
|
||||
"snapshot_result": snapshot_result,
|
||||
"ranking_snapshot_result": ranking_snapshot_result,
|
||||
"elo_mmr_result": elo_mmr_result,
|
||||
"database_maintenance_result": maintenance_result,
|
||||
}
|
||||
@@ -251,6 +257,35 @@ def generate_historical_snapshots(
|
||||
}
|
||||
|
||||
|
||||
def refresh_periodic_ranking_snapshots(
|
||||
*,
|
||||
server_slug: str | None = None,
|
||||
run_number: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""Refresh the public weekly/monthly ranking snapshot matrix for the current cycle."""
|
||||
_emit_json_log(
|
||||
{
|
||||
"event": "ranking-snapshot-refresh-started",
|
||||
"run_number": run_number,
|
||||
"server_slug": server_slug,
|
||||
"snapshot_scope": _describe_snapshot_scope(server_slug),
|
||||
"ranking_snapshot_limit": 30,
|
||||
}
|
||||
)
|
||||
result = refresh_ranking_snapshots(limit=30)
|
||||
return {
|
||||
**result,
|
||||
"run_number": run_number,
|
||||
"refresh_interval_seconds": get_historical_refresh_interval_seconds(),
|
||||
"server_slug": server_slug,
|
||||
"generation_policy": "periodic-historical-refresh-cycle",
|
||||
"recommended_frequency": {
|
||||
"weekly": "5-15-minutes",
|
||||
"monthly": "15-30-minutes",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _emit_json_log(payload: dict[str, Any]) -> None:
|
||||
"""Print JSON logs that remain safe for Compose and log collectors."""
|
||||
print(json.dumps(payload, ensure_ascii=True, default=str), flush=True)
|
||||
|
||||
@@ -44,6 +44,7 @@ SNAPSHOT_GENERATOR_METRICS = (
|
||||
"kd_ratio",
|
||||
"kills_per_match",
|
||||
)
|
||||
DEFAULT_RANKING_SNAPSHOT_REFRESH_LIMIT = 30
|
||||
|
||||
RANKING_SNAPSHOT_SQLITE_SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS ranking_snapshots (
|
||||
@@ -219,6 +220,98 @@ def generate_ranking_snapshot(
|
||||
}
|
||||
|
||||
|
||||
def refresh_ranking_snapshots(
|
||||
*,
|
||||
limit: int = DEFAULT_RANKING_SNAPSHOT_REFRESH_LIMIT,
|
||||
replace_existing: bool = True,
|
||||
now: datetime | None = None,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Generate the full weekly/monthly ranking snapshot matrix with partial-failure reporting."""
|
||||
normalized_limit = _normalize_limit(limit)
|
||||
anchor = _as_utc(now or datetime.now(timezone.utc))
|
||||
combinations = [
|
||||
(timeframe, server_key, metric)
|
||||
for timeframe in SNAPSHOT_GENERATOR_TIMEFRAMES
|
||||
for server_key in SNAPSHOT_GENERATOR_SERVER_KEYS
|
||||
for metric in SNAPSHOT_GENERATOR_METRICS
|
||||
]
|
||||
results: list[dict[str, object]] = []
|
||||
succeeded = 0
|
||||
failed = 0
|
||||
skipped_regeneration = 0
|
||||
|
||||
for timeframe, server_key, metric in combinations:
|
||||
try:
|
||||
payload = generate_ranking_snapshot(
|
||||
timeframe=timeframe,
|
||||
server_key=server_key,
|
||||
metric=metric,
|
||||
limit=normalized_limit,
|
||||
replace_existing=replace_existing,
|
||||
now=anchor,
|
||||
db_path=db_path,
|
||||
)
|
||||
snapshot = payload.get("snapshot") if isinstance(payload, dict) else {}
|
||||
skipped = bool(payload.get("skipped_regeneration")) if isinstance(payload, dict) else False
|
||||
if skipped:
|
||||
skipped_regeneration += 1
|
||||
succeeded += 1
|
||||
results.append(
|
||||
{
|
||||
"status": "ok",
|
||||
"timeframe": timeframe,
|
||||
"server_key": server_key,
|
||||
"metric": metric,
|
||||
"limit": normalized_limit,
|
||||
"snapshot_id": snapshot.get("id") if isinstance(snapshot, dict) else None,
|
||||
"snapshot_status": snapshot.get("snapshot_status")
|
||||
if isinstance(snapshot, dict)
|
||||
else None,
|
||||
"window_start": snapshot.get("window_start") if isinstance(snapshot, dict) else None,
|
||||
"window_end": snapshot.get("window_end") if isinstance(snapshot, dict) else None,
|
||||
"generated_at": snapshot.get("generated_at") if isinstance(snapshot, dict) else None,
|
||||
"ranked_players": int(payload.get("ranked_players") or 0),
|
||||
"source_matches_count": int(payload.get("source_matches_count") or 0),
|
||||
"skipped_regeneration": skipped,
|
||||
}
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - bulk refresh must report per-combination failures
|
||||
failed += 1
|
||||
results.append(
|
||||
{
|
||||
"status": "error",
|
||||
"timeframe": timeframe,
|
||||
"server_key": server_key,
|
||||
"metric": metric,
|
||||
"limit": normalized_limit,
|
||||
"error_type": type(exc).__name__,
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
overall_status = "ok"
|
||||
if failed and succeeded:
|
||||
overall_status = "partial"
|
||||
elif failed:
|
||||
overall_status = "error"
|
||||
|
||||
return {
|
||||
"status": overall_status,
|
||||
"generated_at": _to_iso(anchor),
|
||||
"limit": normalized_limit,
|
||||
"replace_existing": replace_existing,
|
||||
"combinations_expected": len(combinations),
|
||||
"totals": {
|
||||
"combinations_expected": len(combinations),
|
||||
"succeeded": succeeded,
|
||||
"failed": failed,
|
||||
"skipped_regeneration": skipped_regeneration,
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def get_latest_ranking_snapshot(
|
||||
*,
|
||||
server_key: str | None = None,
|
||||
@@ -1275,7 +1368,25 @@ def _main(argv: list[str] | None = None) -> int:
|
||||
dest="replace_existing",
|
||||
help="keep an existing snapshot for the selected exact window and metric scope",
|
||||
)
|
||||
parser.set_defaults(command="generate-ranking-snapshot", replace_existing=True)
|
||||
refresh_parser = subparsers.add_parser("refresh-ranking-snapshots")
|
||||
refresh_parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=DEFAULT_RANKING_SNAPSHOT_REFRESH_LIMIT,
|
||||
)
|
||||
refresh_parser.add_argument(
|
||||
"--sqlite-path",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="explicit local SQLite override; default operational mode uses PostgreSQL when configured",
|
||||
)
|
||||
refresh_parser.add_argument(
|
||||
"--no-replace-existing",
|
||||
action="store_false",
|
||||
dest="replace_existing",
|
||||
help="keep an existing snapshot for the selected exact window and metric scope",
|
||||
)
|
||||
parser.set_defaults(replace_existing=True)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "generate-ranking-snapshot":
|
||||
@@ -1297,6 +1408,22 @@ def _main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
return 0
|
||||
|
||||
if args.command == "refresh-ranking-snapshots":
|
||||
payload = refresh_ranking_snapshots(
|
||||
limit=args.limit,
|
||||
replace_existing=args.replace_existing,
|
||||
db_path=args.sqlite_path,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "ok", "data": payload},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
default=_json_default,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
parser.print_help()
|
||||
return 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user