Авито: добор карточек больше не сжигает 1600 попыток на упавшем сайдкаре и везде ходит браузером #3560

Merged
bot-backend merged 4 commits from fix/avito-detail-backfill into main 2026-09-17 09:23:22 +00:00
Collaborator

Четыре коммита, по одному на задачу. Главное: добор карточек Авито перестаёт впустую тратить 1600 попыток, когда отказывает наш сайдкар. Кроме того, добор теперь ходит браузером во всех процессах, которые его запускают. Миграций нет.

#3439: серия отказов нашего тракта не обрывала добор

Что было. Прогоны 7193, 7218, 7264 и 7302 (15–16.09), а также 6608, 6631, 6643 и 6662 (10.09): в каждом 1600 попыток, обогащено от 1 до 10 карточек, около 1599 блоков, статус done, длительность 70+ минут. В окне 7302 (16.09 22:45–00:00 UTC) в логах tradein-browser 3258 строк InvalidIP и 6380 строк с 503 (проверено docker logs --since/--until). У прогона 6885 (13.09) те же 1600 попыток при ban_kinds {"infra": 1591}, но статус banned. Всего за 14 суток 17 таких прогонов дали 25 668 попыток из 29 498 (87 %) и 55 карточек. Остальные 64 прогона за 3830 попыток обогатили 746 карточек.

Почему. Это регресс #3288/#3367. record_block(kind != platform) уходил в record_failure(), то есть только в знаменатель окна. Доля считает лишь platform, safety-net смотрит на серию platform, а счётчика серии infra не было вовсе. Серия отказов сайдкара любой длины ничем не обрывала прогон, а финализатор #3367 при одной обогащённой карточке ставил done.

Что сделано.

  • В BlockRatioBreaker добавлены infra_streak_limit и счётчик серии infra. Серию обнуляет любой ответ площадки: успех, её блок или 404. abort_reason() возвращает "infra_streak" и проверяет этот критерий первым.
  • В avito_detail_backfill порог равен max_consecutive_failures (по умолчанию 25). Это тот же разряд «отказ нашей стороны», что и отказы-не-блоки. При обрыве вызывается mark_failed с причиной «тракт недоступен: N отказов нашего тракта (сайдкар/пул) подряд без единого ответа площадки…», в counters пишутся abort_reason=infra_streak и ban_kinds. Статус не banned, потому что площадка молчала. Финал идёт мимо mark_backfill_finished, иначе одна карточка снова превращает шторм в done.

Не сделано. Пункт 3 задачи не сделан: вердикт browser_unfit узлу, на котором сайдкар не поднял браузер. Это вердикт пула прокси и протокол сайдкара, нужна отдельная задача. На 17.09 у узла 15 browser_fail_streak=0 и browser_unfit_since=NULL. Поэтому Closes для #3439 не ставлю.

#3288: диагноз блоков был только у banned-прогонов

Что было. Пункт 5 из комментария от 05.09. mark_backfill_finished кладёт counters.ban_kinds только на ветке бана. У 7193, 7264 и 7302 (done) диагноза в БД нет. Что это были отказы сайдкара, видно лишь косвенно: block_streak_histogram считает только серии platform.

Что сделано. Задача сама кладёт counters.ban_kinds перед любым финализатором. Статусы и колонка ban_kind не тронуты, контракты #3196 и #2764 остались как были. Стоп-кран для серии infra, вторая половина пункта 5, сделан в коммите #3439.

Что осталось открытым, поэтому без Closes:

  • решение, должны ли нулевые infra-прогоны без серии оставаться banned;
  • выравнивание cian (его финализирует scheduler.py).

Пункт 5 сам сформулирован как «решение, не код».

#3045: ручной добор через админку шёл curl'ом

Что было. Шаг 3 сделали только через environment сервиса scraper, а в config.py осталось default=True. При этом добор исполняет и tradein-backend: ручка POST /scrape/avito-detail-backfill вызывает run_avito_detail_backfill в своём процессе. Комментарий «true только у tradein-backend, который добор не запускает» был неверен.

Что сделано. Дефолт изменён на False, исправлены три комментария, называвшие curl «прод-дефолтом». Для прода нужен ещё оверрайд в compose: хостовый .env.runtime:37 задаёт true явно. Он в коммите #3182.

