"""Celery application configuration.""" import logging from celery import Celery from celery.schedules import crontab from app.config import get_settings logger = logging.getLogger(__name__) settings = get_settings() def _get_parallel_limit_from_db(): """Try to get parallel_limit from database, fall back to environment. This is called once at Celery startup. Changes require worker restart. """ try: import asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker async def _fetch(): engine = create_async_engine(settings.async_database_url, echo=False) session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) try: async with session_factory() as session: from app.models.setting import Setting, DEFAULT_SETTINGS result = await session.execute( select(Setting).where(Setting.key == "download.parallel_limit") ) setting = result.scalar() if setting: return setting.value return DEFAULT_SETTINGS.get("download.parallel_limit", settings.download_parallel_limit) finally: await engine.dispose() return asyncio.run(_fetch()) except Exception as e: logger.warning(f"Could not read parallel_limit from database, using environment: {e}") return settings.download_parallel_limit # Get parallel limit (database takes precedence over environment) parallel_limit = _get_parallel_limit_from_db() logger.info(f"Celery worker concurrency set to: {parallel_limit}") celery_app = Celery( "gallery_subscriber", broker=settings.redis_url, backend=settings.redis_url, include=["app.tasks.downloads", "app.tasks.maintenance"], ) # Celery configuration celery_app.conf.update( task_serializer="json", accept_content=["json"], result_serializer="json", timezone="UTC", enable_utc=True, task_track_started=True, task_time_limit=3600, # 1 hour max per task worker_prefetch_multiplier=1, # Process one task at a time worker_concurrency=parallel_limit, ) # Celery Beat schedule for periodic tasks # Note: The scheduler runs frequently (every 60s) but only triggers downloads # for sources whose individual check_interval has elapsed. Per-source intervals # are stored in the database and can be configured via the UI. celery_app.conf.beat_schedule = { "check-sources-scheduler": { "task": "app.tasks.downloads.scheduled_check", "schedule": 60, # Run every 60 seconds to check for due sources }, "cleanup-old-downloads-daily": { "task": "app.tasks.downloads.cleanup_old_downloads", "schedule": crontab(hour=3, minute=0), # Daily at 3 AM }, "update-storage-stats": { "task": "tasks.update_storage_stats", "schedule": crontab(minute="*/30"), # Every 30 minutes }, "reset-orphaned-jobs": { "task": "tasks.reset_orphaned_jobs", "schedule": crontab(minute="*/15"), # Every 15 minutes }, } # Worker startup signal - reset any orphaned running jobs @celery_app.on_after_configure.connect def setup_worker_startup(sender, **kwargs): """Called after Celery is configured but before workers start processing.""" pass # Placeholder - actual reset happens in worker_ready signal from celery.signals import worker_ready @worker_ready.connect def on_worker_ready(sender, **kwargs): """Called when a worker is ready to accept tasks. Resets any jobs stuck in 'running' status from previous runs. This handles the case where workers crashed or were restarted. """ try: from app.tasks.maintenance import reset_all_running_jobs_sync result = reset_all_running_jobs_sync() logger.info(f"Worker startup: {result}") except Exception as e: logger.error(f"Failed to reset orphaned jobs on worker startup: {e}")