"""하드닝 R2: db_worker 크래시 재시작 백오프/한도, stock_analysis 보존기간.""" import asyncio import time import types import pytest from app import main as m @pytest.fixture(autouse=True) def _clean(monkeypatch): monkeypatch.setattr(m, "_db_worker_restarts", []) monkeypatch.setattr(m, "_db_worker_restart_total", 0) monkeypatch.setattr(m, "_db_worker_restart_limit_hit", False) monkeypatch.setattr(m, "_db_worker_restart_handle", None) monkeypatch.setattr(m, "_shutting_down", False) monkeypatch.setattr(m, "_DB_WORKER_RESTART_BACKOFF_SEC", (0.0,)) calls = [] monkeypatch.setattr(m, "_notify_system_warning_bg", lambda c, msg: calls.append((c, msg))) return calls async def _crashed(): async def boom(): raise RuntimeError("boom") t = asyncio.create_task(boom()) await asyncio.sleep(0) await asyncio.sleep(0) return t def test_backoff_plan_grows_and_limits(monkeypatch): monkeypatch.setattr(m, "_DB_WORKER_RESTART_BACKOFF_SEC", (1.0, 5.0)) assert m._db_worker_restart_plan_v2(1000.0) == (1.0, False) m._db_worker_restarts.append(990.0) assert m._db_worker_restart_plan_v2(1000.0) == (5.0, False) m._db_worker_restarts[:] = [990.0 + i for i in range(m._DB_WORKER_MAX_RESTARTS_PER_HOUR)] wait, hit = m._db_worker_restart_plan_v2(1000.0) assert hit is True and wait >= m._DB_WORKER_LIMIT_COOLDOWN_SEC @pytest.mark.asyncio async def test_restart_is_deferred_not_immediate(_clean): app = types.SimpleNamespace(state=types.SimpleNamespace()) started = [] async def factory(): started.append(1) await asyncio.sleep(10) t = await _crashed() m._on_db_worker_done_v2(app, t, factory) assert started == [] # call_later 로 예약만, 즉시 재시작 아님 assert m._db_worker_restart_handle is not None assert _clean and _clean[0][0] == "DB_WORKER" await asyncio.sleep(0.05) assert started == [1] and m._db_worker_restart_total == 1 app.state.db_worker_task.cancel() @pytest.mark.asyncio async def test_limit_hit_flagged_in_health(): app = types.SimpleNamespace(state=types.SimpleNamespace()) m._db_worker_restarts[:] = [time.monotonic()] * m._DB_WORKER_MAX_RESTARTS_PER_HOUR async def factory(): await asyncio.sleep(10) t = await _crashed() m._on_db_worker_done_v2(app, t, factory) h = m._db_worker_health_v2(app) assert h["restart_limit_hit"] is True m._db_worker_restart_handle.cancel() @pytest.mark.asyncio async def test_no_restart_when_shutting_down(monkeypatch): monkeypatch.setattr(m, "_shutting_down", True) app = types.SimpleNamespace(state=types.SimpleNamespace()) t = await _crashed() m._on_db_worker_done_v2(app, t, lambda: None) assert m._db_worker_restart_handle is None def test_stock_analysis_retention_beyond_a_week(): assert m._STOCK_ANALYSIS_RETENTION_DAYS >= 90