Fix live AdminLog worker startup with safe historical guard
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user