Split live AdminLog ingestion from historical materialization
This commit is contained in:
@@ -760,6 +760,7 @@ class CurrentMatchPublicEndpointHardeningTests(unittest.TestCase):
|
||||
"app.postgres_rcon_storage.connect_postgres_compat",
|
||||
return_value=connection_scope,
|
||||
) as connect_postgres,
|
||||
patch("app.rcon_admin_log_materialization.materialize_rcon_admin_log") as materialize,
|
||||
):
|
||||
result = rcon_admin_log_storage.list_current_match_kill_feed(
|
||||
server_key="comunidad-hispana-01",
|
||||
@@ -767,6 +768,7 @@ class CurrentMatchPublicEndpointHardeningTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
connect_postgres.assert_called_once_with(initialize=False)
|
||||
materialize.assert_not_called()
|
||||
self.assertEqual(result["items"], [])
|
||||
|
||||
def test_player_stats_postgres_read_only_does_not_initialize_storage(self) -> None:
|
||||
@@ -783,6 +785,7 @@ class CurrentMatchPublicEndpointHardeningTests(unittest.TestCase):
|
||||
"app.postgres_rcon_storage.connect_postgres_compat",
|
||||
return_value=connection_scope,
|
||||
) as connect_postgres,
|
||||
patch("app.rcon_admin_log_materialization.materialize_rcon_admin_log") as materialize,
|
||||
):
|
||||
result = rcon_admin_log_storage.list_current_match_player_stats(
|
||||
server_key="comunidad-hispana-01",
|
||||
@@ -790,6 +793,7 @@ class CurrentMatchPublicEndpointHardeningTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
connect_postgres.assert_called_once_with(initialize=False)
|
||||
materialize.assert_not_called()
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertEqual(result["source"], "rcon-admin-log-current-match-summary")
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.config import (
|
||||
)
|
||||
from app.rcon_current_match_worker import (
|
||||
list_current_match_trusted_targets,
|
||||
run_current_match_adminlog_refresh_loop,
|
||||
run_current_match_adminlog_refresh_once_unlocked,
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ class RconCurrentMatchWorkerTests(unittest.TestCase):
|
||||
fetch_calls.append((target.external_server_id, lookback_seconds, timeout_seconds))
|
||||
return [{"timestamp": "2026-06-18T18:00:00Z", "message": "[1 (1)] Killer(Allies) -> Victim(Axis) with Rifle"}]
|
||||
|
||||
def fake_persist(*, target, entries, db_path=None):
|
||||
def fake_persist(*, target, entries, db_path=None, ensure_storage=True):
|
||||
persist_calls.append({"target": target, "entries": entries, "db_path": db_path})
|
||||
return {
|
||||
"events_seen": len(entries),
|
||||
@@ -94,16 +95,50 @@ class RconCurrentMatchWorkerTests(unittest.TestCase):
|
||||
self.assertEqual(fetch_calls, [("comunidad-hispana-01", 180, None)])
|
||||
self.assertEqual(len(persist_calls), 1)
|
||||
self.assertEqual(persist_calls[0]["target"]["target_key"], "comunidad-hispana-01")
|
||||
self.assertEqual(persist_calls[0]["db_path"], None)
|
||||
self.assertEqual(result["totals"]["events_inserted"], 1)
|
||||
self.assertEqual(result["status"], "ok")
|
||||
|
||||
def test_once_unlocked_initializes_storage_once_and_persists_without_repeated_ddl(self) -> None:
|
||||
persist_calls: list[dict[str, object]] = []
|
||||
|
||||
def fake_fetch(target, *, lookback_seconds, timeout_seconds):
|
||||
return [{"timestamp": "2026-06-18T18:00:00Z", "message": "[1 (1)] Killer(Allies) -> Victim(Axis) with Rifle"}]
|
||||
|
||||
def fake_persist(*, target, entries, db_path=None, ensure_storage=True):
|
||||
persist_calls.append(
|
||||
{
|
||||
"target": target,
|
||||
"entries": entries,
|
||||
"db_path": db_path,
|
||||
"ensure_storage": ensure_storage,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"events_seen": len(entries),
|
||||
"events_inserted": len(entries),
|
||||
"duplicate_events": 0,
|
||||
}
|
||||
|
||||
with patch("app.rcon_current_match_worker.initialize_rcon_admin_log_storage") as initialize:
|
||||
result = run_current_match_adminlog_refresh_once_unlocked(
|
||||
lookback_seconds=180,
|
||||
targets=[TARGET_01],
|
||||
fetch_entries_fn=fake_fetch,
|
||||
persist_entries_fn=fake_persist,
|
||||
)
|
||||
|
||||
initialize.assert_called_once_with(db_path=None)
|
||||
self.assertEqual(persist_calls[0]["ensure_storage"], False)
|
||||
self.assertEqual(result["status"], "ok")
|
||||
|
||||
def test_failing_target_does_not_block_other_target(self) -> None:
|
||||
def fake_fetch(target, *, lookback_seconds, timeout_seconds):
|
||||
if target.external_server_id == "comunidad-hispana-01":
|
||||
raise RuntimeError("boom")
|
||||
return [{"timestamp": "2026-06-18T18:00:00Z", "message": "[1 (1)] Killer(Allies) -> Victim(Axis) with Rifle"}]
|
||||
|
||||
def fake_persist(*, target, entries, db_path=None):
|
||||
def fake_persist(*, target, entries, db_path=None, ensure_storage=True):
|
||||
return {
|
||||
"events_seen": len(entries),
|
||||
"events_inserted": len(entries),
|
||||
@@ -155,6 +190,44 @@ class RconCurrentMatchWorkerTests(unittest.TestCase):
|
||||
self.assertEqual(second["totals"]["events_inserted"], 0)
|
||||
self.assertEqual(second["totals"]["duplicate_events"], 1)
|
||||
|
||||
def test_loop_honors_max_runs(self) -> None:
|
||||
results = [{"status": "ok"}, {"status": "ok"}]
|
||||
|
||||
with (
|
||||
patch("app.rcon_current_match_worker.initialize_rcon_admin_log_storage") as initialize,
|
||||
patch(
|
||||
"app.rcon_current_match_worker.run_current_match_adminlog_refresh_once",
|
||||
side_effect=results,
|
||||
) as run_once,
|
||||
patch("app.rcon_current_match_worker.time.sleep") as sleep,
|
||||
):
|
||||
run_current_match_adminlog_refresh_loop(
|
||||
interval_seconds=5,
|
||||
lookback_seconds=900,
|
||||
max_runs=2,
|
||||
)
|
||||
|
||||
initialize.assert_called_once_with()
|
||||
self.assertEqual(run_once.call_count, 2)
|
||||
self.assertEqual(sleep.call_count, 1)
|
||||
|
||||
def test_compose_nas_runs_split_live_worker_and_safe_historical_interval(self) -> None:
|
||||
compose_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"..",
|
||||
"deploy",
|
||||
"portainer",
|
||||
"docker-compose.nas.yml",
|
||||
)
|
||||
with open(compose_path, encoding="utf-8") as handle:
|
||||
compose_text = handle.read()
|
||||
|
||||
self.assertIn("rcon-live-adminlog-worker:", compose_text)
|
||||
self.assertIn("app.rcon_current_match_worker", compose_text)
|
||||
self.assertIn('--lookback-minutes\n - "15"', compose_text)
|
||||
self.assertIn('HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS: ${HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS:-900}', compose_text)
|
||||
self.assertNotIn('HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS: ${HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS:-2}', compose_text)
|
||||
|
||||
def test_current_match_adminlog_config_defaults_and_overrides(self) -> None:
|
||||
with _temporary_env(
|
||||
CURRENT_MATCH_ADMINLOG_INTERVAL_SECONDS=None,
|
||||
|
||||
@@ -155,6 +155,21 @@ class RconHistoricalWorkerTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(seen["timeout_seconds"], 3.5)
|
||||
|
||||
def test_historical_capture_skips_when_previous_heavy_run_is_still_running(self) -> None:
|
||||
with (
|
||||
patch("app.rcon_historical_worker.initialize_rcon_historical_storage"),
|
||||
patch("app.rcon_historical_worker._select_targets", return_value=[TARGET]),
|
||||
patch(
|
||||
"app.rcon_historical_worker.start_rcon_historical_capture_run",
|
||||
side_effect=RuntimeError("historical materialization capture already running"),
|
||||
),
|
||||
):
|
||||
payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_HISTORICAL)
|
||||
|
||||
self.assertEqual(payload["status"], "skipped")
|
||||
self.assertEqual(payload["run_status"], "skipped")
|
||||
self.assertEqual(payload["materialization_result"]["reason"], "already-running")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _temporary_env(**values: str):
|
||||
|
||||
Reference in New Issue
Block a user