gendesign/tradein-mvp/backend/tests/test_scheduler_main.py
bot-backend 72a4c122c4 feat(tradein): отдельный scraper-контейнер — scheduler вне API-процесса (#1182)
Причина: docker restart tradein-backend (при каждом API-деплое) убивал бегущие
sweep'ы avito/cian/rosreestr — они могут идти часами, и их прерывание середине
батча создавало zombie-runs и пропуски данных.

Что сделано:
- scheduler_main.py — standalone entrypoint для tradein-scraper; SIGTERM/SIGINT
  отменяют scheduler_loop чисто; GlitchTip init без Starlette/FastAPI интеграций.
- docker-compose.prod.yml — новый сервис scraper (тот же image, команда python -m
  app.scheduler_main, только tradein-net); SCHEDULER_ENABLE=false в backend.
- deploy-tradein.yml — селективный up -d --no-deps: scraper пересоздаётся только
  при изменениях scraper/infra путей (SCRAPER_CHANGED), не при каждом backend-деплое.
- tests/test_scheduler_main.py — импорт, kill-switch _should_run(), чистая отмена _run().
2026-06-12 10:50:50 +03:00

76 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты для scheduler_main.py — standalone scraper-контейнер (#1182).
(a) модуль импортируется без реального DB/сети.
(b) _should_run() возвращает False при SCHEDULER_ENABLE=false.
(c) _run() отменяется чисто без unhandled exceptions.
"""
from __future__ import annotations
import asyncio
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import pytest
import app.scheduler_main as sm
# (a) Импорт модуля
def test_scheduler_main_importable() -> None:
"""scheduler_main импортируется без ошибок."""
assert sm is not None
assert hasattr(sm, "_should_run")
assert hasattr(sm, "_run")
# (b) Kill-switch SCHEDULER_ENABLE=false
def test_should_run_false_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
"""_should_run() == False когда SCHEDULER_ENABLE отключён."""
monkeypatch.setattr(sm.settings, "scheduler_enable", False)
assert sm._should_run() is False
def test_should_run_true_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
"""_should_run() == True при включённом scheduler."""
monkeypatch.setattr(sm.settings, "scheduler_enable", True)
assert sm._should_run() is True
# (c) _run() отменяется чисто
@pytest.mark.asyncio
async def test_run_cancels_cleanly(monkeypatch: pytest.MonkeyPatch) -> None:
"""_run() завершается без unhandled exceptions при отмене задачи."""
cancelled = asyncio.Event()
async def _stub_loop() -> None:
"""Короткая заглушка: ждёт отмены, имитируя бесконечный scheduler_loop."""
cancelled.set()
await asyncio.sleep(999)
monkeypatch.setattr("app.scheduler_main.scheduler_loop", _stub_loop, raising=False)
# Патчим импорт внутри _run (он делает `from app.services.scheduler import scheduler_loop`)
import app.services.scheduler as _sched_mod
monkeypatch.setattr(_sched_mod, "scheduler_loop", _stub_loop)
# Также патчим внутри самого модуля если он уже закешировал ссылку
# (на случай если _run делает локальный import каждый раз — оба патча идемпотентны)
task = asyncio.create_task(sm._run())
# Ждём пока _stub_loop запустится (флаг)
await asyncio.wait_for(cancelled.wait(), timeout=2.0)
task.cancel()
# Не должно бросить никакого исключения
try:
await task
except asyncio.CancelledError:
pass # CancelledError от самого task — допустимо
# Тест прошёл если не было unhandled exception
assert task.done()