diff --git a/backend/app/workers/beat_schedule.py b/backend/app/workers/beat_schedule.py index defdc47c..0252d9b7 100644 --- a/backend/app/workers/beat_schedule.py +++ b/backend/app/workers/beat_schedule.py @@ -506,4 +506,20 @@ def build_beat_schedule() -> dict: "options": {"queue": "celery"}, } + # Refresh mv_layout_velocity (#1666) — питает «лучшие планировки» best_layouts (#113). + # До этого MV рефрешился ТОЛЬКО ручным вызовом refresh_layout_velocity() → в проде + # данные молча устаревали. REFRESH ... CONCURRENTLY (non-blocking) по mv_layout_velocity. + # + # Воскресенье 03:00 МСК (Celery conf.timezone=Europe/Moscow → crontab в МСК, #1233). + # Намеренно ВНЕ monday-кластера тяжёлых site_finder-рефрешей (scrape_kn 04:15, + # ird 05:00, gknspecial 05:30, supply-layers 06:00, genplan 06:30, location 07:00) и + # вне воскресного okn-objects (04:30) — час запаса, не конкурирует за БД/CPU. + # MV агрегирует темпы вымывания планировок (меняются медленно) → еженедельно хватает. + # Техническая infra-задача, не в job_settings (как refresh-quarter-price-index / supply-layers). + schedule["refresh-layout-velocity"] = { + "task": "tasks.refresh_layout_velocity.refresh_layout_velocity", + "schedule": _parse_cron("0 3 * * sun"), # 03:00 MSK, воскресенье + "options": {"queue": "celery"}, + } + return schedule diff --git a/backend/app/workers/celery_app.py b/backend/app/workers/celery_app.py index 22d2345b..df98ecb0 100644 --- a/backend/app/workers/celery_app.py +++ b/backend/app/workers/celery_app.py @@ -79,6 +79,7 @@ celery_app = Celery( "app.workers.tasks.pat_subzones_load", "app.workers.tasks.izyatie_ocr_ingest", "app.workers.tasks.developer_registry_refresh", + "app.workers.tasks.refresh_layout_velocity", ], ) celery_app.conf.timezone = "Europe/Moscow" diff --git a/backend/app/workers/tasks/refresh_layout_velocity.py b/backend/app/workers/tasks/refresh_layout_velocity.py new file mode 100644 index 00000000..7f67c4aa --- /dev/null +++ b/backend/app/workers/tasks/refresh_layout_velocity.py @@ -0,0 +1,47 @@ +"""Celery task: refresh mv_layout_velocity (питает «лучшие планировки» best_layouts). + +Scheduled via hardcoded beat entry in workers/beat_schedule.py: + 'refresh-layout-velocity' — weekly on Sunday at 03:00 MSK. + Стоит в «ночном» окне воскресенья, отдельно от monday-кластера тяжёлых + site_finder-рефрешей (supply-layers / ird / gknspecial / cbr и т.д.), чтобы + не конкурировать с ними за БД/CPU. + +Issue: #1666 (рефреш не был подключён в beat → данные best_layouts молча +устаревали в проде). MV-источник: #113 (PR B, mv_layout_velocity → best_layouts). +""" + +from __future__ import annotations + +import logging +from typing import Any + +from app.core.db import SessionLocal +from app.services.site_finder.layout_velocity_refresh import refresh_layout_velocity +from app.workers.celery_app import celery_app + +logger = logging.getLogger(__name__) + + +@celery_app.task( + bind=True, + name="tasks.refresh_layout_velocity.refresh_layout_velocity", + max_retries=2, +) +def refresh_layout_velocity_task(self: Any) -> dict[str, Any]: + """REFRESH MATERIALIZED VIEW mv_layout_velocity (best_layouts, #113 / #1666). + + MV рефрешится CONCURRENTLY (non-blocking, требует unique-индекс + mv_layout_velocity_pk); сервис сам падает в non-concurrent при unpopulated MV. + + Returns result dict for Celery task result store / logging. + """ + db = SessionLocal() + try: + count = refresh_layout_velocity(db, concurrently=True) + logger.info("refresh_layout_velocity: completed, mv rows=%d", count) + return {"status": "ok", "mv_layout_velocity_rows": count} + except Exception as e: + logger.exception("refresh_layout_velocity failed: %s", e) + raise + finally: + db.close()