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 до ненуля. # исход — назвать прогон неуспешным, а не дотянуть succeeded до ненуля.
processed = int(counters.get("processed") or 0) processed = int(counters.get("processed") or 0)
succeeded = int(counters.get("succeeded") or 0) succeeded = int(counters.get("succeeded") or 0)
if counters.get("no_proxy_stop"): if result.no_proxy_stop:
# #3197: прогон оборван на пустом пуле — к площадке не ходили вовсе. # #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: пул прокси пуст — прогон оборван " "yandex_newbuilding_sweep run_id=%d: пул прокси пуст — прогон оборван "
"(обработано %d)", "(обработано %d)",
run_id, run_id,

View file

@ -85,7 +85,15 @@ class YandexNewbuildingSweepResult:
duration_sec: float = field(default=0.0) duration_sec: float = field(default=0.0)
def to_dict(self) -> dict[str, int | float]: 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 ──────────────────────────────────────────────────────────────────────── # ── SQL ────────────────────────────────────────────────────────────────────────

View file

@ -27,18 +27,21 @@ import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from app.services.product_handlers import _job_yandex_newbuilding_sweep from app.services.product_handlers import _job_yandex_newbuilding_sweep
from app.tasks.yandex_newbuilding_sweep import YandexNewbuildingSweepResult
def _ctx() -> SimpleNamespace: def _ctx() -> SimpleNamespace:
return SimpleNamespace(runs=MagicMock()) return SimpleNamespace(runs=MagicMock())
def _result(processed: int, succeeded: int) -> MagicMock: def _result(processed: int, succeeded: int) -> YandexNewbuildingSweepResult:
r = MagicMock() """Настоящий результат задачи, а не MagicMock.
r.to_dict = MagicMock(
return_value={"processed": processed, "succeeded": succeeded, "total": 425} У MagicMock любой не заданный атрибут (`no_proxy_stop` из #3197) — truthy, то есть
) добавление хендлеру новой ветки по полю результата уводило бы сюда ВСЕ три случая,
return r и тест краснел бы по причине, не имеющей отношения к #2860.
"""
return YandexNewbuildingSweepResult(processed=processed, succeeded=succeeded, total=425)
@pytest.mark.asyncio @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 _db, run_id, reason, counters = ctx.runs.mark_failed.call_args.args
assert run_id == 3197 assert run_id == 3197
assert "пул" in reason 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` невозможно на уровне сигнатуры (не полагаемся на `ScraperConfig` невозможно на уровне сигнатуры (не полагаемся на
code-review чтобы поймать пропущенный параметр). code-review чтобы поймать пропущенный параметр).
`proxy_provider=None` (дефолт) валидно: часть providers (avito legacy `proxy_provider=None` (дефолт) валидно, но у providers больше не встречается:
curl-only путь, domclick, cian/yandex newbuilding) пока не подключены к после #3382 (cian newbuilding) и #3389 (yandex newbuilding) провайдер передают
browser-proxy-пулу (#2160 P4 wiring gap, отдельный issue) — `use_pool` в ВСЕ call-site'ы providers (avito/cian/domclick/yandex, serp и newbuilding).
этом случае эффективно игнорируется `BrowserFetcher` (`proxy_provider is Без пула остались только прямые конструкции `BrowserFetcher(...)` мимо этой
None` env-fallback, см. `browser_fetcher.py::_pool_proxy`). фабрики служебные ручки `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` `fetch_timeout_s=None` (дефолт) используется дефолт `BrowserFetcher`
(120s). Yandex-провайдер передаёt здесь 30s явно (см. вызовы в (120s). Явный таймаут передаёт ровно один call-site `yandex/serp.py` (30s);
`yandex/serp.py`/`yandex/newbuilding.py`) единственный provider с `yandex/newbuilding.py` идёт на дефолтных 120s.
отличным от дефолта таймаутом.
`environment=getattr(config, "environment", "dev")` (#2616 шаг 1) — прокидывается в `environment=getattr(config, "environment", "dev")` (#2616 шаг 1) — прокидывается в
`BrowserFetcher._pool_proxy`: пул пуст/сломан + прод отказ вместо мёртвого `BrowserFetcher._pool_proxy`: пул пуст/сломан + прод отказ вместо мёртвого