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,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