fix(#3197): no_proxy_stop=1 как у соседей, INFO на финале хендлера, докстринг фабрики
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m57s

This commit is contained in:
bot-backend 2026-09-06 06:27:40 +05:00
parent cb5714fff0
commit 939a8a1646
5 changed files with 39 additions and 18 deletions

View file

@ -367,11 +367,15 @@ async def _job_yandex_newbuilding_sweep(
# исход — назвать прогон неуспешным, а не дотянуть succeeded до ненуля.
processed = int(counters.get("processed") or 0)
succeeded = int(counters.get("succeeded") or 0)
if counters.get("no_proxy_stop"):
if result.no_proxy_stop:
# #3197: прогон оборван на пустом пуле — к площадке не ходили вовсе.
# Это отказ нашей инфраструктуры, а не «ЖК не разрешились»: называть
# такой прогон успешным нельзя, и причина должна быть отличима.
logger.error(
#
# INFO, как у соседей (scheduler.py:175, avito_detail_backfill.py:1050,
# domclick:626): причина уже записана в mark_failed + counters.no_proxy_stop=1,
# а ERROR ставил её в один разряд с падением задачи (logger.exception ниже).
logger.info(
"yandex_newbuilding_sweep run_id=%d: пул прокси пуст — прогон оборван "
"(обработано %d)",
run_id,

View file

@ -85,7 +85,15 @@ class YandexNewbuildingSweepResult:
duration_sec: float = field(default=0.0)
def to_dict(self) -> dict[str, int | float]:
return {f.name: getattr(self, f.name) for f in fields(self)}
"""Счётчики прогона; `no_proxy_stop` — 1/0, а не JSON true/false.
Формат один с соседями (`scheduler.py:168` cian, `avito_detail_backfill.py:1043`,
`domclick_detail_backfill.py:610`): SQL-монитор ищет
`counters->>'no_proxy_stop' = '1'` и мимо `true` промахнётся молча.
"""
out: dict[str, int | float] = {f.name: getattr(self, f.name) for f in fields(self)}
out["no_proxy_stop"] = int(self.no_proxy_stop)
return out
# ── SQL ────────────────────────────────────────────────────────────────────────

View file

@ -27,18 +27,21 @@ import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from app.services.product_handlers import _job_yandex_newbuilding_sweep
from app.tasks.yandex_newbuilding_sweep import YandexNewbuildingSweepResult
def _ctx() -> SimpleNamespace:
return SimpleNamespace(runs=MagicMock())
def _result(processed: int, succeeded: int) -> MagicMock:
r = MagicMock()
r.to_dict = MagicMock(
return_value={"processed": processed, "succeeded": succeeded, "total": 425}
)
return r
def _result(processed: int, succeeded: int) -> YandexNewbuildingSweepResult:
"""Настоящий результат задачи, а не MagicMock.
У MagicMock любой не заданный атрибут (`no_proxy_stop` из #3197) — truthy, то есть
добавление хендлеру новой ветки по полю результата уводило бы сюда ВСЕ три случая,
и тест краснел бы по причине, не имеющей отношения к #2860.
"""
return YandexNewbuildingSweepResult(processed=processed, succeeded=succeeded, total=425)
@pytest.mark.asyncio

View file

@ -172,4 +172,8 @@ async def test_scheduler_marks_run_failed_on_empty_pool() -> None:
_db, run_id, reason, counters = ctx.runs.mark_failed.call_args.args
assert run_id == 3197
assert "пул" in reason
assert counters["no_proxy_stop"] is True
# Ровно 1, а не JSON true: SQL-монитор соседей ищет counters->>'no_proxy_stop' = '1'
# (scheduler.py:168 cian, avito_detail_backfill.py:1043, domclick_detail_backfill.py:610).
# bool — подкласс int, поэтому `== 1` один сам по себе зелёный и на True: тип проверяем
# отдельно, иначе красное на откате не наступит.
assert counters["no_proxy_stop"] == 1 and not isinstance(counters["no_proxy_stop"], bool)

View file

@ -206,16 +206,18 @@ def build_browser_fetcher(
`ScraperConfig` невозможно на уровне сигнатуры (не полагаемся на
code-review чтобы поймать пропущенный параметр).
`proxy_provider=None` (дефолт) валидно: часть providers (avito legacy
curl-only путь, domclick, cian/yandex newbuilding) пока не подключены к
browser-proxy-пулу (#2160 P4 wiring gap, отдельный issue) — `use_pool` в
этом случае эффективно игнорируется `BrowserFetcher` (`proxy_provider is
None` env-fallback, см. `browser_fetcher.py::_pool_proxy`).
`proxy_provider=None` (дефолт) валидно, но у providers больше не встречается:
после #3382 (cian newbuilding) и #3389 (yandex newbuilding) провайдер передают
ВСЕ call-site'ы providers (avito/cian/domclick/yandex, serp и newbuilding).
Без пула остались только прямые конструкции `BrowserFetcher(...)` мимо этой
фабрики служебные ручки `app/api/v1/admin.py` (511 cian-login, 702
domclick-detail-debug); прямые конструкции в `orchestration/pipeline.py` и в
backfill-задачах пул получают. `use_pool` при `proxy_provider is None` эффективно
игнорируется `BrowserFetcher` (env-fallback, см. `browser_fetcher.py::_pool_proxy`).
`fetch_timeout_s=None` (дефолт) используется дефолт `BrowserFetcher`
(120s). Yandex-провайдер передаёt здесь 30s явно (см. вызовы в
`yandex/serp.py`/`yandex/newbuilding.py`) единственный provider с
отличным от дефолта таймаутом.
(120s). Явный таймаут передаёт ровно один call-site `yandex/serp.py` (30s);
`yandex/newbuilding.py` идёт на дефолтных 120s.
`environment=getattr(config, "environment", "dev")` (#2616 шаг 1) — прокидывается в
`BrowserFetcher._pool_proxy`: пул пуст/сломан + прод отказ вместо мёртвого