Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
118 lines
4.9 KiB
Python
118 lines
4.9 KiB
Python
"""Celery task: ночной cross-load ETL tradein.houses → gendesign.newbuilding_listings.
|
||
|
||
Расписание: 03:30 МСК (hardcoded в beat_schedule, как cbr/poi/supply-layers).
|
||
Идемпотентен: UPSERT ON CONFLICT (source, ext_house_id).
|
||
Если TRADEIN_DATABASE_URL не задан → warn-log, возвращает {"disabled": True}.
|
||
|
||
Breadcrumb (issue #2445 D6): прогон логируется в objective_scrape_runs (тот же
|
||
журнал, что используют соседние nightly/weekly-таски — см.
|
||
app/workers/tasks/objective_etl.py) с group_name='newbuilding-crossload', чтобы
|
||
сбой был виден оператору в admin-таблице логов, а не только в Celery FAILURE +
|
||
GlitchTip. Таблица переиспользуется как есть — без новой миграции; поля
|
||
rows_lots/rows_history/requests_count не про Objective-домен здесь, а про
|
||
source_rows/upserted/skipped cross-load'а (см. комментарии у _finish_run).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.etl.newbuilding_crossload import run_crossload
|
||
from app.workers.celery_app import celery_app
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_GROUP_NAME = "newbuilding-crossload"
|
||
|
||
|
||
def _start_run(db: Session, triggered_by: str) -> int:
|
||
"""Создать breadcrumb-строку в objective_scrape_runs, статус 'running'."""
|
||
row = db.execute(
|
||
text(
|
||
"""
|
||
INSERT INTO objective_scrape_runs (group_name, triggered_by, status)
|
||
VALUES (:gn, :tb, 'running')
|
||
RETURNING run_id
|
||
"""
|
||
),
|
||
{"gn": _GROUP_NAME, "tb": triggered_by},
|
||
).scalar_one()
|
||
db.commit()
|
||
return int(row)
|
||
|
||
|
||
def _finish_run(
|
||
db: Session, run_id: int, *, status: str, error: str | None = None, **counts: int
|
||
) -> None:
|
||
"""Обновить breadcrumb-строку финальным статусом ('done' | 'failed') + счётчиками."""
|
||
sets = ["finished_at = NOW()", "status = :status", "error = :error"]
|
||
params: dict[str, Any] = {"rid": run_id, "status": status, "error": error}
|
||
for k, v in counts.items():
|
||
sets.append(f"{k} = :{k}")
|
||
params[k] = v
|
||
db.execute(
|
||
text(f"UPDATE objective_scrape_runs SET {', '.join(sets)} WHERE run_id = :rid"),
|
||
params,
|
||
)
|
||
db.commit()
|
||
|
||
|
||
@celery_app.task(name="tasks.etl_newbuilding_crossload.etl_newbuilding_crossload")
|
||
def etl_newbuilding_crossload(triggered_by: str = "beat") -> dict:
|
||
"""Cross-load tradein.houses → gendesign.newbuilding_listings (#976).
|
||
|
||
Вызывается beat'ом в 03:30 МСК и вручную через POST /api/v1/admin/scrape/newbuilding-crossload.
|
||
|
||
Args:
|
||
triggered_by: 'beat' | 'manual' | 'api' — для журнала objective_scrape_runs.
|
||
|
||
Breadcrumb-строка в objective_scrape_runs логирует start/success/failure —
|
||
на исключении из run_crossload() помечает run 'failed' и ре-рейзит, чтобы
|
||
Celery ушёл в FAILURE и GlitchTip продолжил ловить сбой (issue #2445 D6).
|
||
"""
|
||
db = SessionLocal()
|
||
run_id: int | None = None
|
||
try:
|
||
run_id = _start_run(db, triggered_by)
|
||
logger.info("task etl_newbuilding_crossload: start run_id=%s", run_id)
|
||
|
||
result = run_crossload(db=db)
|
||
|
||
if result.get("disabled"):
|
||
_finish_run(db, run_id, status="done", error="TRADEIN_DATABASE_URL not set")
|
||
logger.info("task etl_newbuilding_crossload: done (disabled) run_id=%s", run_id)
|
||
return {"run_id": run_id, **result}
|
||
|
||
_finish_run(
|
||
db,
|
||
run_id,
|
||
status="done",
|
||
error=None,
|
||
rows_history=result.get("source_rows", 0),
|
||
rows_lots=result.get("upserted", 0),
|
||
requests_count=result.get("skipped", 0),
|
||
)
|
||
logger.info("task etl_newbuilding_crossload: done run_id=%s — %s", run_id, result)
|
||
return {"run_id": run_id, **result}
|
||
|
||
except Exception as e:
|
||
logger.exception("task etl_newbuilding_crossload: failed run_id=%s: %s", run_id, e)
|
||
if run_id is not None:
|
||
try:
|
||
_finish_run(db, run_id, status="failed", error=f"{type(e).__name__}: {e}")
|
||
except Exception:
|
||
# Breadcrumb-flush не должен скрыть исходное исключение — Celery
|
||
# всё равно должен уйти в FAILURE + GlitchTip его поймает.
|
||
logger.exception(
|
||
"task etl_newbuilding_crossload: failed to write failure breadcrumb "
|
||
"for run_id=%s",
|
||
run_id,
|
||
)
|
||
raise
|
||
finally:
|
||
db.close()
|