"""Standalone entrypoint для tradein-scraper контейнера (#1182). Зачем: API-деплой (docker restart tradein-backend) не должен прерывать бегущие scraper sweep'ы (avito/cian/rosreestr). Вынесен в отдельный контейнер tradein-scraper с тем же backend-образом, но другой командой. Запуск: python -m app.scheduler_main Zombie-reap: reap_zombies() вызывается в начале каждого тика scheduler_loop() (см. app/services/scheduler.py), дублировать здесь не нужно. """ from __future__ import annotations import asyncio import logging import os import signal import sys from contextlib import suppress from app.core.config import settings logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger(__name__) # Мониторинг ошибок — GlitchTip (Sentry-совместимый, #396). # Только integrations без Starlette/FastAPI (у нас нет ASGI-приложения здесь). # SqlalchemyIntegration + HttpxIntegration + LoggingIntegration покрывают scraper-слой. if settings.glitchtip_dsn: import sentry_sdk from sentry_sdk.integrations.httpx import HttpxIntegration from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from app.observability.sentry_scrub import scrub_pii_event sentry_sdk.init( dsn=settings.glitchtip_dsn, environment=settings.environment, release=os.getenv("GIT_SHA") or os.getenv("SENTRY_RELEASE") or "unknown", traces_sample_rate=0.0, send_default_pii=False, before_send=scrub_pii_event, integrations=[ SqlalchemyIntegration(), HttpxIntegration(), LoggingIntegration(level=logging.INFO, event_level=logging.ERROR), ], ) logger.info("GlitchTip monitoring enabled (scheduler_main)") def _should_run() -> bool: """Проверить kill-switch SCHEDULER_ENABLE перед запуском asyncio loop.""" return settings.scheduler_enable async def _run() -> None: """Async entrypoint: запустить scheduler_loop() с чистой отменой по SIGTERM/SIGINT.""" from app.services.scheduler import scheduler_loop task = asyncio.create_task(scheduler_loop()) loop = asyncio.get_running_loop() def _cancel(_signum: int, _frame: object = None) -> None: logger.info("scheduler_main: signal received — cancelling scheduler task") task.cancel() try: loop.add_signal_handler(signal.SIGTERM, lambda: _cancel(signal.SIGTERM)) loop.add_signal_handler(signal.SIGINT, lambda: _cancel(signal.SIGINT)) except NotImplementedError: # Windows dev: signal handlers через loop не поддерживаются logger.warning("scheduler_main: loop.add_signal_handler not supported (Windows dev)") with suppress(asyncio.CancelledError): await task logger.info("scheduler_main: scheduler cancelled cleanly") if __name__ == "__main__": if not _should_run(): logger.warning("scheduler_main: scheduler disabled via SCHEDULER_ENABLE — exiting") sys.exit(0) # Best-effort FDW bootstrap: чтобы ensure_fdw_user_mapping отработал при старте # scraper-контейнера (postgres_fdw нужен для rosreestr_dkp_import). try: from app.core.db import SessionLocal from app.core.fdw import ensure_fdw_user_mapping with SessionLocal() as db: ensure_fdw_user_mapping(db) except Exception: logger.exception( "scheduler_main: FDW user mapping bootstrap failed — cadastral queries may fail" ) asyncio.run(_run())