fix(tradein/yandex): «непригодных» 3535 не было — адресуем их по offerId #2838

Merged
bot-backend merged 1 commit from fix/yandex-unenrichable-frozen into main 2026-08-12 15:04:54 +00:00
Collaborator

Что было

yandex_detail_backfill шесть прогонов подряд писал unenrichable_pending = ровно 3535, ни на единицу, при том что очередь обогащалась по ~500 за прогон.

Диагноз

Из трёх версий (честная константа / застывшая выборка / мёртвая ветка) верна оказалась четвёртая: арифметика честная, а ярлык — нет.

  • Счётчик считается живым SELECT count(*) — воспроизведён на проде, даёт те же 3535. Ни кэша, ни матвьюхи.
  • Множество замкнуто, поэтому и константа: войти нельзя (после #2235 продюсер таких строк не создаёт — 0 из 6892 yandex-строк, вставленных после самой свежей его строки, id 2583989), выйти нельзя (обогащение недостижимо, source_url не переписывается). Вопрос был не «почему не растёт», а «правда ли непригодны».
  • Непригодны они не были. У всех 3535 в source_id лежит числовой yandex offerId (у 3523 продублирован в yandex_offer_id). Живая проба прод-трактом (тот же прокси, curl_cffi chrome120, тот же YandexDetailScraper.parse, БЕЗ записи): 6 из 6 → HTTP 200 + parse OK, включая строки, чей сохранённый source_url — рекламный редирект na100.pro/go.php, и строки, не переобойденные с 17.06.

Корень

source_url пишется только при вставке — его нет ни в ON CONFLICT DO UPDATE, ни в reconcile-UPDATE у save_listings. Поэтому #2235 (канонизация URL в продюсере) вылечил только новые строки, а миграция 164 — только те легаси, чей URL делили несколько строк (она искала дубли URL, а не непарсимость). Строки с уникальной ссылкой на карточку застройщика (macroserver.ru/id/…, strana.com/…/flats/…) не попали ни туда, ни туда и носят адрес, замороженный в момент вставки, хотя свип переобходит ~511 из них в сутки.

Что в PR

  • Снапшот-SELECT адресует такие строки URL, вычисленным из source_id (та же формула, что у продюсера и у миграции 164), а не выбрасывает их.
  • Остаток очереди разделён по причине: url_from_offer_id (адрес чиним; должен убывать от прогона к прогону) и unenrichable_pending (адресовать нечем — ни offer-URL, ни числового source_id; на проде 0). Одно число на две разные судьбы читалось как «тут делать нечего» и держало 3535 квартир вне обогащения неделю. Наблюдаемость не убрана, а разделена.
  • Комментарий у константы больше не утверждает непригодность: там теперь замер и вывод пробы.

Проверка

SQL прогнан на проде read-only, ровно в том виде, в каком его строит задача:

проверка результат
новый снапшот-SELECT (batch 800) 800 строк, 800/800 с адресом, который парсер принимает
новые счётчики url_from_offer_id=3535, unenrichable_pending=0
разбиение точное queueable 13921 = pending 13921, unaddressable 0 — ни одна строка не выпадает

Тесты — с красным прогоном (оба сторожа падают, если ломать то, что они стерегут):

  • сузить гейт очереди обратно к критерию origin/maintest_pending_counter_split_by_reason FAILED;
  • увести формулу канонического URL (потерять хвостовой слэш) → test_recovered_url_equals_producer_canonical_form FAILED (сверяется с _canonical_source_url продюсера, а не с ожиданием из той же настройки).

15 passed, ruff чист.

Требует отдельной работы (НЕ здесь)

Миграция — починка самой колонки source_url одноразовым UPDATE: тот же 164, но без условия на дубли (source='yandex' AND source_url !~ '/offer/[0-9]+' AND source_id ~ '^[0-9]+$'). Проверено: коллизий 0; долговечность доказана самой 164 — сегодня активных yandex-строк с общим source_url 0 групп, т.е. переписанное не откатывается. От этого зависит и yandex_address_backfill: у него 1618 из 5217 кандидатов ходят на сайты застройщиков вместо Яндекса (прогон 3586: checked 200, saved 1, errors 21).

Отдельно на подумать: source_url вне ON CONFLICT DO UPDATE — сейчас утечки нет (продюсер канонический), но любая будущая порча адреса так же не самозалечится.

## Что было `yandex_detail_backfill` шесть прогонов подряд писал `unenrichable_pending` = **ровно 3535**, ни на единицу, при том что очередь обогащалась по ~500 за прогон. ## Диагноз Из трёх версий (честная константа / застывшая выборка / мёртвая ветка) верна оказалась **четвёртая**: арифметика честная, а ярлык — нет. * Счётчик считается живым `SELECT count(*)` — воспроизведён на проде, даёт те же 3535. Ни кэша, ни матвьюхи. * Множество **замкнуто**, поэтому и константа: войти нельзя (после #2235 продюсер таких строк не создаёт — 0 из 6892 yandex-строк, вставленных после самой свежей его строки, id 2583989), выйти нельзя (обогащение недостижимо, `source_url` не переписывается). Вопрос был не «почему не растёт», а «правда ли непригодны». * **Непригодны они не были.** У всех 3535 в `source_id` лежит числовой yandex `offerId` (у 3523 продублирован в `yandex_offer_id`). Живая проба прод-трактом (тот же прокси, curl_cffi chrome120, тот же `YandexDetailScraper.parse`, БЕЗ записи): **6 из 6 → HTTP 200 + parse OK**, включая строки, чей сохранённый `source_url` — рекламный редирект `na100.pro/go.php`, и строки, не переобойденные с 17.06. ## Корень `source_url` пишется **только при вставке** — его нет ни в `ON CONFLICT DO UPDATE`, ни в reconcile-UPDATE у `save_listings`. Поэтому #2235 (канонизация URL в продюсере) вылечил только новые строки, а миграция 164 — только те легаси, чей URL **делили** несколько строк (она искала дубли URL, а не непарсимость). Строки с уникальной ссылкой на карточку застройщика (`macroserver.ru/id/…`, `strana.com/…/flats/…`) не попали ни туда, ни туда и носят адрес, замороженный в момент вставки, хотя свип переобходит ~511 из них в сутки. ## Что в PR * Снапшот-SELECT адресует такие строки URL, **вычисленным из `source_id`** (та же формула, что у продюсера и у миграции 164), а не выбрасывает их. * Остаток очереди разделён **по причине**: `url_from_offer_id` (адрес чиним; должен убывать от прогона к прогону) и `unenrichable_pending` (адресовать нечем — ни offer-URL, ни числового `source_id`; на проде **0**). Одно число на две разные судьбы читалось как «тут делать нечего» и держало 3535 квартир вне обогащения неделю. Наблюдаемость не убрана, а разделена. * Комментарий у константы больше не утверждает непригодность: там теперь замер и вывод пробы. ## Проверка SQL прогнан на проде **read-only**, ровно в том виде, в каком его строит задача: | проверка | результат | |---|---| | новый снапшот-SELECT (batch 800) | 800 строк, **800/800** с адресом, который парсер принимает | | новые счётчики | `url_from_offer_id=3535`, `unenrichable_pending=0` | | разбиение точное | queueable **13921** = pending **13921**, unaddressable **0** — ни одна строка не выпадает | Тесты — с красным прогоном (оба сторожа падают, если ломать то, что они стерегут): * сузить гейт очереди обратно к критерию `origin/main` → `test_pending_counter_split_by_reason` FAILED; * увести формулу канонического URL (потерять хвостовой слэш) → `test_recovered_url_equals_producer_canonical_form` FAILED (сверяется с `_canonical_source_url` продюсера, а не с ожиданием из той же настройки). `15 passed`, ruff чист. ## Требует отдельной работы (НЕ здесь) **Миграция — починка самой колонки** `source_url` одноразовым UPDATE: тот же 164, но без условия на дубли (`source='yandex' AND source_url !~ '/offer/[0-9]+' AND source_id ~ '^[0-9]+$'`). Проверено: коллизий 0; долговечность доказана самой 164 — сегодня активных yandex-строк с общим `source_url` **0 групп**, т.е. переписанное не откатывается. От этого зависит и **`yandex_address_backfill`**: у него **1618 из 5217** кандидатов ходят на сайты застройщиков вместо Яндекса (прогон 3586: checked 200, saved 1, errors 21). Отдельно на подумать: `source_url` вне `ON CONFLICT DO UPDATE` — сейчас утечки нет (продюсер канонический), но любая будущая порча адреса так же не самозалечится.
bot-backend added 1 commit 2026-08-12 14:59:23 +00:00
fix(tradein/yandex): «непригодных» 3535 не было — адресуем их по offerId
All checks were successful
CI / changes (pull_request) Successful in 9s
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 3m57s
CI Trade-In / changes (pull_request) Successful in 9s
de491f4dec
unenrichable_pending шесть прогонов подряд равнялся ровно 3535. Счётчик
считается живым SELECT'ом (кэша/матвьюхи нет), но множество замкнуто: после
#2235 продюсер таких строк не создаёт (0 из 6892 yandex-строк, вставленных
после самой свежей его строки id 2583989), а выйти оттуда нельзя — обогащение
недостижимо, а source_url не переписывается. Замкнутое множество и обязано
быть константой; вопрос был не «почему не растёт», а «правда ли непригодны».

