"""Celery Beat-triggered task. Runs every minute (see celery_app.py's beat_schedule) and dynamically discovers which companies are due for a check by querying MonitorConfiguration/Source directly - adding a new company never requires touching this task or any static schedule config. A company is due either because its own default MonitorConfiguration.next_run has arrived, or because at least one of its sources has its own faster frequency override that's independently due (see SourceRepository.list_due_for_company) - e.g. a "check news daily" source inside a company whose overall default cadence is weekly. Either way, run_monitoring only actually collects the sources that are themselves due for a SCHEDULED trigger - see tasks/collection.py. """ from __future__ import annotations from datetime import UTC, datetime from structlog.contextvars import bound_contextvars from app.core.logging import get_logger from app.db.session import get_sessionmaker from app.models.enums import MonitoringRunTrigger from app.repositories.company_repository import MonitorConfigurationRepository from app.repositories.monitoring_run_repository import MonitoringRunRepository from app.repositories.source_repository import SourceRepository from app.tasks.base import run_async_task from app.tasks.celery_app import celery_app logger = get_logger(__name__) @celery_app.task(bind=True, name="app.tasks.scheduler.sync_schedules") def sync_schedules(self) -> None: with bound_contextvars(task_id=self.request.id): run_async_task(_sync_schedules_async()) async def _sync_schedules_async() -> None: from app.tasks.collection import ( run_monitoring, ) # local import: avoids a circular import at module load session_factory = get_sessionmaker() async with session_factory() as db: monitor_repo = MonitorConfigurationRepository(db) source_repo = SourceRepository(db) run_repo = MonitoringRunRepository(db) now = datetime.now(UTC) enabled_configs = await monitor_repo.list_enabled() enqueued = 0 for config in enabled_configs: is_due = await source_repo.company_has_due_work(config.company_id, now, config.next_run) if not is_due: continue # Idempotency: don't double-enqueue a company that's still # mid-run from a previous tick or a manual "run now". active_run = await run_repo.get_active_for_company(config.company_id) if active_run is not None: continue run = await run_repo.create( company_id=config.company_id, trigger_type=MonitoringRunTrigger.SCHEDULED ) await db.commit() run_monitoring.delay(str(run.id)) enqueued += 1 if enqueued: logger.info("schedule_sync_enqueued_runs", count=enqueued)