Шаг 4 (нагрузка), замер по scrape_runs за 14 суток. Рабочие прогоны браузером: в среднем 60 попыток за 15 минут, 236 попыток в час, 64 прогона. Узлы: scrape_runs.proxy_id = 13, 14 и 15. Все три — mobileproxy (Tele2 и МегаФон). ASOCKS-узел (id=1) привязан к yandex, avito-добор через него не ходил ни разу. Упереться в лимиты ASOCKS этот путь не может. Основной расход давали шторма сайдкара (87 % попыток), их обрывает #3439.

#3182: разные значения флага в контейнерах

Что было. printenv на проде 17.09:

Контейнер AVITO_DETAIL_BACKFILL_USE_CURL
tradein-scraper false
tradein-backend true
tradein-tgbot true
tradein-browser true

true приходит из хостового backend/.env.runtime:37. YANDEX_COOKIES_FILE=/app/yandex_cookies.json задан в .env.runtime:17, но файла нет ни в одном контейнере, а поле settings.yandex_cookies_file не читал ни один модуль: был только мок в тесте, куки Яндекса давно хранятся в БД (#3192).

Что сделано.

  • В docker-compose.prod.yml у backend и tgbot задано AVITO_DETAIL_BACKFILL_USE_CURL: "false": у всех сервисов образа backend одно значение. tgbot добор не исполняет, но так правило проверяется без списка исключений.
  • Остальные флаги режима на проде одинаковы во всех четырёх контейнерах: SCRAPER_FETCH_MODE=browser и AVITO_IMV_USE_BROWSER_FETCHER=true.
  • Поле yandex_cookies_file удалено. Настройка extra="ignore" не даёт процессу упасть на старте из-за переменной, которая осталась в .env.runtime.

Тесты

Все прогоны локально (macOS), rc снят с самого pytest.

  • tests/test_3288b_block_kind_ratio.py: 20 passed. Из них новые:

    • обрыв ровно на 25-м отказе сайдкара подряд, на 24-м нет;
    • ответ площадки (успех, блок или 404) обнуляет серию, параметризация на 3 случая;
    • шторм «1 успех + 1599 отказов»: attempted=26, mark_failed, abort_reason=infra_streak, ban_kinds {"infra": 25};
    • 24 infra + ответ площадки ×3: снапшот пройден целиком;
    • 25 % настоящих блоков: обрыва нет;
    • прогон без обрыва несёт ban_kinds в counters.

    Прежний контроль #3367 («20 infra + 10 успехов → done») зелёный.

  • tests/test_3045_use_curl_default.py: 2 passed (Settings без env даёт False; env по-прежнему переключает на curl).

  • tests/test_3182_detail_transport_one_value.py: 2 passed (разобранный compose: у каждого сервиса образа backend "false"; Settings с YANDEX_COOKIES_FILE в env стартует без поля).

  • Соседние тесты avito-добора (tasks/test_avito_detail_backfill, test_3338, test_3384, test_3283g, test_3251, test_backfill_honest_status, test_2764, tasks/test_3283): 98 passed, 1 skipped.

  • Весь сьют: DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider6237 passed, 42 skipped, rc=0 (109 с).

  • uv run ruff check app tests: rc=0. ruff format --check по 8 изменённым файлам: rc=0.

  • packages/scraper-kit: тестов нет (pytest rc=5, no tests ran).

Фальсификация (красный прогон при сломанном фиксе, исходник восстановлен, diff -q чист)

  • #3439, без проводки порога в задаче: AssertionError: attempted=1600 vs исходы=1600 … ожидали 26 / assert (1600, 1600) == (26, 26).
  • #3439, без обнуления серии на блоке площадки: AssertionError: abort_reason='infra_streak': 24 + ответ площадки (platform) + 24 — серии в 25 подряд не было.
  • #3439, без ветки финала: AssertionError: прогон ушёл в backfill-honest-status: там 1 карточка превращает шторм в 'done'.
  • #3288, без строки ban_kinds: assert None == {'infra': 20}.
  • #3045 с default=True: AssertionError: assert True is False.
  • #3182, без строки у backend: {'backend': None} != {'backend': 'false'}; с возвращённым полем: AssertionError: assert not True.

Деплой

  • Меняется код образа backend, поэтому пересоздаются tradein-backend, tradein-scraper (через drain) и tradein-tgbot. tradein-browser не меняется. Перед мержем нужно проверить, что сейчас ничего не бежит: SELECT id, source, started_at FROM scrape_runs WHERE status='running'. Пересоздание scraper убивает работающие прогоны.
  • Миграций нет.
  • На хосте (по желанию, это не требование PR) строки 17 (YANDEX_COOKIES_FILE) и 37 (AVITO_DETAIL_BACKFILL_USE_CURL=true) в /opt/gendesign/tradein-mvp/backend/.env.runtime теперь просто шум. После их удаления printenv в tradein-browser перестанет показывать true.

Приёмка на проде (после деплоя; сроки от даты деплоя)

  1. Сразу после деплоя: docker exec tradein-backend printenv AVITO_DETAIL_BACKFILL_USE_CURL и то же для tradein-tgbot дают false. docker exec tradein-backend python -c 'from app.core.config import settings; print(settings.avito_detail_backfill_use_curl, hasattr(settings, "yandex_cookies_file"))' печатает False False.
  2. В течение 7 суток, первый шторм сайдкара (InvalidIP / /fetch 503 в логах tradein-browser):
    SELECT id, status, counters->>'attempted', counters->>'enriched', counters->>'abort_reason', counters->'ban_kinds'
    FROM scrape_runs WHERE source='avito_detail_backfill' AND started_at > '<деплой>' ORDER BY id DESC;
    
    Ожидание: ни одного прогона с attempted ≥ 500 и enriched/attempted < 1 %. У штормового прогона status='failed', abort_reason='infra_streak', attempted около 25, в ban_kinds доминирует infra. Если шторма за 7 суток не было, приёмка не пройдена и не провалена: переносится на следующее окно с датой.
  3. У любого завершённого прогона с блоками после деплоя (включая done) есть counters->'ban_kinds'.

Не вошло в PR

  • #3035 (?context= в source_url). Автор в комментарии от 23.08 прямо просит не править вслепую: знак эффекта неизвестен, и протухший searchHash может выглядеть подозрительнее, чем его отсутствие. Первый пункт приёмки — A/B-замер на проде (переключение режима и прогоны на одном узле). Это запись на прод и решение владельца. Судьба listings.phones — тоже решение.
  • #3171 (ротация IP). Первый пункт приёмки задача прямо отдаёт владельцу: «ротация тратит квоту у провайдера». Второй — «по строкам видно, помогла ли ротация» — требует договорённости, что считать «помогла» (окно попыток, куда писать: scrape_proxy_rotations.note или отдельная колонка). Вызов ротации уже проведён в #3302/#3313.

Closes #3045
Closes #3182

🤖 Generated with Claude Code

Четыре коммита, по одному на задачу. Главное: добор карточек Авито перестаёт впустую тратить 1600 попыток, когда отказывает наш сайдкар. Кроме того, добор теперь ходит браузером во всех процессах, которые его запускают. Миграций нет. ## #3439: серия отказов нашего тракта не обрывала добор **Что было.** Прогоны 7193, 7218, 7264 и 7302 (15–16.09), а также 6608, 6631, 6643 и 6662 (10.09): в каждом 1600 попыток, обогащено от 1 до 10 карточек, около 1599 блоков, статус `done`, длительность 70+ минут. В окне 7302 (16.09 22:45–00:00 UTC) в логах `tradein-browser` 3258 строк `InvalidIP` и 6380 строк с `503` (проверено `docker logs --since/--until`). У прогона 6885 (13.09) те же 1600 попыток при `ban_kinds {"infra": 1591}`, но статус `banned`. Всего за 14 суток 17 таких прогонов дали 25 668 попыток из 29 498 (87 %) и 55 карточек. Остальные 64 прогона за 3830 попыток обогатили 746 карточек. **Почему.** Это регресс #3288/#3367. `record_block(kind != platform)` уходил в `record_failure()`, то есть только в знаменатель окна. Доля считает лишь platform, safety-net смотрит на серию platform, а счётчика серии infra не было вовсе. Серия отказов сайдкара любой длины ничем не обрывала прогон, а финализатор #3367 при одной обогащённой карточке ставил `done`. **Что сделано.** - В `BlockRatioBreaker` добавлены `infra_streak_limit` и счётчик серии infra. Серию обнуляет любой ответ площадки: успех, её блок или 404. `abort_reason()` возвращает `"infra_streak"` и проверяет этот критерий первым. - В `avito_detail_backfill` порог равен `max_consecutive_failures` (по умолчанию 25). Это тот же разряд «отказ нашей стороны», что и отказы-не-блоки. При обрыве вызывается `mark_failed` с причиной «тракт недоступен: N отказов нашего тракта (сайдкар/пул) подряд без единого ответа площадки…», в counters пишутся `abort_reason=infra_streak` и `ban_kinds`. Статус не `banned`, потому что площадка молчала. Финал идёт мимо `mark_backfill_finished`, иначе одна карточка снова превращает шторм в `done`. **Не сделано.** Пункт 3 задачи не сделан: вердикт `browser_unfit` узлу, на котором сайдкар не поднял браузер. Это вердикт пула прокси и протокол сайдкара, нужна отдельная задача. На 17.09 у узла 15 `browser_fail_streak=0` и `browser_unfit_since=NULL`. Поэтому `Closes` для #3439 не ставлю. ## #3288: диагноз блоков был только у `banned`-прогонов **Что было.** Пункт 5 из комментария от 05.09. `mark_backfill_finished` кладёт `counters.ban_kinds` только на ветке бана. У 7193, 7264 и 7302 (`done`) диагноза в БД нет. Что это были отказы сайдкара, видно лишь косвенно: `block_streak_histogram` считает только серии platform. **Что сделано.** Задача сама кладёт `counters.ban_kinds` перед любым финализатором. Статусы и колонка `ban_kind` не тронуты, контракты #3196 и #2764 остались как были. Стоп-кран для серии infra, вторая половина пункта 5, сделан в коммите #3439. **Что осталось открытым, поэтому без `Closes`:** - решение, должны ли нулевые infra-прогоны без серии оставаться `banned`; - выравнивание cian (его финализирует `scheduler.py`). Пункт 5 сам сформулирован как «решение, не код». ## #3045: ручной добор через админку шёл curl'ом **Что было.** Шаг 3 сделали только через `environment` сервиса scraper, а в `config.py` осталось `default=True`. При этом добор исполняет и `tradein-backend`: ручка `POST /scrape/avito-detail-backfill` вызывает `run_avito_detail_backfill` в своём процессе. Комментарий «true только у tradein-backend, который добор не запускает» был неверен. **Что сделано.** Дефолт изменён на `False`, исправлены три комментария, называвшие curl «прод-дефолтом». Для прода нужен ещё оверрайд в compose: хостовый `.env.runtime:37` задаёт `true` явно. Он в коммите #3182. **Шаг 4 (нагрузка), замер по `scrape_runs` за 14 суток.** Рабочие прогоны браузером: в среднем 60 попыток за 15 минут, 236 попыток в час, 64 прогона. Узлы: `scrape_runs.proxy_id` = 13, 14 и 15. Все три — mobileproxy (Tele2 и МегаФон). ASOCKS-узел (id=1) привязан к yandex, avito-добор через него не ходил ни разу. Упереться в лимиты ASOCKS этот путь не может. Основной расход давали шторма сайдкара (87 % попыток), их обрывает #3439. ## #3182: разные значения флага в контейнерах **Что было.** `printenv` на проде 17.09: | Контейнер | `AVITO_DETAIL_BACKFILL_USE_CURL` | |---|---| | `tradein-scraper` | `false` | | `tradein-backend` | `true` | | `tradein-tgbot` | `true` | | `tradein-browser` | `true` | `true` приходит из хостового `backend/.env.runtime:37`. `YANDEX_COOKIES_FILE=/app/yandex_cookies.json` задан в `.env.runtime:17`, но файла нет ни в одном контейнере, а поле `settings.yandex_cookies_file` не читал ни один модуль: был только мок в тесте, куки Яндекса давно хранятся в БД (#3192). **Что сделано.** - В `docker-compose.prod.yml` у `backend` и `tgbot` задано `AVITO_DETAIL_BACKFILL_USE_CURL: "false"`: у всех сервисов образа backend одно значение. tgbot добор не исполняет, но так правило проверяется без списка исключений. - Остальные флаги режима на проде одинаковы во всех четырёх контейнерах: `SCRAPER_FETCH_MODE=browser` и `AVITO_IMV_USE_BROWSER_FETCHER=true`. - Поле `yandex_cookies_file` удалено. Настройка `extra="ignore"` не даёт процессу упасть на старте из-за переменной, которая осталась в `.env.runtime`. ## Тесты Все прогоны локально (macOS), `rc` снят с самого pytest. - `tests/test_3288b_block_kind_ratio.py`: 20 passed. Из них новые: - обрыв ровно на 25-м отказе сайдкара подряд, на 24-м нет; - ответ площадки (успех, блок или 404) обнуляет серию, параметризация на 3 случая; - шторм «1 успех + 1599 отказов»: `attempted=26`, `mark_failed`, `abort_reason=infra_streak`, `ban_kinds {"infra": 25}`; - 24 infra + ответ площадки ×3: снапшот пройден целиком; - 25 % настоящих блоков: обрыва нет; - прогон без обрыва несёт `ban_kinds` в counters. Прежний контроль #3367 («20 infra + 10 успехов → done») зелёный. - `tests/test_3045_use_curl_default.py`: 2 passed (`Settings` без env даёт `False`; env по-прежнему переключает на curl). - `tests/test_3182_detail_transport_one_value.py`: 2 passed (разобранный compose: у каждого сервиса образа backend `"false"`; `Settings` с `YANDEX_COOKIES_FILE` в env стартует без поля). - Соседние тесты avito-добора (`tasks/test_avito_detail_backfill`, `test_3338`, `test_3384`, `test_3283g`, `test_3251`, `test_backfill_honest_status`, `test_2764`, `tasks/test_3283`): 98 passed, 1 skipped. - Весь сьют: `DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider` — **6237 passed, 42 skipped, rc=0** (109 с). - `uv run ruff check app tests`: rc=0. `ruff format --check` по 8 изменённым файлам: rc=0. - `packages/scraper-kit`: тестов нет (pytest rc=5, no tests ran). ## Фальсификация (красный прогон при сломанном фиксе, исходник восстановлен, `diff -q` чист) - #3439, без проводки порога в задаче: `AssertionError: attempted=1600 vs исходы=1600 … ожидали 26` / `assert (1600, 1600) == (26, 26)`. - #3439, без обнуления серии на блоке площадки: `AssertionError: abort_reason='infra_streak': 24 + ответ площадки (platform) + 24 — серии в 25 подряд не было`. - #3439, без ветки финала: `AssertionError: прогон ушёл в backfill-honest-status: там 1 карточка превращает шторм в 'done'`. - #3288, без строки `ban_kinds`: `assert None == {'infra': 20}`. - #3045 с `default=True`: `AssertionError: assert True is False`. - #3182, без строки у backend: `{'backend': None} != {'backend': 'false'}`; с возвращённым полем: `AssertionError: assert not True`. ## Деплой - Меняется код образа backend, поэтому пересоздаются `tradein-backend`, `tradein-scraper` (через drain) и `tradein-tgbot`. `tradein-browser` не меняется. **Перед мержем** нужно проверить, что сейчас ничего не бежит: `SELECT id, source, started_at FROM scrape_runs WHERE status='running'`. Пересоздание scraper убивает работающие прогоны. - Миграций нет. - На хосте (по желанию, это не требование PR) строки 17 (`YANDEX_COOKIES_FILE`) и 37 (`AVITO_DETAIL_BACKFILL_USE_CURL=true`) в `/opt/gendesign/tradein-mvp/backend/.env.runtime` теперь просто шум. После их удаления `printenv` в `tradein-browser` перестанет показывать `true`. ## Приёмка на проде (после деплоя; сроки от даты деплоя) 1. Сразу после деплоя: `docker exec tradein-backend printenv AVITO_DETAIL_BACKFILL_USE_CURL` и то же для `tradein-tgbot` дают `false`. `docker exec tradein-backend python -c 'from app.core.config import settings; print(settings.avito_detail_backfill_use_curl, hasattr(settings, "yandex_cookies_file"))'` печатает `False False`. 2. В течение 7 суток, первый шторм сайдкара (`InvalidIP` / `/fetch 503` в логах `tradein-browser`): ```sql SELECT id, status, counters->>'attempted', counters->>'enriched', counters->>'abort_reason', counters->'ban_kinds' FROM scrape_runs WHERE source='avito_detail_backfill' AND started_at > '<деплой>' ORDER BY id DESC; ``` Ожидание: ни одного прогона с `attempted` ≥ 500 и `enriched/attempted` < 1 %. У штормового прогона `status='failed'`, `abort_reason='infra_streak'`, `attempted` около 25, в `ban_kinds` доминирует `infra`. Если шторма за 7 суток не было, приёмка не пройдена и не провалена: переносится на следующее окно с датой. 3. У любого завершённого прогона с блоками после деплоя (включая `done`) есть `counters->'ban_kinds'`. ## Не вошло в PR - **#3035** (`?context=` в `source_url`). Автор в комментарии от 23.08 прямо просит не править вслепую: знак эффекта неизвестен, и протухший `searchHash` может выглядеть подозрительнее, чем его отсутствие. Первый пункт приёмки — A/B-замер на проде (переключение режима и прогоны на одном узле). Это запись на прод и решение владельца. Судьба `listings.phones` — тоже решение. - **#3171** (ротация IP). Первый пункт приёмки задача прямо отдаёт владельцу: «ротация тратит квоту у провайдера». Второй — «по строкам видно, помогла ли ротация» — требует договорённости, что считать «помогла» (окно попыток, куда писать: `scrape_proxy_rotations.note` или отдельная колонка). Вызов ротации уже проведён в #3302/#3313. Closes #3045 Closes #3182 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 4 commits 2026-09-17 07:58:15 +00:00
curl_cffi не проходит QRATOR proof-of-work Авито в принципе (замер 21.08:
~6% обогащений против ~74% на браузере). Шаг 3 задачи был сделан только
environment'ом сервиса scraper, дефолт в config.py остался True. Добор при
этом исполняет и tradein-backend — ручка админки scrape_avito_detail_backfill
вызывает run_avito_detail_backfill в своём процессе, и там printenv даёт true.
Ручной прогон через админку шёл curl'ом.

Дефолт avito_detail_backfill_use_curl -> False. Комментарий в задаче «true
только у tradein-backend, который добор не запускает» был неверен — исправлен,
как и два упоминания use_curl=True «прод-дефолтом».

Тест по значению: Settings без env и без .env даёт False; контроль — env
по-прежнему переключает на curl. Фальсификация: default=True ->
«AssertionError: assert True is False».

На проде хостовый backend/.env.runtime:37 задаёт true явно, поэтому для
tradein-backend нужен ещё environment-оверрайд — он в коммите #3182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прод 17.09, printenv: AVITO_DETAIL_BACKFILL_USE_CURL=false у tradein-scraper
и true у tradein-backend/tgbot/browser. Источник true — хостовый
backend/.env.runtime:37; false задан только environment'ом сервиса scraper.
tradein-backend исполняет добор сам (ручка админки
scrape_avito_detail_backfill), то есть ручной прогон шёл curl'ом, который
QRATOR PoW не проходит.

- docker-compose.prod.yml: AVITO_DETAIL_BACKFILL_USE_CURL "false" у backend
  и tgbot — у всех сервисов образа backend одно значение (environment
  перекрывает env_file). tgbot добор не исполняет; строка там, чтобы правило
  «одно значение на образ» проверялось без списка исключений.
- Остальные флаги режима проверены на проде: SCRAPER_FETCH_MODE=browser и
  AVITO_IMV_USE_BROWSER_FETCHER=true одинаковы во всех четырёх контейнерах.
- yandex_cookies_file удалён из Settings: его не читал ни один модуль
  (только мок в test_scraper_proxy.py), файла /app/yandex_cookies.json нет
  ни в одном контейнере, куки Яндекса давно живут в БД (#3192).
  extra="ignore" держит старт при оставшейся переменной в .env.runtime.

Тест по значению: разобранный compose — у каждого сервиса образа backend
значение "false"; Settings с YANDEX_COOKIES_FILE в env стартует без поля.
Фальсификация: без строки у backend — «{'backend': None} != {'backend':
'false'}»; с возвращённым полем — «assert not True».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 5 из комментария 05.09: ban_kind пишется только у banned-прогонов, у
done/failed диагноза в БД нет. Прод: 7193, 7218, 7264, 7302 — по 1600
попыток, ~1599 блоков, status='done', counters без ban_kinds; что это были
отказы сайдкара, а не площадки, видно лишь по косвенному признаку (мелкая
block_streak_histogram считает только platform-серии). mark_backfill_finished
кладёт перепись в counters только на ветке бана.

Задача теперь кладёт counters.ban_kinds сама перед любым финализатором
(mark_backfill_finished / mark_failed пустого пула). Статусы и колонка
ban_kind не тронуты — контракты #3196/#2764 как были.

Тест по значению: 20 отказов сайдкара + 10 успехов — counters, уходящие в
финализатор, несут {"infra": 20}. Фальсификация: без строки —
«assert None == {'infra': 20}».

Стоп-кран для infra-серии (вторая половина п.5) — отдельным коммитом #3439.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(tradein/avito): серия отказов нашего тракта обрывает detail-добор статусом failed (#3439)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 19s
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 5m12s
6be2bf9aaa
Прод: 7193, 7218, 7264, 7302 (15-16.09) и 6608/6631/6643/6662 (10.09) — по
1600 попыток, 1-10 карточек, ~1599 блоков, status='done', по 70+ минут
каждый. В те же окна в логах tradein-browser шторм «browser launch failed …
InvalidIP» и «/fetch 503». 6885 (13.09) — те же 1600 попыток при
ban_kinds {"infra": 1591}, но 'banned'.

Корень — регресс #3288/#3367: record_block(kind != platform) уходит в
record_failure(), то есть только в знаменатель окна. Числитель доли растёт
лишь на platform, safety-net смотрит на platform-серию, счётчика серии
infra нет вовсе. Серия отказов сайдкара любой длины не рвала прогон ничем,
а финализатор #3367 при одной карточке ставил 'done'.

- BlockRatioBreaker: infra_streak_limit и счётчик серии infra. Серию гасит
  любой ответ площадки — успех, её блок, 404. abort_reason() -> "infra_streak"
  (проверяется первым), abort_explanation() называет эту величину.
- avito_detail_backfill: порог = max_consecutive_failures (дефолт 25) — тот же
  разряд «отказ нашей стороны», что и отказы-не-блоки. На обрыве — mark_failed
  «тракт недоступен: N отказов … подряд без единого ответа площадки», в
  counters abort_reason=infra_streak и ban_kinds. Не 'banned' — площадка
  молчала; не через mark_backfill_finished — там одна карточка даёт 'done'.

Тесты по значению: 1 успех + 1599 отказов сайдкара -> attempted=26,
mark_failed, abort_reason=infra_streak, ban_kinds {"infra": 25}; обрыв ровно
на 25-м (24 — нет); 24 infra + ответ площадки (успех/блок/404) + 24 — не
обрыв; 24 infra + ответ, трижды — снапшот пройден; 25% настоящих блоков —
не обрыв; прежний контроль #3367 (20 infra + 10 успехов -> done) зелёный.

Фальсификация: без проводки порога — «assert (1600, 1600) == (26, 26)»; без
сброса серии на блоке площадки — «abort_reason='infra_streak': 24 + ответ
площадки (platform) + 24»; без ветки финала — «прогон ушёл в
backfill-honest-status: там 1 карточка превращает шторм в 'done'».

Пункт 3 задачи (browser_unfit узлу, на котором сайдкар не поднял браузер)
не сделан: это вердикт пула прокси и протокол сайдкара, отдельная задача.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit e52057e66d into main 2026-09-17 09:23:22 +00:00
Sign in to join this conversation.
No reviewers
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#3560
No description provided.