115 lines
4.0 KiB
Python
115 lines
4.0 KiB
Python
"""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}")
|