Fix live AdminLog worker startup with safe historical guard

This commit is contained in:
devRaGonSa
2026-06-22 15:16:19 +02:00
parent 43ca4696e0
commit e18177ff63
8 changed files with 651 additions and 193 deletions

View File

@@ -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)

View File

@@ -1,11 +1,15 @@
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 rcon_historical_storage
from app.rcon_historical_worker import (
CAPTURE_MODE_CURRENT_LIVE,
CAPTURE_MODE_HISTORICAL,
@@ -157,19 +161,98 @@ class RconHistoricalWorkerTests(unittest.TestCase):
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"),
"app.rcon_historical_worker.historical_capture_runtime_guard",
side_effect=_yield_guard(False),
),
patch("app.rcon_historical_worker.start_rcon_historical_capture_run") as start_run,
):
payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_HISTORICAL)
start_run.assert_not_called()
self.assertEqual(payload["status"], "skipped")
self.assertEqual(payload["run_status"], "skipped")
self.assertEqual(payload["materialization_result"]["reason"], "already-running")
def test_historical_storage_can_start_with_duplicate_stale_running_rows(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
db_path = Path(temp_dir) / "historical.sqlite3"
rcon_historical_storage.initialize_rcon_historical_storage(db_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", "2026-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", "2026-01-01T00:05:00Z"),
)
connection.commit()
run_id = rcon_historical_storage.start_rcon_historical_capture_run(
mode="historical",
target_scope="all-configured-rcon-targets",
db_path=db_path,
)
self.assertGreater(run_id, 0)
with closing(sqlite3.connect(db_path)) as connection:
rows = connection.execute(
"""
SELECT status
FROM rcon_historical_capture_runs
WHERE mode = 'historical'
ORDER BY id ASC
"""
).fetchall()
self.assertEqual([row[0] for row in rows[:2]], ["stale", "stale"])
self.assertEqual(rows[-1][0], "running")
def test_historical_runtime_guard_releases_non_stale_conflict_only(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
db_path = Path(temp_dir) / "historical.sqlite3"
rcon_historical_storage.initialize_rcon_historical_storage(db_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.commit()
with rcon_historical_storage.historical_capture_runtime_guard(
capture_mode="historical",
db_path=db_path,
) as acquired:
self.assertIs(acquired, False)
def test_postgres_historical_runtime_guard_uses_advisory_lock(self) -> None:
with (
patch.object(rcon_historical_storage, "use_postgres_rcon_storage", return_value=True),
patch(
"app.postgres_rcon_storage.postgres_historical_capture_advisory_guard",
side_effect=_yield_guard(False),
) as advisory_guard,
):
with rcon_historical_storage.historical_capture_runtime_guard(
capture_mode="historical",
) as acquired:
self.assertIs(acquired, False)
advisory_guard.assert_called_once_with()
@contextmanager
def _temporary_env(**values: str):
@@ -184,3 +267,11 @@ def _temporary_env(**values: str):
os.environ.pop(name, None)
else:
os.environ[name] = value
def _yield_guard(value: bool):
@contextmanager
def _guard(*_args, **_kwargs):
yield value
return _guard