Isolate runner read model refresh from legacy snapshot failures
This commit is contained in:
@@ -127,7 +127,7 @@ def _run_refresh_with_retries(
|
||||
page_size=page_size,
|
||||
rebuild_snapshots=False,
|
||||
)
|
||||
snapshot_result = generate_historical_snapshots(
|
||||
historical_snapshot_result = refresh_periodic_historical_snapshots(
|
||||
server_slug=server_slug,
|
||||
run_number=run_number,
|
||||
)
|
||||
@@ -141,12 +141,12 @@ def _run_refresh_with_retries(
|
||||
"reason": "rcon-primary-cycle-no-classic-fallback-needed",
|
||||
}
|
||||
if should_generate_snapshots:
|
||||
snapshot_result = generate_historical_snapshots(
|
||||
historical_snapshot_result = refresh_periodic_historical_snapshots(
|
||||
server_slug=server_slug,
|
||||
run_number=run_number,
|
||||
)
|
||||
snapshot_result = {
|
||||
**snapshot_result,
|
||||
historical_snapshot_result = {
|
||||
**historical_snapshot_result,
|
||||
"generation_policy": "rcon-primary-useful-cycle",
|
||||
"reason": "rcon-primary-cycle-produced-new-useful-coverage",
|
||||
}
|
||||
@@ -168,7 +168,7 @@ def _run_refresh_with_retries(
|
||||
**elo_policy,
|
||||
}
|
||||
else:
|
||||
snapshot_result = {
|
||||
historical_snapshot_result = {
|
||||
"status": "skipped",
|
||||
"reason": "rcon-primary-cycle-had-no-new-useful-data",
|
||||
"generation_policy": "rcon-primary-no-new-useful-data",
|
||||
@@ -197,7 +197,7 @@ def _run_refresh_with_retries(
|
||||
return {
|
||||
"status": _resolve_refresh_cycle_status(
|
||||
refresh_result=refresh_result,
|
||||
snapshot_result=snapshot_result,
|
||||
historical_snapshot_result=historical_snapshot_result,
|
||||
player_search_index_result=player_search_index_result,
|
||||
player_period_stats_result=player_period_stats_result,
|
||||
ranking_snapshot_result=ranking_snapshot_result,
|
||||
@@ -210,7 +210,8 @@ def _run_refresh_with_retries(
|
||||
"classic_fallback_used": should_run_classic_fallback,
|
||||
"classic_fallback_reason": classic_fallback_reason,
|
||||
"refresh_result": refresh_result,
|
||||
"snapshot_result": snapshot_result,
|
||||
"historical_snapshot_result": historical_snapshot_result,
|
||||
"snapshot_result": historical_snapshot_result,
|
||||
"player_search_index_result": player_search_index_result,
|
||||
"player_period_stats_result": player_period_stats_result,
|
||||
"ranking_snapshot_result": ranking_snapshot_result,
|
||||
@@ -279,6 +280,34 @@ def generate_historical_snapshots(
|
||||
}
|
||||
|
||||
|
||||
def refresh_periodic_historical_snapshots(
|
||||
*,
|
||||
server_slug: str | None = None,
|
||||
run_number: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""Refresh legacy historical snapshots without aborting operational read-model refreshes."""
|
||||
try:
|
||||
return generate_historical_snapshots(
|
||||
server_slug=server_slug,
|
||||
run_number=run_number,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - legacy failures must remain visible but isolated
|
||||
failure_result = {
|
||||
"status": "error",
|
||||
"error_type": type(exc).__name__,
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc(),
|
||||
"run_number": run_number,
|
||||
"refresh_interval_seconds": get_historical_refresh_interval_seconds(),
|
||||
"server_slug": server_slug,
|
||||
"generation_policy": "periodic-historical-refresh-cycle",
|
||||
"snapshot_scope": _describe_snapshot_scope(server_slug),
|
||||
"legacy_block": "historical-snapshots",
|
||||
}
|
||||
_emit_json_log({"event": "historical-snapshot-refresh-failed", **failure_result})
|
||||
return failure_result
|
||||
|
||||
|
||||
def refresh_periodic_ranking_snapshots(
|
||||
*,
|
||||
server_slug: str | None = None,
|
||||
|
||||
@@ -121,6 +121,107 @@ class HistoricalSnapshotRefreshTests(unittest.TestCase):
|
||||
self.assertIn('"status": "ok"', stream.getvalue())
|
||||
self.assertIn('"captured_at": "2026-05-22 00:00:00+00:00"', stream.getvalue())
|
||||
|
||||
def test_runner_continues_when_legacy_snapshot_refresh_fails(self) -> None:
|
||||
with (
|
||||
patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
|
||||
patch("app.historical_runner._run_primary_rcon_capture", return_value={"status": "ok", "targets": []}),
|
||||
patch(
|
||||
"app.historical_runner._resolve_classic_fallback_policy",
|
||||
return_value=(False, "validation-rcon-primary-cycle"),
|
||||
),
|
||||
patch("app.historical_runner._rcon_capture_has_new_useful_data", return_value=True),
|
||||
patch(
|
||||
"app.historical_runner.generate_historical_snapshots",
|
||||
side_effect=RuntimeError("legacy snapshot failure"),
|
||||
),
|
||||
patch(
|
||||
"app.historical_runner._build_elo_mmr_rebuild_policy",
|
||||
return_value={
|
||||
"due": False,
|
||||
"policy": "validation-policy",
|
||||
"last_generated_at": None,
|
||||
"samples_since_last_rebuild": 1,
|
||||
"minutes_since_last_rebuild": None,
|
||||
"rebuild_interval_minutes": 60,
|
||||
"min_new_samples": 10,
|
||||
},
|
||||
),
|
||||
patch("app.historical_runner.refresh_player_search_index", return_value={"status": "ok"}) as search_refresh,
|
||||
patch("app.historical_runner.refresh_player_period_stats", return_value={"status": "ok"}) as period_refresh,
|
||||
patch("app.historical_runner.refresh_ranking_snapshots", return_value={"status": "ok"}) as ranking_refresh,
|
||||
patch(
|
||||
"app.historical_runner._maybe_run_database_maintenance",
|
||||
return_value={"status": "skipped", "reason": "disabled"},
|
||||
),
|
||||
):
|
||||
result = _run_refresh_with_retries(
|
||||
max_retries=0,
|
||||
retry_delay_seconds=0,
|
||||
server_slug=None,
|
||||
max_pages=None,
|
||||
page_size=None,
|
||||
run_number=1,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "partial")
|
||||
self.assertEqual(result["historical_snapshot_result"]["status"], "error")
|
||||
self.assertEqual(result["snapshot_result"]["status"], "error")
|
||||
self.assertEqual(
|
||||
result["historical_snapshot_result"]["error"],
|
||||
"legacy snapshot failure",
|
||||
)
|
||||
search_refresh.assert_called_once()
|
||||
period_refresh.assert_called_once()
|
||||
ranking_refresh.assert_called_once()
|
||||
|
||||
def test_runner_returns_ok_when_legacy_snapshot_and_read_models_succeed(self) -> None:
|
||||
with (
|
||||
patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
|
||||
patch("app.historical_runner._run_primary_rcon_capture", return_value={"status": "ok", "targets": []}),
|
||||
patch(
|
||||
"app.historical_runner._resolve_classic_fallback_policy",
|
||||
return_value=(False, "validation-rcon-primary-cycle"),
|
||||
),
|
||||
patch("app.historical_runner._rcon_capture_has_new_useful_data", return_value=True),
|
||||
patch(
|
||||
"app.historical_runner.generate_historical_snapshots",
|
||||
return_value={"status": "ok", "generated_at": "2026-06-09T08:00:00Z"},
|
||||
),
|
||||
patch(
|
||||
"app.historical_runner._build_elo_mmr_rebuild_policy",
|
||||
return_value={
|
||||
"due": False,
|
||||
"policy": "validation-policy",
|
||||
"last_generated_at": None,
|
||||
"samples_since_last_rebuild": 1,
|
||||
"minutes_since_last_rebuild": None,
|
||||
"rebuild_interval_minutes": 60,
|
||||
"min_new_samples": 10,
|
||||
},
|
||||
),
|
||||
patch("app.historical_runner.refresh_player_search_index", return_value={"status": "ok"}),
|
||||
patch("app.historical_runner.refresh_player_period_stats", return_value={"status": "ok"}),
|
||||
patch("app.historical_runner.refresh_ranking_snapshots", return_value={"status": "ok"}),
|
||||
patch(
|
||||
"app.historical_runner._maybe_run_database_maintenance",
|
||||
return_value={"status": "skipped", "reason": "disabled"},
|
||||
),
|
||||
):
|
||||
result = _run_refresh_with_retries(
|
||||
max_retries=0,
|
||||
retry_delay_seconds=0,
|
||||
server_slug=None,
|
||||
max_pages=None,
|
||||
page_size=None,
|
||||
run_number=1,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "ok")
|
||||
self.assertEqual(result["historical_snapshot_result"]["status"], "ok")
|
||||
self.assertIn("player_search_index_result", result)
|
||||
self.assertIn("player_period_stats_result", result)
|
||||
self.assertIn("ranking_snapshot_result", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user