fix(tradein/avito): окно ретроспективы под недельный такт + диагноз полного обхода (#2674) #2685
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2685
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2674-avito-full-load-coverage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
1. Окно ретроспективы разошлось с тактом
Каденс
avito_full_loadзадаётdefault_params.interval_days, глубину обхода —incremental_days(since = today - N, ранняя остановка пагинации на первой страницебез карточек
listing_date >= since). Два независимых литерала, обязанных совпадать:129поставил окно 2 при ежедневном такте (2 >= 1, дыр не было),206перевёл такт наinterval_days: 7и окно не тронул.Прод (
scrape_schedulesid=138):{"interval_days": 7, "incremental_days": 2, …}.Прогон видит
[D-2, D]= 3 календарных дня из 7; следующий начинает с[D+5, D+7].Дни D+1..D+4 — 4 суток из 7 — не попадают ни в один прогон.
Сколько лотов достаёт окно шириной W
Непредвзятый замер по одному прогону — run 2990 (exhaustive, 02.08, 2317 строк
снапшота, 2184 с датой):
Почему обрыв ровно на 7 — это наш парсер, а не Avito
Не недельный авто-подъём площадки, а квантование относительных меток нашим же
_parse_relative_date(providers/avito/serp.py): «неделю назад» → ровноtoday-7,«две недели назад» → ровно
today-14. Возрасты 8…13 по этому пути недостижимы —на проде их 3 лота из 2184, это и есть плато. При реальном недельном подъёме полоса
8…13 была бы заполнена непрерывно; плюс пик привязан к дате наблюдения, а не ко дню недели.
Практический вывод от этого крепнет: W=6 режет не по пику распределения, а по границе
квантования — бакет «неделю назад» становится невидим целиком, а внутри него реальный
возраст от 7 до 13 суток.
Тот же бакет в общей выборке:
last_seen_at::date - listing_dateпо avito за 20 суток —возраст 0-2 = 594, возраст ровно 7 = 1212 из 3714 датированных наблюдений = 32.6%.
(В первой версии стояло 51% — это знаменатель, урезанный возрастами 0-13. Число 1212 и
«0-2 → 594» воспроизводятся точно.)
Полосы соседних прогонов
[D-7, D]и[D, D+7]смыкаются с суточным перехлёстом поддрейф расписания (замер:
last_run03.08 13:37 →next_run_at10.08 14:16 = +7 суток 39 минут).Цена по запросам — растёт НЕ пропорционально лотам
Стоимость бакета —
ceil(свежих / 50)страниц с полом в 1-2 страницы. При окне 7 набакет приходится ~15-20 свежих (1275 лотов на 77 бакетов = 7 комнатностей × 11 ценовых
seed-брекетов) — меньше одной страницы. Большинство бакетов как стояло на 1-2
страницах, так и останется; глубже пойдут только плотные. Верхняя граница честная и
продом уже пережитая: полный обход без отсечки вообще — 6 ч 59 мин (run 295) и
2 ч 34 мин (run 2990); окно 13-15 UTC ограничивает только старт, не длительность.
Что сделано
215_avito_full_load_window_matches_cadence.sql— выводит окно из фактическогоinterval_daysстроки (не хардкод 7),GREATESTне сужает. Только данные, DDL нет.Точное равенство
source = 'avito_full_load'. Read-only dry-run на проде:2 → 7.scheduler._job_avito_full_load— расширяет окно до такта на лету +warning.(
GREATESTтолько расширяет), ни планировщик (расширяет, не сужает) окно не сузят.interval_days7 → 1 при окне 7 даст восьмикратный охват каждый день. Такт иокно менять одной правкой.
2. Полный обход не отрабатывает — диагноз
Причина не в площадке. Все пять
banned-прогоновavito_full_load_exhaustive(05.07, 12.07, 19.07, 26.07, 02.08) несут ровно один текст:
Это 503 от своего же сайдкара
tradein-browser:_ensure_browserне смог поднятькамуфокс. В те же 45 дней остальные avito-джобы работали —
avito_city_sweep20 done,avito_newbuilding_sweep21 done,avito_detail_backfill63 done. Avito нас не банил.Корень: до 05.08 в env сайдкара стоял
BROWSER_PROXY_AVITO=…@ard.mobileproxy.space:1055— мёртвый аккаунт (407/refused, #2613). Полный обход проваливался именно в этот
env-fallback, потому что строил свой
BrowserFetcherбез пула (в отличие отcity/newbuilding sweep, ходящих через shared-browser с пулом). Тот же сигнатурный отказ
у соседней
avito_full_load: 22 banned подряд с 04.07.Чинится не здесь, а уже починено: #2637 (02.08, браузерный путь Авито подключён к
пулу) + #2616 шаг 2 (05.08, снос мёртвых
BROWSER_PROXY_*; прод.env.runtimeобновлён 05.08 12:56). Прод подтверждает: 0 лотов 05/12/19/26.07 → 2334 (02.08) и
362 (03.08) — единственные два прогона с ненулевым выхлопом с 03.07.
Остаток, который чинится кодом — в этом PR. Оба пост-фиксовых прогона всё равно
умерли на том же 503, потому что ретраев на него нет вообще:
_fetch_serp_html_browserклассифицирует 503/"browser unavailable" какis_soft_banи отправляет в бюджет IP-ротации;
max_rot = 0,_rotate_ip()всегдаFalse→rot_done < max_rotложно всегда;_AVITO_SIDECAR_TRANSIENT_RETRIES = 2) стоял вelse:и для soft-ban был структурно недостижим.
Один блип сайдкара стоил бакета, четыре подряд (
_AVITO_SWEEP_MAX_CONSECUTIVE_BLOCKED = 4)— всего прогона. Сайдкар на таком 503 сам поднимает фоновый retry launch'а, так что повтор
через пару секунд обычно проходит — мы просто не повторяли. Бюджет backoff теперь общий
для обеих причин. Фикс стоит в общей воронке
_fetch_serp_html_browser→ чинит заодногородской свип и обход новостроек (ревью: ещё 44 убитых прогона той же причиной; из 96
забаненных за 45 суток 90 несут текст сайдкар-503, настоящий firewall площадки — 4).
Где текст эпика расходится с данными
206читаютbannedкак «площадка палит паттерн полного обхода». Данные:площадка ни при чём, это отказ своего сайдкара на мёртвом env-прокси.
206замедлил джобу (
request_delay_sec 1.0 → 7.0, такт 1 → 7) на основании этого чтения.формально да по статусу, но 02.08 и 03.08 прогоны собрали данные (2334 и 362 лота)
и умерли под конец, а не на старте.
Статус
bannedна такой ошибке остаётся ложью. Здесь не трогаю: набор статусовограничен
scrape_runs_status_check, аmark_bannedв отличие отmark_failedсохраняетчекпоинт
done_buckets. Отдельная задача.Как проверить эффект на проде
avito_full_load_exhaustive09.08 14:02 UTC,avito_full_load10.08 14:16 UTC. Раньше — ручной smoke:UPDATE scrape_schedules SET next_run_at = now() WHERE source = 'avito_full_load';(подхват ≤60с, scheduler в
tradein-scraper).unique_fetchedпочти не сдвинется: первые страницыбакета и так полные, прогон с окном 2 уже собирал 2804 лота. Ждать «9-10 тысяч» —
значит увидеть 4 тысячи и решить, что фикс не сработал. Считать надо полосу, которой
раньше не было видно: Ориентир: у exhaustive run 2990 полоса
[D-7, D-3]= 1128 лотов,[D-2, D]= 147.У последнего инкрементального прогона с окном 2 (run 3074, оборвался на 350 строках)
— 144 и 24 соответственно, то есть полоса набиралась только попутно, полными страницами.
_paginate_incremental_bracketвtradein-scraper:avito: <room> [lo, hi] paginated=N pages … incremental since=… stop=….Ждём рост
paginatedна плотных бакетах иstop=early-stop (page all older than since)вместо мгновенной остановки.
tradein-scraper:avito page=N sidecar soft-ban (status=503) — retry in 2.Xs (left=…)— раньше такойстроки не было ни одной, сразу шла
sidecar error budget exhausted.Тесты
tradein-mvp/backend/tests/test_2674_avito_full_load_coverage.py— 13 тестов.Фальсификация патч-методом (откат правки → прогон):
test_window_widened_to_cadenceassert 2 == 7)test_null_interval_days_does_not_crashTypeError: int(None))test_sidecar_503_retried_before_giving_uptest_sidecar_503_recovers_without_aborting_bucketAvitoRateLimitedError)test_window_wider_than_cadence_left_alone,test_daily_cadence_keeps_window,test_no_window_stays_exhaustive,test_exhaustive_job_ignores_window_paramsUPDATEпроверяется на проде (п. 1)Полный прогон
tradein-mvp/backend: 3492 passed, 9 skipped.tests/test_search_api.py::test_search_cache_hitпадает (401 вместо 200) и на чистомorigin/main— предсуществующее, не из этого PR, задеселекчено в прогоне.Refs #2674