All checks were successful
CI / backend-tests (push) Successful in 8m49s
CI / backend-tests (pull_request) Successful in 8m52s
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m57s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
mv_layout_velocity (powers best_layouts, #113) refreshed only via manual refresh_layout_velocity() → prod data silently went stale. Add Celery task mirroring refresh_quarter_price_index, register in celery_app include, add weekly beat entry (Sun 03:00 MSK, outside the monday heavy-refresh cluster). Closes #1666
525 lines
30 KiB
Python
525 lines
30 KiB
Python
"""Beat schedule builder for Celery.
|
||
|
||
Priority order:
|
||
1. DB-driven — читает таблицу job_settings при старте beat-контейнера.
|
||
2. Fallback — env-based расписание (SCRAPE_KN_CRON, OBJECTIVE_SYNC_CRON),
|
||
используется если БД недоступна или таблица пустая.
|
||
3. Hardcoded entries — добавляются поверх DB-расписания (не в job_settings):
|
||
- refresh-ekb-districts-medians (аналитика, ежемесячно 5-го в 04:00)
|
||
- nspd-geo-zombie-cleanup (каждую минуту, замена нестабильного worker_ready)
|
||
- poi-sync-weekly (OSM POI, понедельник 03:00)
|
||
- noise-sync-weekly (OSM шум, понедельник 03:30)
|
||
|
||
Публичный API: build_beat_schedule() → dict.
|
||
"""
|
||
|
||
import logging
|
||
|
||
from celery.schedules import crontab
|
||
|
||
from app.core.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Словарь job_type → celery task name (полное dotted name).
|
||
# Используется при построении beat_schedule из job_settings.
|
||
TASK_MAP: dict[str, str] = {
|
||
"scrape_kn": "tasks.scrape_kn.scrape_kn_region",
|
||
"objective_sync": "tasks.scrape_objective.sync_all_groups",
|
||
# nspd_geo и objective_etl не имеют cron — запускаются вручную.
|
||
# При необходимости можно добавить сюда.
|
||
}
|
||
|
||
|
||
def _parse_cron(spec: str) -> crontab:
|
||
"""Parse 'M H DoM Mon DoW' crontab string into Celery crontab. Empty fields default to '*'."""
|
||
parts = spec.strip().split()
|
||
if len(parts) != 5:
|
||
raise ValueError(f"crontab spec must have 5 fields, got: {spec!r}")
|
||
minute, hour, dom, month, dow = parts
|
||
return crontab(
|
||
minute=minute,
|
||
hour=hour,
|
||
day_of_month=dom,
|
||
month_of_year=month,
|
||
day_of_week=dow,
|
||
)
|
||
|
||
|
||
def _default_regions() -> list[int]:
|
||
return [int(x.strip()) for x in settings.scrape_kn_default_regions.split(",") if x.strip()]
|
||
|
||
|
||
def _build_beat_schedule_from_db() -> dict:
|
||
"""Строит beat_schedule из job_settings БД.
|
||
|
||
Returns: schedule (может быть пустой если все enabled rows без cron или disabled).
|
||
|
||
`get_all` маскирует DB-unreachable case через возврат _DEFAULTS (4 synthetic
|
||
rows, все enabled=True с дефолтными cron'ами) — это значит "DB unreachable" и
|
||
"fresh install (table empty)" неотличимы от "normal с _DEFAULTS". В обоих
|
||
случаях schedule НЕ будет пустым — _DEFAULTS дадут scrape_kn + objective_sync.
|
||
|
||
КРИТИЧНО для operator-disable scenario: когда DB reachable и оператор сделал
|
||
`UPDATE job_settings SET enabled=false WHERE job_type IN (...)` — get_all
|
||
вернёт реальные rows (НЕ _DEFAULTS), loop их пропустит → schedule={}. Caller
|
||
(build_beat_schedule) НЕ должен срывался на env fallback в этом случае,
|
||
иначе disable бесполезен. См. инцидент 2026-05-24 (WAF cooldown disable
|
||
scrape_kn → old code fallback'ал scrape_kn обратно из env).
|
||
"""
|
||
from app.core.db import SessionLocal
|
||
from app.services.job_settings import get_all
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
rows = get_all(db)
|
||
finally:
|
||
db.close()
|
||
|
||
schedule: dict = {}
|
||
for row in rows:
|
||
jt = row["job_type"]
|
||
if not row.get("enabled"):
|
||
continue
|
||
cron_str = row.get("cron_schedule")
|
||
if not cron_str:
|
||
# NULL cron = только ручной запуск, не добавляем в beat
|
||
continue
|
||
if jt not in TASK_MAP:
|
||
logger.warning(
|
||
"build_beat_schedule: job_type '%s' не имеет TASK_MAP-записи — пропускаем", jt
|
||
)
|
||
continue
|
||
try:
|
||
cron = _parse_cron(cron_str)
|
||
except Exception as e:
|
||
logger.warning(
|
||
"build_beat_schedule: невалидный cron '%s' для '%s': %s", cron_str, jt, e
|
||
)
|
||
continue
|
||
|
||
queue = row.get("queue_name", "celery") or "celery"
|
||
|
||
if jt == "scrape_kn":
|
||
# scrape_kn запускается отдельной задачей на каждый регион
|
||
extra = row.get("extra_config") or {}
|
||
regions: list[int] = extra.get("default_regions") or _default_regions()
|
||
for rc in regions:
|
||
schedule[f"kn-region-{rc}"] = {
|
||
"task": TASK_MAP[jt],
|
||
"schedule": cron,
|
||
"args": [rc, None],
|
||
"options": {"queue": queue},
|
||
}
|
||
logger.info(
|
||
"build_beat_schedule: scrape_kn cron=%r regions=%s queue=%s",
|
||
cron_str,
|
||
regions,
|
||
queue,
|
||
)
|
||
else:
|
||
kwargs: dict = {}
|
||
if jt == "objective_sync":
|
||
kwargs = {"triggered_by": "beat"}
|
||
schedule[jt] = {
|
||
"task": TASK_MAP[jt],
|
||
"schedule": cron,
|
||
"kwargs": kwargs,
|
||
"options": {"queue": queue},
|
||
}
|
||
logger.info("build_beat_schedule: %s cron=%r queue=%s", jt, cron_str, queue)
|
||
|
||
# КРТ-площадки ЕКБ (#1060): реестр обновляется редко (квартальные решения ПАГЕ),
|
||
# раз в неделю достаточно. Среда 05:00 МСК — не пересекается с permits (вт/ср 02:00).
|
||
schedule["ekb-krt-sync-weekly"] = {
|
||
"task": "tasks.ekb_krt_sync.sync_krt_sites",
|
||
"schedule": _parse_cron("0 5 * * wed"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
return schedule
|
||
|
||
|
||
def _build_beat_schedule_fallback() -> dict:
|
||
"""Fallback-расписание из env vars — используется если БД недоступна на старте."""
|
||
logger.warning("beat_schedule: строим fallback из env vars")
|
||
schedule: dict = {}
|
||
|
||
# scrape_kn
|
||
try:
|
||
for rc in _default_regions():
|
||
schedule[f"kn-region-{rc}"] = {
|
||
"task": "tasks.scrape_kn.scrape_kn_region",
|
||
"schedule": _parse_cron(settings.scrape_kn_cron),
|
||
"args": [rc, None],
|
||
}
|
||
except Exception as e:
|
||
logger.warning("beat_schedule fallback: scrape_kn failed: %s", e)
|
||
|
||
# objective_sync
|
||
try:
|
||
from app.core.db import SessionLocal
|
||
from app.services.objective_sync_config import get_cron_schedule_safe
|
||
|
||
cron_str = get_cron_schedule_safe(SessionLocal)
|
||
except Exception:
|
||
cron_str = settings.objective_sync_cron
|
||
|
||
try:
|
||
schedule["objective-sync"] = {
|
||
"task": "tasks.scrape_objective.sync_all_groups",
|
||
"schedule": _parse_cron(cron_str),
|
||
"kwargs": {"triggered_by": "beat"},
|
||
}
|
||
except Exception as e:
|
||
logger.warning("beat_schedule fallback: objective_sync failed: %s", e)
|
||
|
||
# refresh ekb-districts medians — ежемесячно 5-го числа в 04:00 МСК
|
||
try:
|
||
schedule["refresh-ekb-districts-medians"] = {
|
||
"task": "tasks.refresh_analytics.refresh_ekb_districts_medians",
|
||
"schedule": _parse_cron("0 4 5 * *"),
|
||
"kwargs": {"window_months": 24, "min_deals": 50},
|
||
}
|
||
except Exception as e:
|
||
logger.warning("beat_schedule fallback: refresh_analytics failed: %s", e)
|
||
|
||
# КРТ-площадки ЕКБ (#1060): реестр обновляется редко (квартальные решения ПАГЕ),
|
||
# раз в неделю достаточно. Среда 05:00 МСК — не пересекается с permits (вт/ср 02:00).
|
||
schedule["ekb-krt-sync-weekly"] = {
|
||
"task": "tasks.ekb_krt_sync.sync_krt_sites",
|
||
"schedule": _parse_cron("0 5 * * wed"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
return schedule
|
||
|
||
|
||
def build_beat_schedule() -> dict:
|
||
"""Строит beat_schedule из DB (job_settings) + добавляет hardcoded entries.
|
||
|
||
`_build_beat_schedule_from_db` использует `get_all` который сам fallback'ает на
|
||
_DEFAULTS при DB unreachable / fresh install → возвращает 4 synthetic rows
|
||
(scrape_kn + objective_sync enabled). То есть schedule из DB-builder ПУСТ
|
||
только когда оператор явно disabled все cron-able jobs через UPDATE
|
||
job_settings — это **намерение**, респектим без env-fallback.
|
||
|
||
Hardcoded entries (refresh-analytics, OSM POI/noise, nspd cleanup) добавляются
|
||
ВСЕГДА — они не управляются через job_settings.
|
||
"""
|
||
schedule = _build_beat_schedule_from_db()
|
||
|
||
# Всегда добавляем refresh_analytics (этот job нет в job_settings,
|
||
# он технический и не требует конфигурации через UI).
|
||
if "refresh-ekb-districts-medians" not in schedule:
|
||
try:
|
||
schedule["refresh-ekb-districts-medians"] = {
|
||
"task": "tasks.refresh_analytics.refresh_ekb_districts_medians",
|
||
"schedule": _parse_cron("0 4 5 * *"),
|
||
"kwargs": {"window_months": 24, "min_deals": 50},
|
||
"options": {"queue": "celery"},
|
||
}
|
||
except Exception as e:
|
||
logger.warning("beat_schedule: refresh_analytics failed: %s", e)
|
||
|
||
# Zombie cleanup: каждую минуту проверяет nspd_geo_jobs с heartbeat > 2 мин
|
||
# и re-enqueue'ит их. Замена worker_ready signal handler'а — он не fires
|
||
# стабильно в нашем setup, beat-task надёжнее.
|
||
schedule["nspd-geo-zombie-cleanup"] = {
|
||
"task": "tasks.nspd_geo.cleanup_zombies",
|
||
"schedule": _parse_cron("* * * * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Bug #1236: парный cleanup для cadastre_jobs. Переводит застрявшие
|
||
# 'running' jobs (heartbeat > 5 мин) в 'paused', чтобы оператор мог
|
||
# resume через admin API. Раз в 2 минуты — cadastre fanout меньше nspd_geo,
|
||
# per-minute опрос DB не оправдан.
|
||
schedule["cadastre-zombie-cleanup"] = {
|
||
"task": "tasks.cadastre.cleanup_zombies",
|
||
"schedule": _parse_cron("*/2 * * * *"),
|
||
"options": {"queue": "geo"},
|
||
}
|
||
|
||
# OSM POI sync — еженедельно в понедельник в 03:00 МСК
|
||
schedule["poi-sync-weekly"] = {
|
||
"task": "tasks.poi_sync.sync_osm_poi_ekb",
|
||
"schedule": _parse_cron("0 3 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# OSM noise sources sync — еженедельно в понедельник в 03:30 МСК
|
||
# (через 30 мин после poi-sync, чтобы не нагружать Overpass одновременно)
|
||
schedule["noise-sync-weekly"] = {
|
||
"task": "tasks.noise_sync.sync_osm_noise_sources_ekb",
|
||
"schedule": _parse_cron("30 3 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# #105 Phase 4: ЕКБ РНС/РВЭ — ежемесячно 1-го числа в 05:00 МСК.
|
||
# Celery conf.timezone=Europe/Moscow → crontab трактуется в МСК (#1233).
|
||
schedule["ekburg-permits-monthly"] = {
|
||
"task": "tasks.ekburg_permits_sync.refresh_all",
|
||
"schedule": _parse_cron("0 5 1 * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Developer registry refresh (#1088 «GG-форсайт») — ежемесячно 1-го числа в 05:30 МСК,
|
||
# через 30 мин ПОСЛЕ ekburg-permits-monthly (05:00), чтобы permits-сторона реестра
|
||
# обновлялась на свежих РНС/РВЭ. fn_refresh_developer_registry() → REFRESH MATERIALIZED
|
||
# VIEW CONCURRENTLY developer_registry (миграция 149; не блокирует читателей analyze).
|
||
# Celery conf.timezone=Europe/Moscow → crontab трактуется в МСК (#1233). Техническая
|
||
# infra-задача (не в job_settings, как refresh-quarter-price-index). domrf-сторона
|
||
# обновляется ежедневно (scrape_kn), но ИНН/track-record меняются редко — monthly хватает.
|
||
schedule["developer-registry-refresh-monthly"] = {
|
||
"task": "tasks.developer_registry_refresh.refresh_developer_registry",
|
||
"schedule": _parse_cron("30 5 1 * *"), # 05:30 MSK, 1st of month
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ПЗЗ территориальных зон ЕКБ из PKK6 ArcGIS — DISABLED 2026-06-13 (issue #259).
|
||
#
|
||
# ПОЧЕМУ ОТКЛЮЧЕНО: источник pkk.rosreestr.ru/.../PKK6/ZONES deprecated (Росреестр
|
||
# вывел PKK6-эндпоинт), таблица pzz_zones_ekb пуста (0 rows на prod) — задача ни
|
||
# разу не наполнила её успешно. ПЗЗ-данные пришли в систему ИНЫМ путём:
|
||
# zone_regulation_cache (#1059, beat zone-regulation-refresh-monthly) + NSPD
|
||
# territorial_zones dumps (Phase 2.5 bulk_harvest, layer 875838). Активный beat
|
||
# каждый месяц дёргал deprecated PKK6 → broad except в pzz_sync.py логировал
|
||
# ошибку как error → рекуррентил GlitchTip BACKEND-1B.
|
||
#
|
||
# Ручной перезапуск (если появится рабочий источник) остаётся через admin-эндпоинт
|
||
# POST /api/v1/admin/scrape/pzz-sync. Ref: #233 (исходно «0 rows»), #259 (disable).
|
||
# schedule["pzz-sync-monthly"] = {
|
||
# "task": "tasks.pzz_sync.sync_pzz_zones_ekb",
|
||
# "schedule": _parse_cron("0 3 1 * *"),
|
||
# "options": {"queue": "celery"},
|
||
# }
|
||
|
||
# Catalog-object scrape — наполняет ~25 NULL колонок domrf_kn_objects из SSR-страниц.
|
||
# kn-API не отдаёт wall_type, energy_eff, ceiling_height_m, parking_* и т.д.
|
||
# Вторник 04:00 UTC. batch 300/run → 1532 объекта за ~5 недель полного обновления.
|
||
#
|
||
# DISABLED 2026-05-24: DOM.РФ WAF дал hard-ban на VPS IP после серии failed
|
||
# extras-сессий (run 26/27/28). Catalog SSR использует тот же BrowserSession
|
||
# + те же /сервисы/* paths → следующий beat-tick (вт 26.05 04:00 UTC) насыпет
|
||
# 300 failed SSR fetches и углубит WAF reputation penalty. Возврат после
|
||
# cooldown 24-48h (проверить через targeted test).
|
||
# schedule["scrape-kn-catalog-objects-weekly"] = {
|
||
# "task": "tasks.scrape_kn_catalog_objects.scrape_kn_catalog_objects",
|
||
# "schedule": _parse_cron("0 4 * * 2"), # Tuesday 04:00 UTC
|
||
# "kwargs": {"region_code": 66, "max_objects": 300},
|
||
# "options": {"queue": "celery"},
|
||
# }
|
||
|
||
# Quarter price index refresh (#762) — monthly on the 5th at 05:00 MSK.
|
||
# Celery conf.timezone=Europe/Moscow → crontab трактуется в МСК (#1233).
|
||
# Chains: mv_quarter_price_per_m2 → mv_quarter_price_index (in order, both CONCURRENTLY).
|
||
# Runs 1 h after refresh-ekb-districts-medians (04:00 MSK) so deals data is settled.
|
||
# Not in job_settings (technical infra task, no UI config needed).
|
||
schedule["refresh-quarter-price-index"] = {
|
||
"task": "tasks.refresh_quarter_price_index.refresh_quarter_price_index_chain",
|
||
"schedule": _parse_cron("0 5 5 * *"), # 05:00 MSK, 5th of month
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# NSPD quarter dump refresh — re-enabled 2026-05-17 после Sub-PR B (#260)
|
||
# переключения search_by_quarter на grid-walk. Foundation (#247) + integration
|
||
# (#260) теперь возвращают полноценные dumps (territorial_zones, ЗОУИТ, risk
|
||
# zones, engineering structures) вместо 0-3 features из single-pixel WMS.
|
||
#
|
||
# Schedule: Mon 04:00 МСК (Celery conf.timezone=Europe/Moscow → crontab в МСК, #1233).
|
||
# batch_size=50 ограничивает fanout per
|
||
# tick — при 11k+ кварталов в cad_quarters_geom полный backfill займёт
|
||
# несколько недель, но защищает от WAF rate-limit burst.
|
||
# max_age_days=90 — refresh свежее квартала; новые / отсутствующие dumps
|
||
# тоже попадают через harvest_stale_quarters (берёт NULL fetched_at).
|
||
schedule["nspd-harvest-stale-quarters"] = {
|
||
"task": "tasks.nspd_sync.harvest_stale_quarters",
|
||
"schedule": _parse_cron("0 4 * * mon"), # 04:00 МСК
|
||
"kwargs": {"region_code": 66, "max_age_days": 90, "batch_size": 50},
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# CBR макро-показатели (ключевая ставка) → macro_indicator (#945 PR B,
|
||
# GG-форсайт). Ставка меняется редко (8 заседаний ЦБ в год) — еженедельного
|
||
# прогона с запасом достаточно. ON CONFLICT делает прогон идемпотентным
|
||
# (полный бэкфилл с 2019-01-01 каждый раз; дёшево — ~1 SOAP-запрос + ~1500
|
||
# upsert'ов). Понедельник 05:30 МСК (Celery conf.timezone=Europe/Moscow, т.е.
|
||
# crontab трактуется в МСК) — после publish итогов пятничного заседания. Задача
|
||
# техническая, не управляется через job_settings — поэтому добавлена сюда (как
|
||
# poi-sync / refresh-analytics). Оператор может перенести расписание правкой crontab.
|
||
schedule["cbr-macro-sync-weekly"] = {
|
||
"task": "tasks.cbr_macro_sync.cbr_macro_sync",
|
||
"schedule": _parse_cron("30 5 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Росстат макро-показатели (демография региона 65 / ЕКБ) → macro_indicator
|
||
# (#946 EPIC2). Ряды Росстата (open-data) обновляются редко (годовые) — ежемесячного
|
||
# прогона с большим запасом достаточно. ON CONFLICT делает прогон идемпотентным
|
||
# (полный ре-fetch каждый раз; дёшево — пара CSV-файлов). 1-е число месяца 05:45 МСК
|
||
# (Celery conf.timezone=Europe/Moscow → crontab в МСК); по нагрузке не пересекается с
|
||
# cbr-macro (пн 05:30). ИПЦ/доходы/СМР-цены — EMISS-only за WAF, пока не тянутся
|
||
# (см. rosstat_emiss docstring). Задача техническая, не управляется через job_settings —
|
||
# добавлена сюда (как cbr/poi/refresh). Оператор может перенести расписание правкой crontab.
|
||
schedule["rosstat-macro-sync-monthly"] = {
|
||
"task": "tasks.rosstat_macro_sync.rosstat_macro_sync",
|
||
"schedule": _parse_cron("45 5 1 * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Supply-layers пересчёт (#950 EPIC 6 Step5, GG-форсайт) → supply_layers (м.125).
|
||
# Детерминированно UPSERT-ит L1(open)/L2(hidden)/L3(future) по всем районам Свердл.
|
||
# обл. через supply_layers.compute_all_layers. ON CONFLICT идемпотентен в рамках
|
||
# одного snapshot_date (same-day re-run обновляет мутабельные колонки).
|
||
#
|
||
# Понедельник 06:00 МСК (Celery conf.timezone=Europe/Moscow → crontab в МСК).
|
||
# ORDERING ASSUMPTION: scrape_kn (наполняет domrf_kn_objects.free_flats, м.124 —
|
||
# источник L2 hidden) идёт Mon 04:15 МСК (settings.scrape_kn_cron='15 4 * * mon').
|
||
# 06:00 даёт ~1.75 ч запаса, чтобы supply-layers считались по СВЕЖИМ free_flats.
|
||
# Если scrape_kn перенесут позже/он затянется — сдвинуть этот cron соответственно
|
||
# (или сделать chain). Задача техническая, не в job_settings (как cbr/poi/refresh).
|
||
schedule["supply-layers-refresh-weekly"] = {
|
||
"task": "tasks.supply_layers_refresh.supply_layers_refresh",
|
||
"schedule": _parse_cron("0 6 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ИРД-overlay harvest (#1067 B5): grid-walk готовых НСПД ИРД-слоёв по кварталам ЕКБ →
|
||
# ird_overlays (м.132). Понедельник 05:00 МСК — до supply-layers (06:00), не конкурируя
|
||
# с scrape_kn (04:15). Геометрия зон стабильна, еженедельно достаточно. Техническая,
|
||
# не в job_settings (как cbr/poi/supply-layers).
|
||
schedule["ird-harvest-weekly"] = {
|
||
"task": "tasks.ird_harvest.harvest_ird_overlays",
|
||
"schedule": _parse_cron("0 5 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ЗОУИТ-harvest ЕКБ-геопортала (#1063/#1064/#1065/#1066, #1067 GG-форсайт): WFS-фетч
|
||
# gknspecial_zone по full_name-префиксам → ird_overlays (layer_kind=servitude/water_*/
|
||
# airfield/okn_protection_zone/zrz). Понедельник 05:30 МСК — после ird-harvest (05:00),
|
||
# до supply-layers (06:00). Геометрия зон стабильна, еженедельно достаточно.
|
||
schedule["gknspecial-harvest-weekly"] = {
|
||
"task": "tasks.gknspecial_harvest.harvest_gknspecial_zones",
|
||
"schedule": _parse_cron("30 5 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Opportunity-ЗУ harvest (#1086): grid-walk НСПД opportunity-слоёв по кварталам ЕКБ →
|
||
# ird_overlays (layer_kind='opportunity_*'). 1-е число месяца 05:30 МСК — реже ИРД (weekly),
|
||
# opportunity-земля меняется медленнее ограничений. Тот же движок, что ird-harvest.
|
||
schedule["opportunity-harvest-monthly"] = {
|
||
"task": "tasks.opportunity_harvest.harvest_opportunity_overlays",
|
||
"schedule": _parse_cron("30 5 1 * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Cross-load ETL tradein→gendesign (#976 950-E5): tradein.houses → newbuilding_listings.
|
||
# Ночной запуск: 00:30 UTC = 03:30 МСК (Celery conf.timezone=Europe/Moscow → crontab в МСК).
|
||
# Не в job_settings (технический ETL, не требует конфигурации UI).
|
||
# Идемпотентен через ON CONFLICT (source, ext_house_id).
|
||
# Если TRADEIN_DATABASE_URL не задан → warn-log, {"disabled": True} без исключения.
|
||
schedule["newbuilding-crossload-nightly"] = {
|
||
"task": "tasks.etl_newbuilding_crossload.etl_newbuilding_crossload",
|
||
"schedule": _parse_cron("30 0 * * *"), # 00:30 UTC = 03:30 МСК
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ППТ/ПМТ harvest (#1085): WFS-фетч геопортала ЕКБ по bbox агломерации → planning_projects.
|
||
# 1-е число месяца 06:00 МСК — после opportunity (05:30). Документы планировки стабильны,
|
||
# ежемесячно достаточно. WFS BBOX (не grid-walk) — один запрос на слой.
|
||
schedule["planning-harvest-monthly"] = {
|
||
"task": "tasks.planning_harvest.harvest_planning_projects",
|
||
"schedule": _parse_cron("0 6 1 * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Прогрев кэша ПЗЗ-регламента зон ЕКБ (#1059/C8b): WFS-перечисление терзон → urbanCard →
|
||
# zone_regulation_cache. 1-е число месяца 06:30 МСК — после planning (06:00). Регламент
|
||
# меняется только при правке ПЗЗ, ежемесячно достаточно. Наполняет кэш C8b проактивно.
|
||
schedule["zone-regulation-refresh-monthly"] = {
|
||
"task": "tasks.zone_regulation_refresh.refresh_zone_regulations",
|
||
"schedule": _parse_cron("30 6 1 * *"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# КРТ-площадки ЕКБ (#1060): реестр обновляется редко (квартальные решения ПАГЕ),
|
||
# раз в неделю достаточно. Среда 05:00 МСК — не пересекается с permits (вт/ср 02:00).
|
||
schedule["ekb-krt-sync-weekly"] = {
|
||
"task": "tasks.ekb_krt_sync.sync_krt_sites",
|
||
"schedule": _parse_cron("0 5 * * wed"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# КРТ-геометрии ЕКБ (#1130): WFS-фичи portal_geo_oks_krt → ekb_krt_geometry.
|
||
# Геометрия КРТ-территорий стабильна (новые площадки — квартальные решения ПАГЕ),
|
||
# раз в неделю достаточно. Среда 05:30 МСК — сразу после ekb-krt-sync-weekly (05:00),
|
||
# одна сессия геопортала, не дублируем нагрузку.
|
||
schedule["ekb-krt-geometry-sync-weekly"] = {
|
||
"task": "tasks.krt_geometry_sync.sync_krt_geometry",
|
||
"schedule": _parse_cron("30 5 * * wed"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# Функциональные зоны генплана ЕКБ-2045 (#1058): ГИСОГД-СО WFS →
|
||
# ekb_genplan_functional_zone (м.139). ~6436 полигонов, EPSG:4326. Генплан
|
||
# меняется редко (раз в год+), еженедельно избыточно но дёшево — WFS-фетч +
|
||
# upsert по globalid идемпотентен. Понедельник 06:30 МСК — после supply-layers
|
||
# (06:00). Техническая, не в job_settings.
|
||
schedule["genplan-zones-sync-weekly"] = {
|
||
"task": "tasks.genplan_zones_sync.sync_functional_zones",
|
||
"schedule": _parse_cron("30 6 * * mon"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ОКН-объекты ЕКБ/Свердл.обл из АИС ЕГРКН (#1066): POST searchMap →
|
||
# okn_objects (м.141). ~800+ фич (54 фед + 213 рег + remainder по ЕКБ;
|
||
# область добавляет дополнительные). Воскресенье 04:30 МСК — не конкурирует
|
||
# с ird-harvest (понедельник 05:00) и gknspecial (05:30). Реестр ОКН меняется
|
||
# медленно — еженедельно достаточно. Техническая, не в job_settings.
|
||
schedule["okn-objects-sync-weekly"] = {
|
||
"task": "tasks.okn_objects_sync.sync_okn_objects",
|
||
"schedule": _parse_cron("30 4 * * sun"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# ОКН-анализ ЕКБ (#1062): OCR PDF-сканов изъятия с екатеринбург.рф → land_reservation.
|
||
# Постановления об изъятии выходят редко (2-4 раза в год) → еженедельного прогона хватает.
|
||
# Пятница 07:00 МСК — не пересекается с monday-кластером (ird/gkn/supply/cbr).
|
||
# Tesseract rus: каждый PDF ~30 стр., OCR ~1-2 мин/PDF → итого <10 мин/прогон.
|
||
schedule["izyatie-ocr-ingest-weekly"] = {
|
||
"task": "tasks.izyatie_ocr_ingest.ingest_izyatie_ocr",
|
||
"schedule": _parse_cron("0 7 * * fri"),
|
||
"options": {"queue": "celery"},
|
||
}
|
||
|
||
# District-level индексы локаций (#948 Part B, §8.2) → location (м.146): reuse
|
||
# per-district forecast-функций (market_metrics/future_supply/POI), нормализация
|
||
# 0..1, upsert по 8 районам ЕКБ. Понедельник 07:00 МСК — ПОСЛЕ supply-layers
|
||
# (06:00) и genplan (06:30), чтобы future_supply-индекс считался по СВЕЖЕМУ складу
|
||
# предложения. Индексы меняются медленно (рынок поглощает сток неделями) —
|
||
# еженедельно достаточно. ON CONFLICT идемпотентен. Техническая, не в job_settings
|
||
# (как cbr/supply-layers). Оператор может перенести расписание правкой crontab.
|
||
schedule["location-refresh-weekly"] = {
|
||
"task": "tasks.location_refresh.location_refresh",
|
||
"schedule": _parse_cron("0 7 * * mon"),
|
||
"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
|