fix(tradein/avito): окно ретроспективы под недельный такт + диагноз полного обхода (#2674) #2685
Merged
bot-backend
merged 2 commits from 2026-08-05 23:13:40 +00:00
fix/2674-avito-full-load-coverage into main
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 6d76328168 |
fix(tradein/avito): верное объяснение границы окна и критерий приёмки по глубине (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
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 2m52s
Разбор ревью PR #2685. Выбор окна в 7 суток подтверждён непредвзятым замером по одному прогону (run 2990, exhaustive 02.08, 2184 датированных строки): W=2 -> 147, W=6 -> 446, W=7 -> 1275, W=12 -> 1278. Шестёрка теряет две трети семёрки, а 7..12 — плато в +3 лота, то есть семёрка стоит на самой дешёвой его точке. Потеря окна 2 занижена мной в первом заходе: не 3.26x, а 8.7x. Механизм объяснён неверно. Пик на возрасте ровно 7 — не недельный авто-подъём Авито, а квантование нашего же парсера относительных дат: «неделю назад» -> ровно today-7, «две недели назад» -> today-14, возрасты 8..13 по этому пути недостижимы. То самое плато (3 лота из 2184) это и доказывает: при реальном подъёме полоса 8..13 была бы заполнена. Вывод от этого только крепнет — шестёрка режет не по пику распределения, а по границе квантования и теряет бакет «неделю назад» целиком, а внутри него реальный возраст от 7 до 13 суток. 51% не воспроизводится: 1212 из 3714 датированных наблюдений — 32.6%. Пятьдесят один получается только на знаменателе, урезанном возрастами 0-13. Цена по запросам описана неверно и в опасную сторону. Рост не пропорционален лотам: стоимость бакета — ceil(свежих/50) страниц с полом 1-2, при окне 7 на бакет выходит ~15-20 свежих (1275 на 77 бакетов), то есть меньше страницы. Большинство бакетов как стояло на 1-2 страницах, так и останется. Верхняя граница честная и продом пережитая: полный обход без отсечки — 6 ч 59 мин (run 295) и 2 ч 34 мин (run 2990). Отсюда же переписан критерий приёмки: ждать «9-10 тысяч собранных лотов» нельзя, это уведёт в ложный вывод. Прогон с окном 2 уже собирал 2804 лота, потому что первые страницы всё равно полные — объём почти не сдвинется, сдвинется глубина. Считать надо лоты с listing_date в полосе [D-7, D-3] и число страниц из лог-строки paginated=. Впечатана мина на случай отката такта: ни миграция (GREATEST только расширяет), ни планировщик (расширяет до такта, не сужает) окно не сузят, поэтому interval_days 7 -> 1 при окне 7 даст восьмикратный охват каждый день. Такт и окно менять вместе. int(params.get("interval_days", 1)) падал на значении null в jsonb — соседний параметр строкой выше обрабатывался через явную проверку на None, этот нет. |
|||
| e4ac0365cf |
fix(tradein/avito): окно ретроспективы под недельный такт + диагноз полного обхода (#2674)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
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 2m53s
Окно и такт разъехались. Каденс avito_full_load задаёт default_params.interval_days, глубину обхода — incremental_days (since = today - N, ранняя остановка по listing_date). Это два независимых литерала, обязанных совпадать: 129 поставил окно 2 при ежедневном такте (перекрытие было), 206 перевёл такт на 7 суток и окно не тронул. Прогон видит [D-2, D] = 3 суток из 7; дни D+1..D+4 не попадают ни в один прогон. Числа с прода (tradein, read-only 2026-08-06). 2026-06-21 — единственный день, когда оба обхода отработали: инкрементальный run 297 — 2804 unique, exhaustive run 295 — 9992 unique, то есть окно в 2 суток достаёт 28.1% инвентаря. Когорта run 295, не виденная после 23.06 (listing_date заморожен): полоса [D-2, D] — 189 лотов, полоса [D-7, D-3] — ещё 427, расширение окна берёт в 3.26 раза больше. Гистограмма (last_seen_at::date - listing_date) за 20 суток: возраст 0-2 — 594, возраст ровно 7 — 1212 (51% датированных наблюдений) — у Avito недельный авто-подъём, sortTimeStamp сдвигается кратно 7. Структурный минимум бездырочного покрытия — 6, но 6 режет ровно по этому пику; 7 = такт, полосы соседних прогонов смыкаются с суточным перехлёстом под дрейф расписания (замер: last_run 03.08 13:37 -> next_run_at 10.08 14:16). Цена: страниц примерно втрое больше на прогон, но прогон недельный. До 206 система платила ~150-370 страниц семь раз в неделю; после правки — ~500-1200 в неделю, всё ещё примерно вдвое дешевле, чем до 206. Чиню в двух местах: миграция 215 выводит окно из фактического interval_days строки (GREATEST — не сужает окно шире такта), scheduler расширяет его на лету и пишет warning, чтобы расхождение не вернулось следующей правкой каденса. Полный обход: причина не в площадке. Все пять banned-прогонов avito_full_load_exhaustive (05.07-02.08) несут один текст — "browser-sidecar error: browser unavailable (proxy may be down)", то есть 503 от своего же сайдкара, у которого не поднялся камуфокс. В те же дни avito_city_sweep (20 done), avito_newbuilding_sweep (21) и avito_detail_backfill (63) работали. Корень — мёртвый BROWSER_PROXY_AVITO (ard.mobileproxy.space) в env сайдкара, куда полный обход проваливался, потому что строил свой BrowserFetcher без пула; починено не здесь, а #2637 (02.08, пул для браузерного пути Авито) и #2616 шаг 2 (05.08, снос мёртвых env). Прод подтверждает: 0 лотов 05/12/19/26.07, 2334 и 362 в двух прогонах после 02.08. Остаток, который чинится кодом, здесь: 503 сайдкара классифицировался как soft-ban и уходил в бюджет IP-ротации, а ротация снята (#2616 шаг 2, max_rot=0) — условие rot_done < max_rot ложно всегда, а бюджет коротких backoff-retry стоял в else и был для soft-ban недостижим. Ни одного ретрая на самую частую ошибку: один блип сайдкара стоил бакета, четыре подряд — всего прогона. Бюджет backoff теперь общий для обеих причин; сайдкар на таком 503 сам поднимает фоновый retry launch'а, поэтому повтор через пару секунд обычно проходит. Статус banned на такой ошибке остаётся ложью (площадка не банила) — это же чтение легло в основание 206. Здесь не трогаю: набор status ограничен CHECK-констрейнтом, а mark_banned в отличие от mark_failed сохраняет чекпоинт done_buckets. |