Непригодны они не были. У всех 3535 в source_id лежит числовой yandex offerId
(у 3523 продублирован в yandex_offer_id), а канонический адрес из него
собирается — тот же инвариант #2235 и та же формула, которой чинила легаси
миграция 164. Живая проба прод-трактом (тот же прокси, curl_cffi chrome120,
тот же parse): 6 из 6 HTTP 200 + parse OK, включая строки, чей сохранённый
source_url — рекламный редирект na100.pro/go.php.

Откуда стухший адрес: source_url пишется только при вставке — его нет ни в
ON CONFLICT DO UPDATE, ни в reconcile-UPDATE save_listings. Поэтому #2235
вылечил только новые строки, а миграция 164 — только те легаси, чей URL делили
несколько строк (она искала дубли URL, а не непарсимость). Строки с уникальной
ссылкой на карточку застройщика не попали никуда и носят адрес, замороженный в
момент вставки, хотя свип переобходит ~511 из них в сутки.

Что сделано: очередь адресует такие строки URL, вычисленным из source_id, а
остаток очереди разделён по причине — url_from_offer_id (адрес чиним, должен
убывать) и unenrichable_pending (адресовать нечем; на проде 0). Одно число на
две разные судьбы читалось как «тут делать нечего» и держало 3535 квартир вне
обогащения неделю.

