Яндекс: пять вечно недогружаемых карточек больше не обрывают каждый прогон добора #3574

Merged
bot-backend merged 2 commits from fix/yandex-perpetual-underloaded into main 2026-09-17 11:22:05 +00:00
Collaborator

Refs #3191 (issue не закрывается: в нём остаются п.4 — backfill уже сохранённых недогруженных карточек — и маркер для newbuilding.py; это не входит в правку).

Что было

PR #3364 (05.09) сделал недогруженную карточку отказом: detail_enriched_at не ставится, объявление остаётся в очереди. Счётчика попыток на объявлении при этом не было — в комментарии 05.09 прямо записано, что вечно недогружаемая карточка «ограничена только брейкером прогона».

С 13.09 21:14 этот риск сработал. Пять объявлений — 10776456, 10775964, 10775945, 10775747, 10775594 (все scraped_at 12.09 15:02, в очереди стоят подряд, is_active=t, detail_enriched_at NULL) — в каждом прогоне приходили страницей ~381 КБ без encryptedPhones/redirectPhones. Пять недогрузов подряд выбивали брейкер consecutive_none=5.

Улики (Loki tradein-scraper + scrape_runs, прод, 17.09):

  • 16 прогонов закончились ровно attempted=5 incomplete=5 на этих пяти listing_id: 6972, 6982, 6997, 7016, 7040, 7053, 7064, 7129, 7149, 7158, 7167, 7188, 7198, 7212, 7228, 7259; ещё 7109 — 4 недогруза + таймаут на пятом.
  • «Успешные» прогоны 6963, 7076, 7178, 7281 тоже обрывались, как только доходили до пятёрки: у каждого incomplete=5, и дальше очередь не двигалась. За пятёркой — 20 501 необогащённое yandex-объявление, впереди неё сейчас всего 35. Значит, ближайший прогон снова в неё упрётся.
  • Проба 17.09 с другого IP (не прод-прокси): /offer/5601896548455123200/, /offer/67398737598609920, /offer/7338505269989347072 → HTTP 200, 380–381 КБ, пустая SPA-оболочка: нет INITIAL_STATE, id оффера в странице не встречается ни разу. Для сравнения, обогащённый тем же утром /offer/1553505215059041024/ — 3,04 МБ, 54 encryptedPhones. Выходит, дело в самом объявлении, а не в сети или узле. С 12.09 свип их больше не видел (last_seen_at = scraped_at).

Причина роста failed у yandex_detail_backfill с 14.09

Разобрал все прогоны 12.09–17.09 по warning-логу. Причин две:

  1. Эта пятёрка — основной рост: 16 из 23 упавших прогонов 13.09–17.09 плюс обрывы «успешных». Правка ниже закрывает именно её.
  2. Таймауты загрузки через прокси (curl: (28) Operation timed out after 30000 ms with 45–569 КБ received) — отдельный класс, и появился он раньше пятёрки: 6790, 6808, 6870, 6891 (12–13.09), потом 7087, 7250, 7298, 7307. Последние два упавших прогона (16.09 21:19 и 17.09 00:19) — чистые таймауты на других объявлениях. В этом PR таймауты не правятся: неизвестно, поможет ли больший таймаут, если узел отдаёт 3-мегабайтную страницу медленно. Нужен отдельный разбор.

