fix(backend): исправить 4 регрессии code-review (PR #1660 follow-up)
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (push) Successful in 8s
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI / openapi-codegen-check (push) Successful in 2m14s
CI / backend-tests (push) Failing after 9m10s
CI / backend-tests (pull_request) Failing after 9m12s
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (push) Successful in 8s
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI / openapi-codegen-check (push) Successful in 2m14s
CI / backend-tests (push) Failing after 9m10s
CI / backend-tests (pull_request) Failing after 9m12s
- parcels: forecast_site_finder_report.delay() ПОСЛЕ persist_analysis_run (был commit-before-enqueue race → воркер видел None/stale ран, ретраев нет) - test_affordability: ожидания сценарного платежа с _KEY_RATE_MARKET_SPREAD_PP (правка спреда ломала test_scenario_payment_per_horizon → красный gate) - scrape_cadastre: targets_total += уже выполненное при resume (skipped_fresh кварталы не должны занижать total → преждевременный 'done') - trade_in_pdf: бейдж достоверности = n_analogs (полная статбаза), не len(analogs)
This commit is contained in:
parent
e0fff9cfb0
commit
dd1443045e
4 changed files with 48 additions and 29 deletions
|
|
@ -2861,28 +2861,15 @@ def analyze_parcel(
|
|||
# (repository.py:140) ДО этих мутаций, и сохранённый ран расходится с live-ответом
|
||||
# (GET /runs/{run_id} вернул бы отчёт без этих блоков при re-open).
|
||||
|
||||
# §22-форсайт (3b-ii, #995): best-effort fire-and-forget enqueue. Таска
|
||||
# `forecast_site_finder_report` в фоне (~30-180s) читает persisted analyze-1.0 ран
|
||||
# (latest_run_for на момент исполнения, не на момент enqueue) и считает §22
|
||||
# SiteFinderReport ('1.0'). analyze НЕ ждёт её — возвращаемся сразу. Celery/Redis down
|
||||
# НЕ должен валить ответ (он уже успешен: frontend зависит от формы). Зеркалит
|
||||
# best-effort стиль find_or_enqueue_fetch. Lazy import — избегаем import-цикла
|
||||
# api ↔ workers.tasks на старте.
|
||||
try:
|
||||
from app.workers.tasks.forecast import forecast_site_finder_report
|
||||
|
||||
forecast_site_finder_report.delay(cad_num, horizon, x_authenticated_user)
|
||||
result_payload["forecast"] = {"status": "pending", "horizon": horizon}
|
||||
except Exception:
|
||||
# Enqueue не удался (broker недоступен и т.п.) — §9.x форсайт advisory,
|
||||
# клиент узнаёт по status="unavailable" и не будет зря поллить /forecast.
|
||||
logger.warning(
|
||||
"forecast enqueue failed for cad=%s horizon=%s — analyze response unaffected",
|
||||
cad_num,
|
||||
horizon,
|
||||
exc_info=True,
|
||||
)
|
||||
result_payload["forecast"] = {"status": "unavailable", "horizon": horizon}
|
||||
# §22-форсайт (3b-ii, #995): снимок статуса в result_payload ДО persist, чтобы
|
||||
# jsonb-снимок совпадал с live-ответом. Оптимистично ставим "pending" — фактический
|
||||
# enqueue делаем ПОСЛЕ persist_analysis_run (ниже): иначе Celery-воркер может стартануть
|
||||
# ДО коммита analyze-рана и latest_run_for вернёт None/старый ран → форсайт молча не
|
||||
# посчитается, а ретраев у таски нет (regression #1561-followup). result_payload
|
||||
# передаётся в persist by-reference; если enqueue провалится после persist —
|
||||
# перепишем снимок на "unavailable" уже только в возвращаемом ответе (persisted
|
||||
# снимок останется "pending", но это безвредно: poll-ручка читает live-статус рана).
|
||||
result_payload["forecast"] = {"status": "pending", "horizon": horizon}
|
||||
|
||||
# ИРД-слой (#1067 D9b «GG-форсайт»): parcel_ird_overlaps (м.132, incl opportunity) +
|
||||
# функц.зона/КРТ (геопортал WFS) + ПЗЗ-регламент зоны (C8b). Flag-gated (default off):
|
||||
|
|
@ -2950,6 +2937,30 @@ def analyze_parcel(
|
|||
created_by=x_authenticated_user,
|
||||
)
|
||||
|
||||
# §22-форсайт enqueue — СТРОГО ПОСЛЕ persist_analysis_run. persist_analysis_run —
|
||||
# единственный commit analyze-рана (get_db() на success не коммитит), поэтому enqueue
|
||||
# должен случиться только после того, как ран закоммичен: иначе фоновая таска
|
||||
# `forecast_site_finder_report` (~30-180s) прочтёт latest_run_for и не найдёт свежий
|
||||
# ран (None/старый) → форсайт молча не посчитается, ретраев нет (#1561-followup).
|
||||
# Best-effort fire-and-forget: Celery/Redis down НЕ валит ответ (он уже успешен,
|
||||
# frontend зависит от формы). Зеркалит best-effort стиль find_or_enqueue_fetch.
|
||||
# Lazy import — избегаем import-цикла api ↔ workers.tasks на старте.
|
||||
try:
|
||||
from app.workers.tasks.forecast import forecast_site_finder_report
|
||||
|
||||
forecast_site_finder_report.delay(cad_num, horizon, x_authenticated_user)
|
||||
except Exception:
|
||||
# Enqueue не удался (broker недоступен и т.п.) — §9.x форсайт advisory,
|
||||
# клиент узнаёт по status="unavailable" и не будет зря поллить /forecast.
|
||||
# persisted снимок остаётся "pending" (безвреден — poll читает live-статус рана).
|
||||
logger.warning(
|
||||
"forecast enqueue failed for cad=%s horizon=%s — analyze response unaffected",
|
||||
cad_num,
|
||||
horizon,
|
||||
exc_info=True,
|
||||
)
|
||||
result_payload["forecast"] = {"status": "unavailable", "horizon": horizon}
|
||||
|
||||
return result_payload
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ def _build_html(estimate: AggregatedEstimate, input_snapshot: dict) -> str: # t
|
|||
<div class="badge"
|
||||
style="background:{conf_bg}; color:{conf_fg}; border:1px solid {conf_border};">
|
||||
Достоверность: {conf_label}<br>
|
||||
<span style="font-size:8pt; font-weight:400;">{len(estimate.analogs)} аналогов</span>
|
||||
<span style="font-size:8pt; font-weight:400;">{estimate.n_analogs} аналогов</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -408,14 +408,19 @@ def enqueue_cadastre_harvest(self: Any, job_id: int) -> dict[str, Any]:
|
|||
# skipped_fresh повторно. Поэтому:
|
||||
# • targets_skipped — идемпотентный SET (= число свежих сейчас),
|
||||
# а не cumulative INCREMENT (иначе раздувается на каждый resume);
|
||||
# • targets_total — GREATEST с уже учтённым прогрессом
|
||||
# (targets_done + targets_failed), чтобы total не сжался ниже
|
||||
# done → не вызвал преждевременный 'done' в _maybe_finish_job.
|
||||
# • targets_total — :new_total это только ОСТАВШИЕСЯ к обработке
|
||||
# кварталы (len(quarters) − skipped_fresh). При resume уже
|
||||
# обработанные попадают в skipped_fresh, поэтому к остатку
|
||||
# добавляем уже учтённый прогресс (done + failed), иначе total
|
||||
# занижается и _maybe_finish_job помечает job 'done' после первого
|
||||
# доработанного квартала (#1654-followup). GREATEST с самим
|
||||
# прогрессом сохраняет монотонность (total не уменьшается).
|
||||
db.execute(
|
||||
text(
|
||||
"UPDATE cadastre_jobs SET "
|
||||
"targets_total = GREATEST("
|
||||
":new_total, COALESCE(targets_done, 0) + COALESCE(targets_failed, 0)"
|
||||
":new_total + COALESCE(targets_done, 0) + COALESCE(targets_failed, 0), "
|
||||
"COALESCE(targets_done, 0) + COALESCE(targets_failed, 0)"
|
||||
"), "
|
||||
"targets_skipped = :sk "
|
||||
"WHERE job_id = :id"
|
||||
|
|
|
|||
|
|
@ -269,11 +269,14 @@ class TestPaymentAtScenario:
|
|||
res = _run(price_per_m2=120_000.0, rate_path={6: 8.0, 12: 20.0})
|
||||
assert res.payment_at_scenario is not None
|
||||
principal = 120_000.0 * _REF_AREA_M2
|
||||
# rate_path несёт КЛЮЧЕВУЮ ставку сценария; affordability приводит к рыночной
|
||||
# базе (+ _KEY_RATE_MARKET_SPREAD_PP), как и monthly_payment_rub (#1639). Ожидания
|
||||
# выражаем символически — тест переживёт перекалибровку спреда.
|
||||
assert res.payment_at_scenario[6] == pytest.approx(
|
||||
_annuity(principal, 8.0, _ANNUITY_TERM_MONTHS)
|
||||
_annuity(principal, 8.0 + _KEY_RATE_MARKET_SPREAD_PP, _ANNUITY_TERM_MONTHS)
|
||||
)
|
||||
assert res.payment_at_scenario[12] == pytest.approx(
|
||||
_annuity(principal, 20.0, _ANNUITY_TERM_MONTHS)
|
||||
_annuity(principal, 20.0 + _KEY_RATE_MARKET_SPREAD_PP, _ANNUITY_TERM_MONTHS)
|
||||
)
|
||||
# Выше ставка → выше платёж на этом горизонте.
|
||||
assert res.payment_at_scenario[12] > res.payment_at_scenario[6]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue