[tradein][scrapers] avito: changeip-recovery одношотный (30s timeout роняет run) + статус 'banned' завышен когда SERP отработал #1950

Closed
opened 2026-06-27 07:47:03 +00:00 by lekss361 · 6 comments
Owner

Из диагностики 2026-06-27 (живой репро). avito по факту healthy (SERP собирает), но интермиттентно красится как banned.

Корни (подтверждены живым репро)

1. detail-recovery ротация IP одношотная → единичный сбой changeip роняет весь run. Точка отказа run 418 (banned 06:37): anchor #2 detail-фаза, 3 подряд Avito detail firewall (browser-mode) → recovery _rotate_proxy_ip(source=avito) rotation #2 упала: curl (28) Operation timed out after 30000ms with 0 bytes received (scrape_pipeline.py:86, await rot.get(rotate_url) AsyncSession timeout=30, без retry/fallback) → вернула False → consecutive остался 3 → detail ABORT → mark_banned (:1190). Rotation #1 (06:54) при этом успела. Прокси/changeip при ручной проверке здоровы (200 за 2.5s, IP сменился 94.25.229.1→.228.108). Бан транзиентный: те же SERP+detail URL + тот же прокси через 13 мин → HTTP 200, firewall_markers=[].

2. mark_banned красит ВЕСЬ run, хотя SERP-интейк отработал — run 418 сохранил lots_inserted=99 + lots_updated=152 + detail_enriched=27 при status=banned. Лейбл завышает severity, маскирует что листинги собираются, путает мониторинг.

DoD (направление)

  • changeip-resilience в _rotate_proxy_ip (scrape_pipeline.py:56-112): на curl-28/timeout — retry с коротким timeout (5-8s) ×2 и/или fallback на _reset_cffi/новый backconnect-туннель (свежий exit-IP через mproxy.site) вместо одношотного 30s-GET. Чтобы единичный stall changeip не ронял run.
  • honest status: когда SERP отработал (lots сохранены) а упал только detail/house enrichment → статус partial/done, НЕ banned. 99 inserted не должны жить под banned.
  • (отдельно, likely) avito house-catalog парсер не находит window.__preloadedState__ на /catalog/houses/* (houses_enriched=0/16, ×10 в логах run 418) — возможен редизайн; + full_load периодически sidecar-500 /fetch (runs 356/335/315/263). — кандидаты в отдельный тикет.

NB: ротация в avito ЕСТЬ в обеих фазах (SERP слой A _rotate_ip бюджет 4; detail слой B #1790) — гэп НЕ в отсутствии ротации, а в её хрупкости (one-shot changeip). UniqueViolation-фикс (#1921) live, подтверждён (run 418 без него).

scope/backend, scrapers, tradein, priority/p2. Refs аудит 2026-06-27, #1790, #1848.

Из диагностики 2026-06-27 (живой репро). avito по факту **healthy** (SERP собирает), но интермиттентно красится как `banned`. ## Корни (подтверждены живым репро) **1. detail-recovery ротация IP одношотная → единичный сбой changeip роняет весь run.** Точка отказа run 418 (banned 06:37): anchor #2 detail-фаза, 3 подряд `Avito detail firewall (browser-mode)` → recovery `_rotate_proxy_ip(source=avito)` rotation #2 упала: `curl (28) Operation timed out after 30000ms with 0 bytes received` (scrape_pipeline.py:86, `await rot.get(rotate_url)` AsyncSession timeout=30, без retry/fallback) → вернула False → consecutive остался 3 → `detail ABORT` → mark_banned (:1190). Rotation #1 (06:54) при этом успела. Прокси/changeip при ручной проверке здоровы (200 за 2.5s, IP сменился 94.25.229.1→.228.108). Бан **транзиентный**: те же SERP+detail URL + тот же прокси через 13 мин → HTTP 200, firewall_markers=[]. **2. mark_banned красит ВЕСЬ run, хотя SERP-интейк отработал** — run 418 сохранил `lots_inserted=99 + lots_updated=152 + detail_enriched=27` при status=banned. Лейбл завышает severity, маскирует что листинги собираются, путает мониторинг. ## DoD (направление) - **changeip-resilience** в `_rotate_proxy_ip` (scrape_pipeline.py:56-112): на `curl-28`/timeout — retry с коротким timeout (5-8s) ×2 и/или fallback на `_reset_cffi`/новый backconnect-туннель (свежий exit-IP через mproxy.site) вместо одношотного 30s-GET. Чтобы единичный stall changeip не ронял run. - **honest status**: когда SERP отработал (lots сохранены) а упал только detail/house enrichment → статус `partial`/`done`, НЕ `banned`. 99 inserted не должны жить под `banned`. - (отдельно, likely) avito house-catalog парсер не находит `window.__preloadedState__` на `/catalog/houses/*` (houses_enriched=0/16, ×10 в логах run 418) — возможен редизайн; + full_load периодически sidecar-500 `/fetch` (runs 356/335/315/263). — кандидаты в отдельный тикет. NB: ротация в avito ЕСТЬ в обеих фазах (SERP слой A `_rotate_ip` бюджет 4; detail слой B #1790) — гэп НЕ в отсутствии ротации, а в её хрупкости (one-shot changeip). UniqueViolation-фикс (#1921) live, подтверждён (run 418 без него). scope/backend, scrapers, tradein, priority/p2. Refs аудит 2026-06-27, #1790, #1848.
lekss361 added the
priority/p2
scope/backend
scrapers
tradein
labels 2026-06-27 08:51:21 +00:00
Author
Owner

Дополнение (2026-06-27, после деплоя 757f6486): changeip-resilience + honest-status уже в проде ✓. Но у detail-backfill всплыл новый failure-mode → zombie-hang:

  • run 423 (avito_detail_backfill, сегодня) → status zombie: started 09:19, reaped 17:01 = 7.7ч (против healthy ~2.4ч у run 397/377). Backlog не уменьшился: 21 456 avito ждут detail (29.3% enriched).

Корень (code-review avito_detail_backfill.py): fetch_detail (стр.215) в loop'е без hard-timeout. Один зависший fetch (camoufox/browser hang или curl-stall) блокирует loop навсегда → budget-guard (стр.190, проверяется только в начале итерации) не срабатывает → heartbeat (стр.261, каждые 25 attempts) не обновляется → reap_zombies красит весь run zombie. Вторично: на блок зовётся scraper._rotate_ip() (стр.236), а не #1950-resilient _rotate_proxy_ip — ещё один потенциальный hang.

Фикс (PR следом, Refs #1950): обернуть fetch_detail в asyncio.wait_for(timeout=avito_detail_fetch_timeout_s≈90s) → TimeoutError → counters.failed++ + continue (один зависший листинг не роняет run); bound rotate в wait_for; admin-триггер для on-demand прогона/верификации. Восстанавливает throughput detail-очереди → точные координаты (#1967) + house_type/kitchen для avito.

Дополнение (2026-06-27, после деплоя 757f6486): changeip-resilience + honest-status уже в проде ✓. Но у detail-backfill всплыл **новый failure-mode → zombie-hang**: - run **423** (`avito_detail_backfill`, сегодня) → status **zombie**: started 09:19, reaped 17:01 = **7.7ч** (против healthy ~2.4ч у run 397/377). Backlog не уменьшился: **21 456** avito ждут detail (29.3% enriched). **Корень** (code-review `avito_detail_backfill.py`): `fetch_detail` (стр.215) в loop'е **без hard-timeout**. Один зависший fetch (camoufox/browser hang или curl-stall) блокирует loop навсегда → budget-guard (стр.190, проверяется только в начале итерации) не срабатывает → heartbeat (стр.261, каждые 25 attempts) не обновляется → `reap_zombies` красит весь run zombie. Вторично: на блок зовётся `scraper._rotate_ip()` (стр.236), а не #1950-resilient `_rotate_proxy_ip` — ещё один потенциальный hang. **Фикс (PR следом, Refs #1950):** обернуть `fetch_detail` в `asyncio.wait_for(timeout=avito_detail_fetch_timeout_s≈90s)` → TimeoutError → counters.failed++ + continue (один зависший листинг не роняет run); bound rotate в wait_for; admin-триггер для on-demand прогона/верификации. Восстанавливает throughput detail-очереди → точные координаты (#1967) + house_type/kitchen для avito.
Author
Owner

Фикс merged (PR #2028) + deployed + verified на проде.

Прогон через новый admin-триггер (POST /scrape/avito-detail-backfill?batch_size=25&budget_sec=200, вне 09-12 окна):

  • run 430 → status done (НЕ zombie): attempted=23, enriched=18, failed=5, завершился по бюджету чисто.
  • Из свежеобогащённых 8/8 получили точные координаты (lat/lon not null) — подтверждает, что detail-стадия = building-level coords (рычаг #1967) + house_type/kitchen.
  • Контраст: run 423 (зомби) успел attempted=1000/enriched=432 и завис на heartbeat 11:00 → reaped 17:01. Теперь зависший fetch ловится asyncio.wait_for(90s) → failed+continue, run не виснет.

Scope changeip-resilience + honest-status + zombie-hang — закрыт. Остаточный отдельный пункт из тела issue (avito house-catalog парсер не находит window.__preloadedState__, houses_enriched=0; full_load sidecar-500) — это иной баг, кандидат в свой тикет. Можно закрывать #1950 по основному скоупу или оставить под house-catalog хвост.

✅ **Фикс merged (PR #2028) + deployed + verified на проде.** Прогон через новый admin-триггер (`POST /scrape/avito-detail-backfill?batch_size=25&budget_sec=200`, вне 09-12 окна): - **run 430 → status `done`** (НЕ zombie): attempted=23, enriched=18, failed=5, завершился по бюджету чисто. - Из свежеобогащённых **8/8 получили точные координаты** (`lat/lon` not null) — подтверждает, что detail-стадия = building-level coords (рычаг #1967) + house_type/kitchen. - Контраст: run 423 (зомби) успел attempted=1000/enriched=432 и завис на heartbeat 11:00 → reaped 17:01. Теперь зависший fetch ловится `asyncio.wait_for(90s)` → failed+continue, run не виснет. **Scope changeip-resilience + honest-status + zombie-hang — закрыт.** Остаточный отдельный пункт из тела issue (avito house-catalog парсер не находит `window.__preloadedState__`, houses_enriched=0; full_load sidecar-500) — это иной баг, кандидат в свой тикет. Можно закрывать #1950 по основному скоупу или оставить под house-catalog хвост.
Author
Owner

Shipped — PR #2034 merged + deployed (08:09 UTC 2026-06-28)

:latest tradein-backend собран 08:08:37 UTC (post-merge, не stale), _is_detail_soft_block live в обоих контейнерах (backend + scraper — где живёт 10:39-run). Полный tradein-suite зелёный (2591 passed), deploy-гейт testbuilddeploy все success.

Root cause (подтверждён живым fetch с прод-прокси)

Под нагрузкой Avito вместо detail-страницы отдаёт HTTP 200 с общей витриной <title>Авито — Объявления на сайте Авито</title> (293 KB, без item-view) — мягкий анти-бот deflect. Старый _is_firewall_page (3 IP-block маркера) её не ловил → parse_detail_html падал ValueError("Cannot extract item_id") → generic failed, blocked=0, IP не ротировался. Теперь _is_detail_soft_block детектит её как блок → reconnect свежей exit-IP / ротация.

Before (baseline — ночные runs, blocked=0 всегда, enriched каскад)

run att enriched blocked failed
431 130 115 0 15
432 149 82 0 67
433 154 8 0 146
434 154 2 0 152
437 146 0 0 146
423 1000 432 0 568

After (post-deploy admin-проба, run 452, batch=80/budget=200s)

attempted=24  enriched=23  blocked=0  failed=1  duration=206s  status=done
  • Yield 95.8% (23/24) против ночных ~0-1.5%.
  • FIX 1 coords-priority подтверждён: из 23 обогащённых 23/23 получили координаты (lat NULL→NOT NULL). Snapshot ORDER BY (lat IS NULL) DESC подал coord-holes первыми → каждый фетч закрыл гео-дыру (#1967). Pending lat-null в /ekaterinburg/-scope: 321 → 298.
  • blocked=0 потому что прокси-IP сейчас здоров — soft-block'ов нет, классифицировать нечего. Это ожидаемо: фикс не фабрикует блоки, а корректно их классифицирует когда они есть.

Pending — сверка по окну 10:39 UTC

Прямое срабатывание soft-block → AvitoBlockedError → ротация (т.е. blocked > 0 + отсутствие каскада enriched→0) проявится только при реальной деградации IP под полной нагрузкой окна. Если в окне 10:39 IP снова деградирует — ждём blocked > 0 и enriched НЕ падающий к 0 (вместо ночного 0/146). Замер по итогам окна — следующим комментом.

NB по scope #1967: из 13 832 active avito с detail NULL + lat NULL в snapshot-scope попадает лишь 321 (/ekaterinburg/-URL); 13 511 — non-EKB URL (legacy moskva/spb/tyumen), намеренно вне scope #1814. FIX 1 ускоряет эти 321; bulk-gap — отдельный вопрос расширения scope (рискован, #1814 его избегает).

## Shipped — PR #2034 merged + deployed (08:09 UTC 2026-06-28) `:latest` tradein-backend собран 08:08:37 UTC (post-merge, не stale), `_is_detail_soft_block` live в обоих контейнерах (backend + **scraper** — где живёт 10:39-run). Полный tradein-suite зелёный (2591 passed), deploy-гейт `test`→`build`→`deploy` все success. ### Root cause (подтверждён живым fetch с прод-прокси) Под нагрузкой Avito вместо detail-страницы отдаёт **HTTP 200** с общей витриной `<title>Авито — Объявления на сайте Авито</title>` (293 KB, без `item-view`) — мягкий анти-бот deflect. Старый `_is_firewall_page` (3 IP-block маркера) её не ловил → `parse_detail_html` падал `ValueError("Cannot extract item_id")` → generic `failed`, `blocked=0`, IP не ротировался. Теперь `_is_detail_soft_block` детектит её как блок → reconnect свежей exit-IP / ротация. ### Before (baseline — ночные runs, `blocked=0` всегда, enriched каскад) | run | att | enriched | blocked | failed | |----|----|----|----|----| | 431 | 130 | 115 | 0 | 15 | | 432 | 149 | 82 | 0 | 67 | | 433 | 154 | 8 | 0 | 146 | | 434 | 154 | 2 | 0 | 152 | | 437 | 146 | **0** | 0 | 146 | | 423 | 1000 | 432 | 0 | 568 | (zombie — хенг #1950, уже починен) ### After (post-deploy admin-проба, run 452, batch=80/budget=200s) ``` attempted=24 enriched=23 blocked=0 failed=1 duration=206s status=done ``` - **Yield 95.8%** (23/24) против ночных ~0-1.5%. - **FIX 1 coords-priority подтверждён:** из 23 обогащённых **23/23 получили координаты** (lat NULL→NOT NULL). Snapshot `ORDER BY (lat IS NULL) DESC` подал coord-holes первыми → каждый фетч закрыл гео-дыру (#1967). Pending lat-null в /ekaterinburg/-scope: 321 → **298**. - `blocked=0` потому что прокси-IP сейчас здоров — soft-block'ов нет, классифицировать нечего. Это ожидаемо: фикс не фабрикует блоки, а корректно их классифицирует **когда они есть**. ### Pending — сверка по окну 10:39 UTC Прямое срабатывание `soft-block → AvitoBlockedError → ротация` (т.е. `blocked > 0` + отсутствие каскада enriched→0) проявится только при реальной деградации IP под полной нагрузкой окна. Если в окне 10:39 IP снова деградирует — ждём `blocked > 0` и enriched НЕ падающий к 0 (вместо ночного 0/146). Замер по итогам окна — следующим комментом. NB по scope #1967: из 13 832 active avito с `detail NULL + lat NULL` в snapshot-scope попадает лишь **321** (`/ekaterinburg/`-URL); **13 511 — non-EKB URL** (legacy moskva/spb/tyumen), намеренно вне scope #1814. FIX 1 ускоряет эти 321; bulk-gap — отдельный вопрос расширения scope (рискован, #1814 его избегает).
Author
Owner

Верификация PR #2034 (#1950) — прод-замер после деплоя

PR смержен 2026-06-28 08:06 UTC, задеплоен 08:09 (рестарт scheduler в логах 08:09:03). Замер снят форсированным прогоном (next_run_at=now) сразу после admin-probe 452 — back-to-back на том же exit-IP. Это сознательный стресс; ночной auto-run 10:39 отменён (next_run сдвинут на завтра), т.к. форс-ран уже дал измерение. Держим это в уме при чтении цифр.

Прогоны avito_detail_backfill (attempted / enriched / blocked / failed)

run время (UTC) attempted enriched blocked failed прим.
431 06-27 20:39 130 115 0 15 начало каскада
433 06-27 21:21 154 8 0 146 деградация
434 06-27 21:43 154 2 0 152 почти всё в failed
437 06-27 22:26 146 0 0 146 терминал ночи: enriched=0, blocked=0
452 06-28 08:11 24 23 0 1 здоровый probe после деплоя
453 06-28 08:45 24 16 5 3 после фикса, форс-ран, 302.7s

Ключевое: blocked был 0 в КАЖДОМ прогоне за всю историю; run 453 — первый ненулевой (blocked=5).

FIX 2 (soft-block → reconnect/rotation) — задеплоен и сработал; атрибуция с оговоркой

Доказано:

  • Новый путь живой: в логе run 453 — изменённая PR-строка avito detail 403/firewall/soft-block <url> — backconnect reconnect retry K/5 (K=1..5 по каждому из 5 заблок. URL — каждый исчерпал все 5 ротаций exit-IP).
  • Сработал consecutive-block счётчик (C=1..5) и брейкер: ABORT -- 5 consecutive blocks, IP rate-limited.
  • Это первый прогон, где blocked>0 + отработали ротация exit-IP и ABORT, тогда как 433/434/437 валили по 146–152 запроса при blocked=0. Исторический паттерн (blocked=0 при сотнях failed) показывает, что старая firewall-детекция не ловила анти-бот страницы Avito, и новое поведение согласуется с тем, что soft-block классификатор их теперь ловит.

Честная оговорка (не выкидываем):

  • Run 453 — confounded одиночная выборка: n=24 vs ночных ~150, форс back-to-back на том же IP, что сам мог вызвать реальный rate-limit.
  • Логи не различают 200-soft-block и настоящий sc==403: HTTP 403 в строке BLOCKED — generic AvitoBlockedError; метка 403/firewall/soft-block — общая строка shared reconnect-рутины (доказывает, что ветка отработала, не какое плечо матчнулось).
  • Ночной silent-failure паттерн под новым кодом не воспроизведён → конверсия failed → blocked пока выведена логически, а не наблюдена.

Итог FIX 2: фикс задеплоен и живой, давний silent-failure gap теперь всплывает как blocks; но связать именно эти 5 блоков с soft-block путём пока нельзя — нужен прогон на ночном каденсе/здоровом IP для подтверждения.

FIX 1 (coords-priority ORDER BY, #1967) — подтверждён замером (не no-op)

  • Реализация корректна: новый тест проверяет (lat IS NULL) DESC в snapshot-SQL.
  • Знаменатель снят (резолвит вопрос «не вырождается ли сортировка в no-op»): in-scope pending (detail_enriched_at IS NULL, active, /ekaterinburg/) = 5169, из них lat-null — лишь 273 (5.3%), lat-present — 4896. Coord-holes — явное меньшинство, и старый ORDER BY scraped_at DESC размазал бы их по всему пулу.
  • При этом 16/16 обогащённых в окне run 453 оказались lat-null. При доле 5.3% случайный батч из ~24 содержал бы ~1 hole, не 16 → 16/16 случайно ≈ невозможно (0.053^16). Значит ordering демонстрируемо двигает coord-holes в начало батча.
  • pending_nolat дренаж монотонный: 321 → 298 → 281 → 273.

Итог FIX 1: coords-priority ordering работает как задумано — гарантированно ставит coord-holes первыми (это подтверждено знаменателем + 16/16, а не просто «согласуется»).

Net-оценка + брейкер

Circuit-breaker (ABORT на attempted=24 при 5 consecutive) — корректное поведение и улучшение, не регресс: run 453 дал 16/24 (66.7% полезного выхода) и остановился, против терминального run 437 (146 attempted, 0 enriched, 0% — долбил мёртвый IP). Per-request новое поведение строго доминирует старый grind и защищает exit-IP. Брейкер требует устойчивого отказа (5 подряд, каждый уже исчерпал ~25 reconnect-ротаций) → transient-блип здоровый прогон не оборвёт.

Follow-up (caveats, не регрессы):

  1. Throughput теперь capped (~16–24/run vs ~150): слив бэклога (273 coord-hole из 5169 in-scope) зависит от того, отдаёт ли backconnect-пул свежие здоровые IP между прогонами. Стоит алерт, если N прогонов подряд abort-ят рано без снижения pending_nolat.
  2. Новая поверхность false-positive: soft-block OR-нут в is_firewall — если Avito уберёт маркеры item-view/item-id / item-view/title-info, здоровые 200-страницы с «объявления на сайте авито» могут массово abort-иться. Мониторить hit-rate soft-block.
  3. blocked=5 в run 453 частично — артефакт same-IP стресса, вероятно завышает steady-state долю блоков.

Тесты

tests/scrapers/test_avito_detail_soft_block.py (20 passed, ruff-100) подтверждают сам путь независимо от прод-IP: soft-block→reconnect→успех; исчерпание→AvitoBlockedError (build.call_count==5); классификатор generic-витрина=True / item-view=False; (lat IS NULL) DESC в SQL. То, что прод-выборка из-за same-IP стресса подтвердить не может, закрыто юнит-тестами.

## Верификация PR #2034 (#1950) — прод-замер после деплоя PR смержен 2026-06-28 08:06 UTC, задеплоен 08:09 (рестарт scheduler в логах 08:09:03). Замер снят **форсированным прогоном** (`next_run_at=now`) сразу после admin-probe 452 — back-to-back на **том же exit-IP**. Это сознательный стресс; ночной auto-run 10:39 отменён (next_run сдвинут на завтра), т.к. форс-ран уже дал измерение. Держим это в уме при чтении цифр. ### Прогоны `avito_detail_backfill` (attempted / enriched / **blocked** / failed) | run | время (UTC) | attempted | enriched | blocked | failed | прим. | |----|----|----|----|----|----|----| | 431 | 06-27 20:39 | 130 | 115 | **0** | 15 | начало каскада | | 433 | 06-27 21:21 | 154 | 8 | **0** | 146 | деградация | | 434 | 06-27 21:43 | 154 | 2 | **0** | 152 | почти всё в failed | | 437 | 06-27 22:26 | 146 | 0 | **0** | 146 | терминал ночи: enriched=0, blocked=0 | | 452 | 06-28 08:11 | 24 | 23 | **0** | 1 | здоровый probe после деплоя | | **453** | **06-28 08:45** | **24** | **16** | **5** | **3** | **после фикса**, форс-ран, 302.7s | Ключевое: **blocked был 0 в КАЖДОМ прогоне за всю историю**; run 453 — первый ненулевой (blocked=5). ### FIX 2 (soft-block → reconnect/rotation) — задеплоен и сработал; атрибуция с оговоркой **Доказано:** - Новый путь живой: в логе run 453 — изменённая PR-строка `avito detail 403/firewall/soft-block <url> — backconnect reconnect retry K/5` (K=1..5 по каждому из 5 заблок. URL — каждый исчерпал все 5 ротаций exit-IP). - Сработал consecutive-block счётчик (C=1..5) и брейкер: `ABORT -- 5 consecutive blocks, IP rate-limited`. - Это **первый прогон**, где blocked>0 + отработали ротация exit-IP и ABORT, тогда как 433/434/437 валили по 146–152 запроса при blocked=0. Исторический паттерн (blocked=0 при сотнях failed) показывает, что **старая firewall-детекция не ловила анти-бот страницы Avito**, и новое поведение **согласуется** с тем, что soft-block классификатор их теперь ловит. **Честная оговорка (не выкидываем):** - Run 453 — confounded одиночная выборка: n=24 vs ночных ~150, форс back-to-back на том же IP, что сам мог вызвать реальный rate-limit. - Логи **не различают** 200-soft-block и настоящий `sc==403`: `HTTP 403` в строке BLOCKED — generic `AvitoBlockedError`; метка `403/firewall/soft-block` — общая строка shared reconnect-рутины (доказывает, что ветка отработала, не какое плечо матчнулось). - Ночной silent-failure паттерн под новым кодом не воспроизведён → конверсия `failed → blocked` пока **выведена логически, а не наблюдена**. Итог FIX 2: фикс **задеплоен и живой**, давний silent-failure gap теперь всплывает как blocks; но связать именно эти 5 блоков с soft-block путём пока нельзя — нужен прогон на ночном каденсе/здоровом IP для подтверждения. ### FIX 1 (coords-priority ORDER BY, #1967) — подтверждён замером (не no-op) - Реализация корректна: новый тест проверяет `(lat IS NULL) DESC` в snapshot-SQL. - **Знаменатель снят** (резолвит вопрос «не вырождается ли сортировка в no-op»): in-scope pending (`detail_enriched_at IS NULL`, active, `/ekaterinburg/`) = **5169**, из них lat-null — лишь **273 (5.3%)**, lat-present — 4896. Coord-holes — явное меньшинство, и старый `ORDER BY scraped_at DESC` размазал бы их по всему пулу. - При этом **16/16** обогащённых в окне run 453 оказались lat-null. При доле 5.3% случайный батч из ~24 содержал бы ~1 hole, не 16 → 16/16 случайно ≈ невозможно (`0.053^16`). Значит ordering **демонстрируемо двигает coord-holes в начало** батча. - `pending_nolat` дренаж монотонный: 321 → 298 → 281 → 273. Итог FIX 1: coords-priority ordering **работает как задумано** — гарантированно ставит coord-holes первыми (это подтверждено знаменателем + 16/16, а не просто «согласуется»). ### Net-оценка + брейкер Circuit-breaker (ABORT на attempted=24 при 5 consecutive) — **корректное поведение и улучшение, не регресс**: run 453 дал 16/24 (66.7% полезного выхода) и остановился, против терминального run 437 (146 attempted, **0** enriched, 0% — долбил мёртвый IP). Per-request новое поведение строго доминирует старый grind и защищает exit-IP. Брейкер требует устойчивого отказа (5 подряд, каждый уже исчерпал ~25 reconnect-ротаций) → transient-блип здоровый прогон не оборвёт. **Follow-up (caveats, не регрессы):** 1. **Throughput теперь capped** (~16–24/run vs ~150): слив бэклога (273 coord-hole из 5169 in-scope) зависит от того, отдаёт ли backconnect-пул свежие здоровые IP между прогонами. Стоит **алерт**, если N прогонов подряд abort-ят рано без снижения `pending_nolat`. 2. **Новая поверхность false-positive**: soft-block OR-нут в `is_firewall` — если Avito уберёт маркеры `item-view/item-id` / `item-view/title-info`, здоровые 200-страницы с «объявления на сайте авито» могут массово abort-иться. Мониторить hit-rate soft-block. 3. blocked=5 в run 453 частично — артефакт same-IP стресса, вероятно завышает steady-state долю блоков. ### Тесты `tests/scrapers/test_avito_detail_soft_block.py` (20 passed, ruff-100) подтверждают **сам путь** независимо от прод-IP: soft-block→reconnect→успех; исчерпание→`AvitoBlockedError` (`build.call_count==5`); классификатор generic-витрина=True / item-view=False; `(lat IS NULL) DESC` в SQL. То, что прод-выборка из-за same-IP стресса подтвердить не может, закрыто юнит-тестами.
Author
Owner

Working on this (автономный проход по backlog) — #1949 (тот же файл scrape_pipeline.py) уже смержен, безопасно стартовать. Ветка fix/tradein-avito-changeip-resilience.

Working on this (автономный проход по backlog) — #1949 (тот же файл scrape_pipeline.py) уже смержен, безопасно стартовать. Ветка `fix/tradein-avito-changeip-resilience`.
Author
Owner

Уже полностью закрыт коммитом 757f6486 "fix(scrapers): avito changeip-resilience (retry короткими попытками) + честный статус когда SERP отработал (#1950)" (2026-06-27), до старта этого прохода по backlog.

Подтверждено живьём: _rotate_proxy_ip() — retry-loop 3×8s (вместо one-shot 30s); run_avito_city_sweep()avito_serp_ok_not_banned гейтит mark_done/mark_banned по факту lots_inserted+updated>0 (кстати, #1949's _mark_cian_sweep_abort зеркалил именно этот паттерн, а не наоборот). 13/13 regression-тестов зелёные, ruff чисто.

Небольшой смежный gap, НЕ в скоупе этого issue (детали в vault, не открываю отдельный issue сейчас): run_avito_full_load (SERP-only price-bucket crawl, без detail-фазы) безусловно ставит mark_banned даже если часть бакетов уже сохранена через on_bucket — структурно похоже, но это issue описывал именно detail-фазу. Если понадобится — отдельная маленькая задача.

Закрываю.

Уже полностью закрыт коммитом `757f6486` "fix(scrapers): avito changeip-resilience (retry короткими попытками) + честный статус когда SERP отработал (#1950)" (2026-06-27), до старта этого прохода по backlog. Подтверждено живьём: `_rotate_proxy_ip()` — retry-loop 3×8s (вместо one-shot 30s); `run_avito_city_sweep()` — `avito_serp_ok_not_banned` гейтит mark_done/mark_banned по факту lots_inserted+updated>0 (кстати, `#1949`'s `_mark_cian_sweep_abort` зеркалил именно этот паттерн, а не наоборот). 13/13 regression-тестов зелёные, ruff чисто. Небольшой смежный gap, НЕ в скоупе этого issue (детали в vault, не открываю отдельный issue сейчас): `run_avito_full_load` (SERP-only price-bucket crawl, без detail-фазы) безусловно ставит `mark_banned` даже если часть бакетов уже сохранена через `on_bucket` — структурно похоже, но это issue описывал именно detail-фазу. Если понадобится — отдельная маленькая задача. Закрываю.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1950
No description provided.