Что сделано

  • Миграция 321listings.detail_incomplete_count smallint NOT NULL DEFAULT 0 и detail_incomplete_at timestamptz. ADD COLUMN с константным DEFAULT не переписывает heap. Есть SET LOCAL lock_timeout = '5s', повторный прогон ничего не ломает (idempotent). Существующие строки не меняются.
  • app/tasks/yandex_detail_backfill.py:
    • при недогрузе объявление получает count + 1 и at = now(), запись коммитится сразу. Если запись упала — rollback и warning, карточка просто вернётся без паузы;
    • снапшот не берёт карточку сутки после недогруза, а после трёх недогрузов не берёт совсем. Разовый недогруз (1,8 МБ в замере 28.08) повтором проходит, поэтому первым шагом идёт пауза, а не исключение;
    • повторный недогруз карточки, которая уже недогружалась, не двигает брейкер: это сведения о карточке, а не о площадке. Первый недогруз двигает брейкер, как и раньше, так что защита от системного недогруза (оболочки на всё) осталась;
    • новый счётчик прогона incomplete_given_up: сколько необогащённых объявлений вышло из очереди после трёх недогрузов. Выбывшие не пропадают из виду.

Тесты

  • tests/test_3191_yandex_perpetual_underloaded.py:
    • мок: пять уже недогружавшихся карточек в голове снапшота не обрывают прогон (attempted=8 enriched=3 incomplete=5, три следующие запрошены); пять первых недогрузов подряд по-прежнему обрывают его (attempted=5, следующие не запрошены);
    • живой Postgres, настоящий SQL: прогон 1 обрывается на пятёрке; прогон 2 сразу следом её не запрашивает и обогащает следующие три; через 25 ч пятёрка возвращается, и прогон идёт дальше неё; после трёх недогрузов её нет в снапшоте, incomplete_given_up >= 5, в БД detail_incomplete_count = [3,3,3,3,3,0,0,0]. В CI (ci-tradein.yml, Postgres) тест выполняется, локально без БД пропускается и объявлен в skip_allowlist.txt.
  • Мок-лэйн целиком (DATABASE_URL=…localhost:5432/test pytest tests/), до rebase: 6234 passed, 45 skipped, rc=0. После rebase на свежий main: 6372 passed, 4 failed, 45 skipped, rc=1. Все 4 падения — tests/test_3466_corridor_tier_a.py (2) и tests/test_estimator_radius_floor.py (2) с AttributeError: 'Settings' object has no attribute 'estimate_corridor_clamp_*'. На чистом origin/main a130303c они падают так же (4 failed), значит, это не эта ветка: судя по всему, #3554 и #3556 разошлись при мерже.
  • Живой лэйн (postgis/postgis:16-3.4 со схемой из всех data/sql, как в CI), до rebase: 6278 passed, 1 skipped, rc=0. После rebase 6 затронутых файлов — 35 passed, rc=0.
  • Миграция 321 на этой схеме применяется, повторный прогон проходит (rc=0). scripts/check-migration-lock-timeout.py — rc=0. tests/test_migration_numbering.py — 3 passed. ruff check app tests — чисто. ruff format --check на изменённых файлах — чисто.

Фальсификация (живой Postgres, исходник восстановлен из копии, diff -q пусто)

  • F1, снят фильтр паузы и потолка в снапшоте → 1 failed, 2 passed:
    AssertionError: недогруженные час назад карточки снова в снапшоте — прогон опять упрётся в них, как 16 прогонов 13.09–16.09
  • F2, повторный недогруз снова двигает брейкер → 2 failed, 1 passed:
    AssertionError: запрошено 5 из 8: повторный недогруз известных карточек оборвал прогон раньше, чем очередь дошла до следующих и AssertionError: повтор известных карточек оборвал прогон
  • F3, UPDATE не пишет недогруз → 1 failed: та же ошибка «недогруженные час назад карточки снова в снапшоте…»
  • F4, снят только потолок в три попытки → 1 failed:
    AssertionError: после трёх недогрузов карточка всё ещё в очереди

Деплой

Миграция 321 применяется до пересоздания контейнеров; старый код новые колонки не читает. Пересоздаются tradein-backend, tradein-scraper и tradein-tgbot. yandex_detail_backfill стартует каждые 3 ч около :19 и идёт до часа, поэтому перед деплоем проверить SELECT id, source FROM scrape_runs WHERE status='running'.

