feat(celery): schedule weekly updates and run startup refresh
Some checks failed
CI/CD Pipeline / Run Tests (push) Successful in 1m41s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 1m31s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been cancelled
CI/CD Pipeline / Run Tests (pull_request) Has been cancelled

This commit is contained in:
2026-03-20 11:14:03 +01:00
parent c8bdc282de
commit b8e3f0a5d3
4 changed files with 134 additions and 8 deletions

View File

@@ -4,11 +4,17 @@ Celery configuration for the project.
This module contains Celery configuration and task registration.
"""
import logging
import os
import sys
from apps.core.startup_checks import run_startup_checks
from celery import Celery
from celery.signals import worker_ready
from django.conf import settings
from django.core.cache import cache
logger = logging.getLogger(__name__)
# Set the Django settings module for the 'celery' program.
if "DJANGO_SETTINGS_MODULE" not in os.environ:
@@ -27,6 +33,12 @@ def _is_celery_runtime() -> bool:
)
def _is_worker_runtime() -> bool:
"""True when current process is a Celery worker command."""
argv = " ".join(sys.argv).lower()
return "celery" in argv and " worker" in argv
if _is_celery_runtime():
run_startup_checks(component="celery")
@@ -43,20 +55,24 @@ app.autodiscover_tasks()
# Configure Celery Beat schedule
app.conf.beat_schedule = {
# Парсинг сертификатов промышленного производства - каждый день в 3:00
# Обновления источников по умолчанию — раз в неделю.
# Сохраняем legacy-названия ключей, чтобы корректно обновлять существующие
# PeriodicTask в django-celery-beat через update_or_create по name.
"parse-industrial-production-daily": {
"task": "apps.parsers.tasks.parse_industrial_production",
"schedule": 86400.0, # Every 24 hours
"schedule": 7 * 24 * 60 * 60, # Every 7 days
},
# Парсинг реестра производителей - каждый день в 4:00
"parse-manufactures-daily": {
"task": "apps.parsers.tasks.parse_manufactures",
"schedule": 86400.0, # Every 24 hours
"schedule": 7 * 24 * 60 * 60, # Every 7 days
},
# Парсинг реестра промышленной продукции - каждый день в 5:00
"parse-industrial-products-daily": {
"task": "apps.parsers.tasks.parse_industrial_products",
"schedule": 86400.0, # Every 24 hours
"schedule": 7 * 24 * 60 * 60, # Every 7 days
},
"parse-inspections-weekly": {
"task": "apps.parsers.tasks.parse_inspections",
"schedule": 7 * 24 * 60 * 60, # Every 7 days
},
# Сканирование папки FNS - каждые 5 минут
"scan-fns-directory": {
@@ -68,6 +84,54 @@ app.conf.beat_schedule = {
app.conf.timezone = "Europe/Moscow"
def _queue_startup_sources_refresh() -> None:
"""
Запустить parse_all_sources один раз при старте worker.
Используется distributed lock через cache, чтобы несколько worker/pod
не запускали автообновление одновременно.
"""
if not getattr(settings, "CELERY_STARTUP_REFRESH_ENABLED", True):
logger.info("Startup refresh is disabled by settings")
return
lock_key = getattr(
settings,
"CELERY_STARTUP_REFRESH_LOCK_KEY",
"celery:startup:parse_all_sources:lock",
)
lock_ttl = int(getattr(settings, "CELERY_STARTUP_REFRESH_LOCK_TTL_SECONDS", 3600))
acquired = cache.add(lock_key, "1", timeout=lock_ttl)
if not acquired:
logger.info(
"Startup refresh skipped: lock already acquired (%s)",
lock_key,
)
return
countdown = int(getattr(settings, "CELERY_STARTUP_REFRESH_DELAY_SECONDS", 30))
from apps.parsers.tasks import parse_all_sources
async_result = parse_all_sources.apply_async(countdown=max(countdown, 0))
logger.info(
"Startup refresh queued: task=%s task_id=%s countdown=%ss",
"apps.parsers.tasks.parse_all_sources",
async_result.id,
countdown,
)
@worker_ready.connect(weak=False)
def _on_worker_ready(sender=None, **kwargs) -> None: # noqa: ARG001
"""Queue source refresh when worker starts."""
if not _is_worker_runtime():
return
try:
_queue_startup_sources_refresh()
except Exception:
logger.exception("Failed to queue startup source refresh")
@app.task(bind=True)
def debug_task(self):
print(f"Request: {self.request!r}")