Fix live AdminLog worker startup with safe historical guard
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from contextlib import closing, contextmanager
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from app import postgres_rcon_storage
|
||||
from app import rcon_admin_log_storage
|
||||
from app.config import (
|
||||
get_current_match_adminlog_enabled,
|
||||
get_current_match_adminlog_interval_seconds,
|
||||
@@ -190,6 +194,101 @@ class RconCurrentMatchWorkerTests(unittest.TestCase):
|
||||
self.assertEqual(second["totals"]["events_inserted"], 0)
|
||||
self.assertEqual(second["totals"]["duplicate_events"], 1)
|
||||
|
||||
def test_live_worker_starts_when_duplicate_historical_runs_exist(self) -> None:
|
||||
entry = {
|
||||
"timestamp": "2026-06-18T18:00:00Z",
|
||||
"message": (
|
||||
"[5:00 min (321)] KILL: Alpha(Allies/steam-alpha) -> "
|
||||
"Victim(Axis/steam-victim) with Rifle"
|
||||
),
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "current_match.sqlite3")
|
||||
rcon_admin_log_storage.initialize_rcon_admin_log_storage(db_path=Path(db_path))
|
||||
with closing(sqlite3.connect(db_path)) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO rcon_historical_capture_runs (
|
||||
mode, status, target_scope, started_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
("historical", "running", "all-configured-rcon-targets", "2099-01-01T00:00:00Z"),
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO rcon_historical_capture_runs (
|
||||
mode, status, target_scope, started_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
("historical", "running", "all-configured-rcon-targets", "2099-01-01T00:05:00Z"),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
result = run_current_match_adminlog_refresh_once_unlocked(
|
||||
lookback_seconds=180,
|
||||
targets=[TARGET_01],
|
||||
fetch_entries_fn=lambda *_args, **_kwargs: [entry],
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "ok")
|
||||
self.assertEqual(result["totals"]["events_inserted"], 1)
|
||||
self.assertEqual(result["totals"]["failed_targets"], 0)
|
||||
|
||||
def test_live_worker_does_not_trigger_admin_log_materialization(self) -> None:
|
||||
def fake_fetch(target, *, lookback_seconds, timeout_seconds):
|
||||
return [
|
||||
{
|
||||
"timestamp": "2026-06-18T18:00:00Z",
|
||||
"message": "[1:00 min (60)] CONNECTED Alpha (steam-alpha)",
|
||||
}
|
||||
]
|
||||
|
||||
with (
|
||||
tempfile.TemporaryDirectory() as temp_dir,
|
||||
patch("app.rcon_admin_log_materialization.materialize_rcon_admin_log") as materialize,
|
||||
):
|
||||
db_path = os.path.join(temp_dir, "current_match.sqlite3")
|
||||
result = run_current_match_adminlog_refresh_once_unlocked(
|
||||
lookback_seconds=180,
|
||||
targets=[TARGET_01],
|
||||
fetch_entries_fn=fake_fetch,
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
materialize.assert_not_called()
|
||||
self.assertEqual(result["status"], "ok")
|
||||
|
||||
def test_live_worker_does_not_use_historical_runtime_guard(self) -> None:
|
||||
def fake_fetch(target, *, lookback_seconds, timeout_seconds):
|
||||
return [
|
||||
{
|
||||
"timestamp": "2026-06-18T18:00:00Z",
|
||||
"message": "[1:00 min (60)] CONNECTED Alpha (steam-alpha)",
|
||||
}
|
||||
]
|
||||
|
||||
def fake_persist(*, target, entries, db_path=None, ensure_storage=True):
|
||||
return {
|
||||
"events_seen": len(entries),
|
||||
"events_inserted": len(entries),
|
||||
"duplicate_events": 0,
|
||||
}
|
||||
|
||||
with (
|
||||
patch("app.rcon_current_match_worker.initialize_rcon_admin_log_storage"),
|
||||
patch("app.rcon_historical_storage.historical_capture_runtime_guard") as guard,
|
||||
):
|
||||
result = run_current_match_adminlog_refresh_once_unlocked(
|
||||
lookback_seconds=180,
|
||||
targets=[TARGET_01],
|
||||
fetch_entries_fn=fake_fetch,
|
||||
persist_entries_fn=fake_persist,
|
||||
)
|
||||
|
||||
guard.assert_not_called()
|
||||
self.assertEqual(result["status"], "ok")
|
||||
|
||||
def test_loop_honors_max_runs(self) -> None:
|
||||
results = [{"status": "ok"}, {"status": "ok"}]
|
||||
|
||||
@@ -211,6 +310,51 @@ class RconCurrentMatchWorkerTests(unittest.TestCase):
|
||||
self.assertEqual(run_once.call_count, 2)
|
||||
self.assertEqual(sleep.call_count, 1)
|
||||
|
||||
def test_postgres_admin_log_storage_uses_admin_log_bootstrap_only(self) -> None:
|
||||
with (
|
||||
patch.object(rcon_admin_log_storage, "use_postgres_rcon_storage", return_value=True),
|
||||
patch("app.postgres_rcon_storage.initialize_postgres_admin_log_storage") as admin_log_init,
|
||||
patch("app.postgres_rcon_storage.initialize_postgres_rcon_storage") as full_init,
|
||||
):
|
||||
rcon_admin_log_storage.initialize_rcon_admin_log_storage()
|
||||
|
||||
admin_log_init.assert_called_once_with()
|
||||
full_init.assert_not_called()
|
||||
|
||||
def test_postgres_schema_strings_no_longer_create_historical_running_unique_index(self) -> None:
|
||||
self.assertNotIn(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_rcon_historical_single_running_historical",
|
||||
postgres_rcon_storage.RCON_SCHEMA_SQL,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"idx_rcon_historical_single_running_historical",
|
||||
postgres_rcon_storage.POSTGRES_ADMIN_LOG_SCHEMA_SQL,
|
||||
)
|
||||
self.assertIn(
|
||||
"DROP INDEX IF EXISTS idx_rcon_historical_single_running_historical",
|
||||
postgres_rcon_storage.DROP_LEGACY_HISTORICAL_GUARD_INDEX_SQL,
|
||||
)
|
||||
|
||||
def test_initialize_postgres_rcon_storage_does_not_execute_removed_unique_index(self) -> None:
|
||||
executed_sql: list[str] = []
|
||||
|
||||
with patch.object(
|
||||
postgres_rcon_storage,
|
||||
"connect_postgres",
|
||||
return_value=_FakePostgresConnectionScope(executed_sql),
|
||||
):
|
||||
postgres_rcon_storage.initialize_postgres_rcon_storage()
|
||||
|
||||
executed_text = "\n".join(executed_sql)
|
||||
self.assertIn(
|
||||
"DROP INDEX IF EXISTS idx_rcon_historical_single_running_historical",
|
||||
executed_text,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_rcon_historical_single_running_historical",
|
||||
executed_text,
|
||||
)
|
||||
|
||||
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__)),
|
||||
@@ -264,3 +408,31 @@ def _temporary_env(**values: str | None):
|
||||
os.environ.pop(name, None)
|
||||
else:
|
||||
os.environ[name] = value
|
||||
|
||||
|
||||
class _FakePostgresConnectionScope:
|
||||
def __init__(self, executed_sql: list[str]) -> None:
|
||||
self.connection = _FakePostgresConnection(executed_sql)
|
||||
|
||||
def __enter__(self):
|
||||
return self.connection
|
||||
|
||||
def __exit__(self, exc_type, exc, traceback) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _FakePostgresConnection:
|
||||
def __init__(self, executed_sql: list[str]) -> None:
|
||||
self.executed_sql = executed_sql
|
||||
|
||||
@contextmanager
|
||||
def cursor(self):
|
||||
yield _FakePostgresCursor(self.executed_sql)
|
||||
|
||||
|
||||
class _FakePostgresCursor:
|
||||
def __init__(self, executed_sql: list[str]) -> None:
|
||||
self.executed_sql = executed_sql
|
||||
|
||||
def execute(self, sql: str) -> None:
|
||||
self.executed_sql.append(sql)
|
||||
|
||||
Reference in New Issue
Block a user