Проверено на проде (read-only): новый снапшот-SELECT отдаёт 800/800 строк с
адресом, который парсер принимает; счётчики 3535/0; разбиение точное —
queueable 13921 = pending 13921, unaddressable 0.

Не входит сюда: починка самой колонки source_url одноразовым UPDATE (тот же
164 без условия на дубли) — за миграцией; от неё зависит и
yandex_address_backfill, где 1618 из 5217 кандидатов ходят на сайты
застройщиков вместо Яндекса.
bot-backend merged commit 447fbbd3a5 into main 2026-08-12 15:04:54 +00:00
bot-backend deleted branch fix/yandex-unenrichable-frozen 2026-08-12 15:04:54 +00:00
Author
Collaborator

Критерий приёмки выполнен — замером, а не рассуждением

Критерий записан 12.08 до факта: «в счётчиках следующего прогона url_from_offer_id должен быть меньше 3535; если снова ровно 3535 — правка не доехала».

прогон старт url_from_offer_id unenrichable_pending
3771 12.08 08:08 ключа нет 3535
3863 13.08 10:28 0 0

Оба счётчика в нуле, и оба нуля правильные: у первого — «адресовать по идентификатору больше нечего», у второго — «нечем адресовать не осталось».

Состояние очереди на тот же момент:

не адресуемых (source_url не на realty.yandex)   0 из 17 970
обогащено                                        4069  (вчера 3529)
ждут обогащения                                 14038

То есть механизм не просто перестал жаловаться — он начал работать: +540 обогащённых за сутки при полностью вычищенных адресах.

Ловушка, которую этот критерий и снимал

Если бы я посмотрел на счётчик сразу после мержа, увидел бы 3535 и заключил «правка не доехала». На деле те прогоны стартовали за семь часов до деплоя (08:08 против 15:35). Сравнение по номеру прогона и времени старта, а не по факту мержа, — единственное, что отделило верный вывод от неверного.

Ровно поэтому критерий и был записан заранее, с указанием, какое число означает провал.

## Критерий приёмки выполнен — замером, а не рассуждением Критерий записан 12.08 **до** факта: «в счётчиках следующего прогона `url_from_offer_id` должен быть меньше 3535; если снова ровно 3535 — правка не доехала». | прогон | старт | `url_from_offer_id` | `unenrichable_pending` | |---|---|---:|---:| | 3771 | 12.08 08:08 | *ключа нет* | **3535** | | **3863** | **13.08 10:28** | **0** | **0** | Оба счётчика в нуле, и оба нуля правильные: у первого — «адресовать по идентификатору больше нечего», у второго — «нечем адресовать не осталось». Состояние очереди на тот же момент: ``` не адресуемых (source_url не на realty.yandex) 0 из 17 970 обогащено 4069 (вчера 3529) ждут обогащения 14038 ``` То есть механизм не просто перестал жаловаться — он начал работать: +540 обогащённых за сутки при полностью вычищенных адресах. ## Ловушка, которую этот критерий и снимал Если бы я посмотрел на счётчик сразу после мержа, увидел бы `3535` и заключил «правка не доехала». На деле те прогоны стартовали **за семь часов до деплоя** (08:08 против 15:35). Сравнение по номеру прогона и времени старта, а не по факту мержа, — единственное, что отделило верный вывод от неверного. Ровно поэтому критерий и был записан заранее, с указанием, какое число означает провал.
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#2838
No description provided.