Приёмка на проде (критерий записан до факта, проверить до 21.09.2026)

  1. _schema_migrations содержит 321_listings_detail_incomplete_attempts.sql, у listings обе колонки.
  2. Первый прогон yandex_detail_backfill, дошедший до пятёрки, ещё может закончиться attempted=5 incomplete=5: у карточек пока 0 попыток, это первый недогруз. После него SELECT id, detail_incomplete_count, detail_incomplete_at FROM listings WHERE id IN (10776456,10775964,10775945,10775747,10775594) → у всех 1.
  3. Все прогоны в следующие 24 ч: в Loki нет warning с этими listing_id, attempted > 5 (кроме обрывов по таймаутам, это второй класс). SELECT id, counters FROM scrape_runs WHERE source='yandex_detail_backfill' ORDER BY id DESC LIMIT 10.
  4. Повторы (2-я и 3-я попытки, через сутки) идут без ABORT, в логе попытка 2/3, попытка 3/3. После третьей попытки counters.incomplete_given_up >= 5, и прогонов вида attempted=5 incomplete=5 нет.
  5. Если после первого касания повторяется attempted=5 incomplete=5 на тех же id — правка не сработала.

🤖 Generated with Claude Code

Refs #3191 (issue не закрывается: в нём остаются п.4 — backfill уже сохранённых недогруженных карточек — и маркер для `newbuilding.py`; это не входит в правку). ## Что было PR #3364 (05.09) сделал недогруженную карточку отказом: `detail_enriched_at` не ставится, объявление остаётся в очереди. Счётчика попыток на объявлении при этом не было — в комментарии 05.09 прямо записано, что вечно недогружаемая карточка «ограничена только брейкером прогона». С 13.09 21:14 этот риск сработал. Пять объявлений — 10776456, 10775964, 10775945, 10775747, 10775594 (все `scraped_at` 12.09 15:02, в очереди стоят подряд, `is_active=t`, `detail_enriched_at` NULL) — в каждом прогоне приходили страницей ~381 КБ без `encryptedPhones`/`redirectPhones`. Пять недогрузов подряд выбивали брейкер `consecutive_none=5`. Улики (Loki `tradein-scraper` + `scrape_runs`, прод, 17.09): - 16 прогонов закончились ровно `attempted=5 incomplete=5` на этих пяти `listing_id`: 6972, 6982, 6997, 7016, 7040, 7053, 7064, 7129, 7149, 7158, 7167, 7188, 7198, 7212, 7228, 7259; ещё 7109 — 4 недогруза + таймаут на пятом. - «Успешные» прогоны 6963, 7076, 7178, 7281 тоже обрывались, как только доходили до пятёрки: у каждого `incomplete=5`, и дальше очередь не двигалась. За пятёркой — 20 501 необогащённое yandex-объявление, впереди неё сейчас всего 35. Значит, ближайший прогон снова в неё упрётся. - Проба 17.09 с другого IP (не прод-прокси): `/offer/5601896548455123200/`, `/offer/67398737598609920`, `/offer/7338505269989347072` → HTTP 200, 380–381 КБ, пустая SPA-оболочка: нет `INITIAL_STATE`, id оффера в странице не встречается ни разу. Для сравнения, обогащённый тем же утром `/offer/1553505215059041024/` — 3,04 МБ, 54 `encryptedPhones`. Выходит, дело в самом объявлении, а не в сети или узле. С 12.09 свип их больше не видел (`last_seen_at` = `scraped_at`). ## Причина роста failed у yandex_detail_backfill с 14.09 Разобрал все прогоны 12.09–17.09 по warning-логу. Причин две: 1. **Эта пятёрка** — основной рост: 16 из 23 упавших прогонов 13.09–17.09 плюс обрывы «успешных». Правка ниже закрывает именно её. 2. **Таймауты загрузки через прокси** (`curl: (28) Operation timed out after 30000 ms with 45–569 КБ received`) — отдельный класс, и появился он раньше пятёрки: 6790, 6808, 6870, 6891 (12–13.09), потом 7087, 7250, 7298, 7307. Последние два упавших прогона (16.09 21:19 и 17.09 00:19) — чистые таймауты на других объявлениях. **В этом PR таймауты не правятся**: неизвестно, поможет ли больший таймаут, если узел отдаёт 3-мегабайтную страницу медленно. Нужен отдельный разбор. ## Что сделано - **Миграция 321** — `listings.detail_incomplete_count smallint NOT NULL DEFAULT 0` и `detail_incomplete_at timestamptz`. ADD COLUMN с константным DEFAULT не переписывает heap. Есть `SET LOCAL lock_timeout = '5s'`, повторный прогон ничего не ломает (idempotent). Существующие строки не меняются. - **`app/tasks/yandex_detail_backfill.py`:** - при недогрузе объявление получает `count + 1` и `at = now()`, запись коммитится сразу. Если запись упала — rollback и warning, карточка просто вернётся без паузы; - снапшот не берёт карточку сутки после недогруза, а после трёх недогрузов не берёт совсем. Разовый недогруз (1,8 МБ в замере 28.08) повтором проходит, поэтому первым шагом идёт пауза, а не исключение; - повторный недогруз карточки, которая уже недогружалась, **не двигает брейкер**: это сведения о карточке, а не о площадке. Первый недогруз двигает брейкер, как и раньше, так что защита от системного недогруза (оболочки на всё) осталась; - новый счётчик прогона `incomplete_given_up`: сколько необогащённых объявлений вышло из очереди после трёх недогрузов. Выбывшие не пропадают из виду. ## Тесты - `tests/test_3191_yandex_perpetual_underloaded.py`: - мок: пять уже недогружавшихся карточек в голове снапшота не обрывают прогон (`attempted=8 enriched=3 incomplete=5`, три следующие запрошены); пять первых недогрузов подряд по-прежнему обрывают его (`attempted=5`, следующие не запрошены); - живой Postgres, настоящий SQL: прогон 1 обрывается на пятёрке; прогон 2 сразу следом её не запрашивает и обогащает следующие три; через 25 ч пятёрка возвращается, и прогон идёт дальше неё; после трёх недогрузов её нет в снапшоте, `incomplete_given_up >= 5`, в БД `detail_incomplete_count = [3,3,3,3,3,0,0,0]`. В CI (ci-tradein.yml, Postgres) тест выполняется, локально без БД пропускается и объявлен в `skip_allowlist.txt`. - Мок-лэйн целиком (`DATABASE_URL=…localhost:5432/test pytest tests/`), до rebase: **6234 passed, 45 skipped, rc=0**. После rebase на свежий main: **6372 passed, 4 failed, 45 skipped, rc=1**. Все 4 падения — `tests/test_3466_corridor_tier_a.py` (2) и `tests/test_estimator_radius_floor.py` (2) с `AttributeError: 'Settings' object has no attribute 'estimate_corridor_clamp_*'`. На чистом `origin/main` a130303c они падают так же (4 failed), значит, это не эта ветка: судя по всему, #3554 и #3556 разошлись при мерже. - Живой лэйн (postgis/postgis:16-3.4 со схемой из всех `data/sql`, как в CI), до rebase: **6278 passed, 1 skipped, rc=0**. После rebase 6 затронутых файлов — **35 passed, rc=0**. - Миграция 321 на этой схеме применяется, повторный прогон проходит (rc=0). `scripts/check-migration-lock-timeout.py` — rc=0. `tests/test_migration_numbering.py` — 3 passed. `ruff check app tests` — чисто. `ruff format --check` на изменённых файлах — чисто. ## Фальсификация (живой Postgres, исходник восстановлен из копии, `diff -q` пусто) - **F1**, снят фильтр паузы и потолка в снапшоте → `1 failed, 2 passed`: `AssertionError: недогруженные час назад карточки снова в снапшоте — прогон опять упрётся в них, как 16 прогонов 13.09–16.09` - **F2**, повторный недогруз снова двигает брейкер → `2 failed, 1 passed`: `AssertionError: запрошено 5 из 8: повторный недогруз известных карточек оборвал прогон раньше, чем очередь дошла до следующих` и `AssertionError: повтор известных карточек оборвал прогон` - **F3**, UPDATE не пишет недогруз → `1 failed`: та же ошибка «недогруженные час назад карточки снова в снапшоте…» - **F4**, снят только потолок в три попытки → `1 failed`: `AssertionError: после трёх недогрузов карточка всё ещё в очереди` ## Деплой Миграция 321 применяется до пересоздания контейнеров; старый код новые колонки не читает. Пересоздаются `tradein-backend`, `tradein-scraper` и `tradein-tgbot`. `yandex_detail_backfill` стартует каждые 3 ч около :19 и идёт до часа, поэтому перед деплоем проверить `SELECT id, source FROM scrape_runs WHERE status='running'`. ## Приёмка на проде (критерий записан до факта, проверить до 21.09.2026) 1. `_schema_migrations` содержит `321_listings_detail_incomplete_attempts.sql`, у `listings` обе колонки. 2. Первый прогон `yandex_detail_backfill`, дошедший до пятёрки, ещё может закончиться `attempted=5 incomplete=5`: у карточек пока 0 попыток, это первый недогруз. После него `SELECT id, detail_incomplete_count, detail_incomplete_at FROM listings WHERE id IN (10776456,10775964,10775945,10775747,10775594)` → у всех `1`. 3. Все прогоны в следующие 24 ч: в Loki нет warning с этими `listing_id`, `attempted > 5` (кроме обрывов по таймаутам, это второй класс). `SELECT id, counters FROM scrape_runs WHERE source='yandex_detail_backfill' ORDER BY id DESC LIMIT 10`. 4. Повторы (2-я и 3-я попытки, через сутки) идут без `ABORT`, в логе `попытка 2/3`, `попытка 3/3`. После третьей попытки `counters.incomplete_given_up >= 5`, и прогонов вида `attempted=5 incomplete=5` нет. 5. Если после первого касания повторяется `attempted=5 incomplete=5` на тех же id — правка не сработала. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 09:47:05 +00:00
fix(tradein): вечно недогружаемые карточки Яндекса больше не держат очередь добора (#3191)
Some checks failed
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 8m10s
CI Trade-In / changes (pull_request) Successful in 18s
CI / changes (pull_request) Successful in 34s
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
3b9742c60d
С 13.09 21:14 по 16.09 18:52 пять объявлений (10776456, 10775964, 10775945,
10775747, 10775594), стоящих в очереди подряд, в каждом прогоне приходили пустой
SPA-оболочкой ~381 КБ без блока контактов. Счётчика попыток на объявлении не было,
они возвращались в голову каждого снапшота и пятью недогрузами подряд выбивали
брейкер: 16 прогонов закончились ровно attempted=5 incomplete=5.

Миграция 321: listings.detail_incomplete_count / detail_incomplete_at.
yandex_detail_backfill пишет недогруз на объявление; снапшот не берёт карточку
сутки и не берёт совсем после трёх недогрузов (счётчик incomplete_given_up);
повторный недогруз уже известной карточки не двигает брейкер, первый — двигает,
защита от системного недогруза сохранена.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-17 10:49:18 +00:00
Merge remote-tracking branch 'origin/main' into fix/yandex-perpetual-underloaded
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 19s
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 / changes (pull_request) Successful in 23s
CI Trade-In / backend-tests (pull_request) Successful in 8m29s
6375b191e4
bot-backend merged commit 4c11a7dab5 into main 2026-09-17 11:22:05 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3574
No description provided.